Skip to main content
2026-07-28 is the revision to write against, and two older ones still answer for clients that have not moved. This page is the wire contract; for the tools themselves see MCP Tools. Most people never read it. The bundled tablepro-mcp stdio bridge speaks this for you, and every client in MCP Clients goes through the bridge. Read on if you are writing a client against the HTTP endpoint directly.

One round trip

Everything below is a detail of this request.

Endpoint

One path, one method:
The server binds 127.0.0.1 and speaks plain HTTP, which the specification permits for a server on the same machine, and there is no remote mode. GET and DELETE on /mcp answer 405 Method Not Allowed with Allow: POST, OPTIONS. OPTIONS answers 204 for CORS preflight. Any other path answers 404. The Host header must name a loopback host (localhost, 127.0.0.1, ::1) and, if it carries a port, the port the server bound. Anything else answers 403 forbidden_host. Origin, when present, must be https://claude.ai or https://app.cursor.com. Any other origin answers 403 forbidden_origin. A request with no Origin is fine, which is the normal case for a local client. Read the port from the handshake file at ~/Library/Application Support/TablePro/mcp-handshake.json. Do not hardcode it.

Protocol versions

2025-03-26 is no longer accepted. It requires JSON-RPC batching, which TablePro never implemented. server/discover serves the live list in supportedVersions. Read it from there rather than copying it into your client. An unsupported version comes back as error -32022 with the supported list in data.supported.

Every request carries its version

There is no handshake in the modern era: no initialize, no Mcp-Session-Id, no ping, no GET stream, no SSE resumability. Each POST stands on its own, so it has to say who is calling and what it speaks. Two _meta fields are required on every request:
  • io.modelcontextprotocol/protocolVersion (required): a supported version string. Missing or empty is -32602; unsupported is -32022.
  • io.modelcontextprotocol/clientCapabilities (required): a JSON object. {} is valid and means you support nothing extra. A non-object is -32602.
  • io.modelcontextprotocol/clientInfo (optional): name, title, version. It names your client in the activity log and in Connected Clients.
  • progressToken (optional): string or integer. See Progress.
  • traceparent, tracestate, baggage (optional): W3C trace context, carried through untouched.
Declaring elicitation in clientCapabilities changes how TablePro asks for approval of a write. See Approvals.

Required headers

The modern era mirrors part of the body into headers so a proxy can route and audit without parsing JSON. TablePro checks that the copies agree, and rejects any disagreement with -32020. Mcp-Param-<field> is opt-in: send it and it is validated, omit it and nothing happens. The name after the prefix is matched case-insensitively against the arguments object for tools/call and against params for everything else, descending into nested objects. Values compare as text against strings, numerically against numbers, and as true/false against booleans. An array or object value never matches.

Non-ASCII header values

HTTP headers are ASCII. A table name with an accent, or a value with a leading space, is wrapped:
The payload between =?base64? and ?= is standard base64 of the UTF-8 bytes, and TablePro decodes the wrapper before comparing to the body. Wrap only when you have to: a value that is already printable ASCII with no leading or trailing space goes as is. A malformed wrapper, or a header carrying a control character, is -32020.

Methods

An unknown method answers HTTP 404 with JSON-RPC -32601. A method that exists but is modern-only answers the same way to a legacy client. notifications/cancelled is the only notification the server acts on. Every other notification is accepted with 202 and dropped.

server/discover

Replaces initialize for stateless clients. It takes no parameters beyond _meta and needs no scope, so it is the first call a client should make.
instructions is the same text a legacy initialize returns. It tells the model to start from list_connections, to call describe_table before writing SQL, and to treat a refusal as the user’s policy rather than a transient error. The stdio bridge calls server/discover before it forwards anything, and refuses to start if the server does not list 2026-07-28.

Result envelope

Every modern result carries resultType:
  • "complete": the normal case. The payload is the answer.
  • "input_required": the server needs the user to approve something. See Approvals.
Only tools/call, resources/read and prompts/get may return input_required. Modern results also carry _meta.io.modelcontextprotocol/serverInfo. Legacy results carry neither field.

Cache hints

Six methods return ttlMs and cacheScope so a client can cache without guessing: public means the answer does not depend on the token. private means it does: tools/list is filtered by the token’s scopes, and a resource read by its connection allowlist. A ttlMs of 0 means do not cache. No other method carries a cache hint, and a hint that reaches a result from a method that may not carry one is stripped before it goes out.

Pagination

tools/list, prompts/list, resources/list and resources/templates/list page at 50 items. When more remain, the result carries nextCursor; pass it back as params.cursor for the next page. A cursor is opaque and bound to the method that issued it. Handing a tools/list cursor to prompts/list is -32602.

Progress

Send _meta.progressToken on a request and the server streams notifications/progress back on that request’s own response stream. The response switches to text/event-stream, progress notifications arrive as SSE frames with no event name, and the final JSON-RPC response is the last frame. There is no separate GET stream to open, and no notification ever arrives out of band.
progress must advance; an update that does not move forward is dropped. execute_query reports at 0.0, 0.3 and 1.0.

Cancellation

Send notifications/cancelled with the requestId of a request that is still running:
The notification is answered with 202 whether or not anything matched. The cancelled request finishes with -33002. Cancellation is per principal: a token can only cancel its own requests. Revoking a token cancels everything it has in flight. The server also cancels for you. Every handler has a 330-second deadline, and the server polls for a dropped connection once a second, so a client that walks away does not leave a query running. subscriptions/listen is the one method that deadline does not reach.

Approvals and input required

A statement that writes needs the user’s consent. Where that consent is collected depends on what your client declared in clientCapabilities. Without elicitation, TablePro shows its own confirmation dialog on the user’s Mac and the call blocks until they answer. With it, the call returns resultType: "input_required" instead:
Present the form, then repeat the same call with the answer and the state echoed back:
requestState is signed and bound to the token, the method, and a digest of the parameters. Change any argument between the two calls and it is rejected with -32602. It expires after 5 minutes. An action other than accept, or approved: false, fails the call with a denied tool error. A call that carries inputResponses or requestState is never cached, whatever the method’s normal hint says.

Errors

TablePro’s own error codes live in -33000 and above. The -32000 block is frozen by the specification and TablePro no longer uses it. The three codes in -32020 and below are defined by the specification itself. Resource-not-found is -32602, not a code of its own. An unknown method is HTTP 404. 401 responses carry a WWW-Authenticate challenge: Bearer realm="TablePro" for a missing token, Bearer error="invalid_token" for an unknown or revoked one, and error_description="token expired" when it expired. A 403 from a scope check carries Bearer error="insufficient_scope" and lists the scopes in data.requiredScopes.

Tool errors are not protocol errors

A tool that fails because the database said no returns a successful JSON-RPC result with isError: true:
The text starts with a machine-readable code: invalid_argument, not_connected, not_found, denied, timeout, unsupported, query_failed, user_cancelled, or internal_failure. Error text is redacted before it leaves the process, so a driver message that echoed a password does not reach the client. Protocol errors are reserved for the envelope: a malformed request, a missing scope, an expired token. A client that only handles JSON-RPC errors reads every failed query as a success.

Transport limits

Per-caller request rates, auth lockouts and pairing lockouts are counted separately and are listed under rate limits.

Legacy era

A request with no io.modelcontextprotocol/protocolVersion in _meta is treated as legacy, and legacy still works the old way:
  1. POST /mcp with initialize and a protocolVersion of 2025-11-25 or 2025-06-18. Any other value is -32022.
  2. The response carries protocolVersion, capabilities, serverInfo and instructions.
  3. Every later request carries Mcp-Session-Id. A session belongs to the token that created it; presenting it under another token is -33001.
  4. A session lives until the server stops. The store holds at most 16 and evicts the least recently used past that. A session the server does not recognize is -33001, and the client has to initialize again.
Legacy clients see resources.subscribe: false and cannot call server/discover or subscriptions/listen. Their results carry no resultType, no cache hints, and no serverInfo in _meta. Legacy is there so an old client keeps working, not as a target to build against.