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:
| Version | Era | Status |
|---|---|---|
2026-07-28 | Stateless, no handshake β discovery through server/discover | Supported |
2025-06-18 | Handshake through initialize | Supported |
2025-03-26 | Handshake through initialize | Supported |
The era is decided by the request body, 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 β the 2025 era, 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 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 key | Required | Meaning |
|---|---|---|
io.modelcontextprotocol/protocolVersion | yes | 2026-07-28 |
io.modelcontextprotocol/clientCapabilities | yes | Client capabilities (an object; {} will do). Declaring elicitation is what lets a destructive tool be confirmed in the chat. |
io.modelcontextprotocol/clientInfo | recommended | Client 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:
| Header | Must equal |
|---|---|
MCP-Protocol-Version | The envelope version |
Mcp-Method | The message method |
Mcp-Name | Only 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β
| Header | Required | Meaning |
|---|---|---|
Authorization | yes | Bearer <access token> issued for the MCP β see Authentication |
Content-Type | yes | application/json |
Accept | recommended | application/json |
MCP-Protocol-Version | yes in era 2026-07-28; recommended in 2025 | Protocol revision |
Mcp-Method | yes in era 2026-07-28 | The message method |
Mcp-Name | yes on tools/call in era 2026-07-28 | The tool name |
Origin | conditional | Sent 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β
| Header | Meaning |
|---|---|
MCP-Protocol-Version | Revision in use |
Cache-Control | Always no-store |
Retry-After | On 429 and 503 β see Rate Limiting |
Methodsβ
| Method | Era 2026-07-28 | Era 2025 | What for |
|---|---|---|---|
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 | The tools this caller can run, with JSON Schema and annotations |
tools/call | yes | yes | Runs a tool |
resources/list | yes | yes | The fixed-URI documents this caller can read |
resources/templates/list | yes | yes | The URI templates (kobana://kadu/tasks/{id}, β¦) this caller can read |
resources/read | yes | yes | Reads a document by URI |
prompts/list | yes | yes | The ready-made scripts this caller can use |
prompts/get | yes | yes | Builds a script 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 β 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:
| Annotation | Meaning |
|---|---|
readOnlyHint | The tool only reads |
destructiveHint | The 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 |
idempotentHint | Repeating 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 |
openWorldHint | Always 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_createanswers with an upload URL;PUTthe content to it and callkadu_files_confirm. Then pass the id ininput_file_idsofkadu_tasks_run. - Downloading a file:
kadu_files_downloadanswers with a short-lived download URL.