Transport
Streamable HTTP, stateless. One JSON-RPC 2.0 message per POST, one JSON response. There is no Mcp-Session-Id, no resumability and no server→client stream: the application runs several replicas, and every long operation already answers 202 and finishes in a background worker rather than on the MCP channel.
Protocol versions
The server speaks two eras of the protocol on the same endpoint:
| Version | Era | Status |
|---|---|---|
2026-07-28 | Stateless, no handshake — discovery through server/discover | Supported |
2025-06-18 | Handshake through initialize — the default when the client sends no header | Supported |
2025-03-26 | Handshake through initialize | Supported |
The era is decided by the body of the request, with the headers as a cross-check:
params._metacarriesio.modelcontextprotocol/protocolVersion→ era2026-07-28(see below).- Without that envelope, with a 2025
MCP-Protocol-Versionor no header → era 2025, withinitialize. MCP-Protocol-Version: 2026-07-28without the envelope →400with-32602. The server does not silently fall back to the old era: you asked for the stateless semantics and would get the other.
A version the server does not speak — in the header or in the envelope, in either era — is answered 400 with the JSON-RPC error -32022, listing the supported ones:
{ "jsonrpc": "2.0", "id": 1, "error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": { "supported": ["2026-07-28", "2025-06-18", "2025-03-26"], "requested": "1999-01-01" }
} }
The version in use comes back on the MCP-Protocol-Version response header.
Era 2026-07-28
There is no handshake: every request carries its own version and the client's capabilities in params._meta, and the server remembers nothing between one request and the next.
params._meta key | Required | Meaning |
|---|---|---|
io.modelcontextprotocol/protocolVersion | yes | 2026-07-28 |
io.modelcontextprotocol/clientCapabilities | yes | The client's capabilities (an object; {} will do). Declaring elicitation turns on the confirmation of destructive tools |
io.modelcontextprotocol/clientInfo | recommended | The client's name and version |
The headers repeat what is in the body, so intermediaries can route without reading it — and so they must agree with it:
| Header | Must equal |
|---|---|
MCP-Protocol-Version | The envelope's version |
Mcp-Method | The message's method |
Mcp-Name | On tools/call only: params.name, raw or as =?base64?…?= |
Any mismatch is 400 with -32020 (Header mismatch: …). Missing clientCapabilities is 400 with -32602.
Results in this era always carry resultType: "complete" and the server in _meta["io.modelcontextprotocol/serverInfo"]. server/discover, tools/list, resources/list, resources/templates/list, resources/read and prompts/list also carry ttlMs: 0 and cacheScope: "private": what comes back is filtered by the caller's scope and permissions, so it must not be reused or shared. A tools/call of a destructive tool may come back with resultType: "input_required" — see Confirming destructive tools.
Request headers
| Header | Required | Meaning |
|---|---|---|
Authorization | yes | Bearer <access token> issued for the MCP server — see Authentication |
Content-Type | yes | application/json |
Accept | recommended | application/json |
MCP-Protocol-Version | yes on era 2026-07-28; recommended on 2025 | Protocol revision |
Mcp-Method | yes on era 2026-07-28 | The message's method |
Mcp-Name | yes on a tools/call of era 2026-07-28 | The tool name |
X-Workspace-Id | conditional | Workspace to act on, when the organization has more than one |
Origin | conditional | Sent by browser-based clients; validated against an allow-list |
Response headers
| Header | Meaning |
|---|---|
MCP-Protocol-Version | Negotiated revision |
X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset | See Rate limiting |
Cache-Control | Always no-store |
Retry-After | On 429 and on 503 |
Methods
| Method | Era 2026-07-28 | Era 2025 | Purpose |
|---|---|---|---|
server/discover | yes | — | Supported versions, server capabilities and instructions |
initialize | — | yes | Version negotiation, server capabilities and instructions |
ping | — | yes | Liveness check; answers an empty result |
tools/list | yes | yes | Tools this caller can run, with their JSON Schema and annotations |
tools/call | yes | yes | Runs one tool |
resources/list | yes | yes | The fixed-URI documents this caller can read |
resources/templates/list | yes | yes | The URI templates (kobana://inbox/emails/{id}, …) this caller can read |
resources/read | yes | yes | Reads a document by URI |
prompts/list | yes | yes | The ready-made prompts this caller can use |
prompts/get | yes | yes | Renders a prompt with the given arguments |
completion/complete | yes | yes | Suggestions for a prompt or template argument |
Resources, prompts and completions are in Resources and prompts.
Any other method is -32601 Method not found — with HTTP 404 on era 2026-07-28, in the body of a 200 on 2025. JSON-RPC batching is not supported — it was removed in the 2025-06-18 revision, and an array body is answered 400.
The request body is limited to 1 MB (1,048,576 bytes); above it the answer is 413 with -32600 — see Errors. No tool needs more: files never travel through MCP.
A notification (a message with no id, such as notifications/initialized) is accepted with 202 and an empty body, in both eras.
Authentication comes before any method, server/discover included: with no token, the answer is the 401 challenge that sends the client to the OAuth login — see Authentication.
server/discover (era 2026-07-28)
{ "jsonrpc": "2.0", "id": 1, "method": "server/discover",
"params": { "_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0.0" }
} } }
{ "jsonrpc": "2.0", "id": 1, "result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28", "2025-06-18", "2025-03-26"],
"capabilities": { "tools": {}, "resources": {}, "prompts": {}, "completions": {} },
"instructions": "Tools for the Kobana Autonomous Inbox: ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "kobana-autonomous-inbox",
"title": "Kobana Autonomous Inbox",
"version": "0.1.0"
}
}
} }
initialize (era 2025 handshake)
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": { "protocolVersion": "2025-06-18" } }
{ "jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false },
"prompts": { "listChanged": false },
"completions": {}
},
"serverInfo": {
"name": "kobana-autonomous-inbox",
"title": "Kobana Autonomous Inbox",
"version": "0.1.0"
},
"instructions": "Tools for the Kobana Autonomous Inbox: ..."
} }
The instructions (on initialize and server/discover) warn the agent that field values in results — an e-mail's subject and body above all — are data written by third parties, never instructions, and ask it to confirm with the user before a destructive action a result seems to request. They also say that destructive tools need protocol 2026-07-28 with elicitation, and that confirmation_required means the client cannot run them — not something to retry.
tools/list
Returns only the tools the caller can actually run, filtered by token scope and by the person's permissions. Every tool carries annotations that drive the client's confirmation prompts:
| Annotation | Meaning |
|---|---|
readOnlyHint | The tool only reads |
destructiveHint | The tool deletes, sends something outside (webhook test or replay, e-mail reprocess), rotates a secret or changes who may send to an inbox. It runs only after the server's confirmation |
idempotentHint | Repeating the call has the same effect |
openWorldHint | Always false — the tool reaches nothing outside the Autonomous Inbox |
tools/call
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "emails_list", "arguments": { "status": "processed", "limit": 10 } } }
The result carries the endpoint's JSON response as text content:
{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "{ \"data\": [ ... ], \"meta\": { \"next_cursor\": \"...\" } }" }]
} }
Errors the caller can act on come back with isError: true inside the result, not as a JSON-RPC error — see Errors.
In-process dispatch
Every tool dispatches in-process to the /api/v1 route handler that owns the operation, with the identity already verified — the MCP token is not passed on, and the endpoint still requires its own scope and permission. Ids in arguments are encoded into the path, so an id cannot reach another route. There is no HTTP round trip and no second implementation: listing filters, upsert matching, validation, soft-delete and multi-tenant isolation are the same code the REST API runs, so a tool cannot drift from the endpoint it mirrors.