Skip to main content

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 answers right away and finishes in the background, not on the MCP channel. Running a task (kadu_tasks_run), for example, queues the execution and answers at once; you follow it with kadu_executions_get.

Protocol versions​

The server speaks two protocol eras on the same endpoint:

VersionEraStatus
2026-07-28Stateless, no handshake β€” discovery through server/discoverSupported
2025-06-18Handshake through initializeSupported
2025-03-26Handshake through initializeSupported

The era is decided by the request body, with the headers as a cross-check:

  • params._meta carries io.modelcontextprotocol/protocolVersion β†’ era 2026-07-28 (see below).
  • Without that envelope, with a 2025 MCP-Protocol-Version or no header β†’ the 2025 era, with initialize.
  • MCP-Protocol-Version: 2026-07-28 without the envelope β†’ 400 with -32602. The server does not silently fall back to the old era: you asked for stateless semantics and would get the other ones.

A version the server does not speak β€” in the header or in the envelope β€” gets 400 and 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 in the response's MCP-Protocol-Version header. An initialize asking for a version outside the 2025 era gets 2025-06-18; 2026-07-28 is discovered through server/discover.

Era 2026-07-28​

There is no handshake: every request carries its own version and the client capabilities in params._meta, and the server remembers nothing between one request and the next.

params._meta keyRequiredMeaning
io.modelcontextprotocol/protocolVersionyes2026-07-28
io.modelcontextprotocol/clientCapabilitiesyesClient capabilities (an object; {} will do). Declaring elicitation is what lets a destructive tool be confirmed in the chat.
io.modelcontextprotocol/clientInforecommendedClient name and version. The name shows on the approval-by-link screen.

The headers repeat what is in the body, so intermediaries can route without reading it β€” and so they have to agree with it:

HeaderMust equal
MCP-Protocol-VersionThe envelope version
Mcp-MethodThe message method
Mcp-NameOnly on tools/call: params.name, raw or as =?base64?…?=

Any mismatch is 400 with -32020 (Header mismatch: …). A missing clientCapabilities is 400 with -32602.

Results in this era always carry resultType 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 permissions, so it must not be reused or shared. A tools/call of a destructive tool may return resultType: "input_required" β€” see Confirming destructive tools.

Request headers​

HeaderRequiredMeaning
AuthorizationyesBearer <access token> issued for the MCP β€” see Authentication
Content-Typeyesapplication/json
Acceptrecommendedapplication/json
MCP-Protocol-Versionyes in era 2026-07-28; recommended in 2025Protocol revision
Mcp-Methodyes in era 2026-07-28The message method
Mcp-Nameyes on tools/call in era 2026-07-28The tool name
OriginconditionalSent by browser clients; only the server's own or localhost is accepted

There is no organization-selection header: it comes from the token itself, and no tool takes an organization argument.

The request body is limited to 1 MB, the same as the API. Above that β€” by the declared Content-Length or by what actually arrives β€” the answer is 413 with { "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": "Request body too large (limit 1048576 bytes)" } }. No tool receives file content (see Files), so a legitimate call stays far below that.

Response headers​

HeaderMeaning
MCP-Protocol-VersionRevision in use
Cache-ControlAlways no-store
Retry-AfterOn 429 and 503 β€” see Rate Limiting

Methods​

MethodEra 2026-07-28Era 2025What for
server/discoveryesβ€”Supported versions, server capabilities and instructions
initializeβ€”yesVersion negotiation, server capabilities and instructions
pingβ€”yesLiveness check; answers an empty result
tools/listyesyesThe tools this caller can run, with JSON Schema and annotations
tools/callyesyesRuns a tool
resources/listyesyesThe fixed-URI documents this caller can read
resources/templates/listyesyesThe URI templates (kobana://kadu/tasks/{id}, …) this caller can read
resources/readyesyesReads a document by URI
prompts/listyesyesThe ready-made scripts this caller can use
prompts/getyesyesBuilds a script with the given arguments
completion/completeyesyesSuggestions for a prompt or template argument

Resources, prompts and completions are in Resources and prompts.

Any other method is -32601 Method not found β€” in era 2026-07-28, with HTTP 404; in 2025, in the body of a 200. JSON-RPC batching is not supported β€” it was removed in revision 2025-06-18, and an array body gets 400.

A notification (a message without id, such as notifications/initialized) is accepted with 202 and an empty body, in both eras.

Authentication comes before any method, server/discover included: without a token the answer is the 401 with the challenge that takes the client to the OAuth sign-in β€” see Authentication.

server/discover (era 2026-07-28)​

POST / HTTP/1.1
Authorization: Bearer <access token>
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: server/discover
{ "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 Kadu, the Kobana financial agent: ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "kadu", "title": "Kadu", "version": "1.0.0" }
}
} }

initialize (2025-era 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": "kadu", "title": "Kadu", "version": "1.0.0" },
"instructions": "Tools for Kadu, the Kobana financial agent: ..."
} }

The instructions guide the model: the organization comes from the token, fields are snake_case, listings move by cursor, running a task only queues the execution, writes accept idempotency_key, and what comes from an execution is data, never instructions.

tools/list​

Returns only the tools the caller can run, filtered by the token's scope and the person's role. Each tool carries title, description, inputSchema (JSON Schema) and annotations:

AnnotationMeaning
readOnlyHintThe tool only reads
destructiveHintThe tool deletes, cancels, runs the agent, replays a webhook, rotates a secret, or changes who can run a task or where data goes. The server asks for confirmation in the chat in era 2026-07-28 with elicitation; any other client gets an approval link, and nothing runs until you choose Continue
idempotentHintRepeating the call has the same effect. False for kadu_tasks_run, kadu_executions_continue and kadu_webhook_deliveries_replay: each repeated call is one more execution or delivery
openWorldHintAlways false β€” the tool only calls the Kadu API

tools/call​

{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "kadu_executions_list",
"arguments": { "status": "failed", "limit": 10 } } }

The result carries the route's JSON response as text content:

{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "{ \"data\": [ ... ], \"meta\": { \"next_cursor\": \"…\", \"limit\": 10 } }" }]
} }

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​

Each tool dispatches in-process to the /api/v1 handler that implements the operation, with the identity the token has already proven. There is no second HTTP hop and no second implementation: validation, filters, the execution queue, the plan, the cost limit, soft delete and organization isolation are the same code the API runs. A tool cannot drift from the route it mirrors. The MCP token is never forwarded to the API.

Files​

No tool returns or receives bytes: a base64 file in a model's context is expensive and useless. Files travel through presigned URLs:

  • Uploading an input file: kadu_files_create answers with an upload URL; PUT the content to it and call kadu_files_confirm. Then pass the id in input_file_ids of kadu_tasks_run.
  • Downloading a file: kadu_files_download answers with a short-lived download URL.