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 immediately 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 — discovered through server/discover | Supported |
2025-06-18 | initialize handshake — the version initialize settles on by default | Supported |
2025-03-26 | initialize handshake | Supported |
The era is decided by the request body, with the headers as a cross-check:
params._metacarriesio.modelcontextprotocol/protocolVersion→2026-07-28era (see below).- No such envelope, with a 2025
MCP-Protocol-Versionor no header at all → 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, 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.
The 2026-07-28 era
There is no handshake: every request carries its own version and the client's capabilities in params._meta, and the server remembers nothing from one request to 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) |
io.modelcontextprotocol/clientInfo | recommended | The client's name and version |
The headers repeat what the body says, so intermediaries can route without reading it — which is why they have to agree with it:
| Header | Must equal |
|---|---|
MCP-Protocol-Version | The envelope version |
Mcp-Method | The message method |
Mcp-Name | On tools/call only: params.name, raw or as =?base64?…?= |
Any disagreement 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 permissions, so it must not be reused or shared. A tools/call to 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 MCP — see Authentication |
Content-Type | yes | application/json |
Accept | recommended | application/json |
MCP-Protocol-Version | yes in the 2026-07-28 era; recommended in the 2025 one | Protocol revision |
Mcp-Method | yes in the 2026-07-28 era | The message method |
Mcp-Name | yes on tools/call in the 2026-07-28 era | The tool name |
Origin | conditional | Sent by browser-based clients; validated against an allow-list |
There is no tenant-selection header: the organization comes from the token itself, and no tool takes an organization argument.
The request body is capped at 1 MB. 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 takes a file, so a legitimate call stays far below it.
Response headers
| Header | Meaning |
|---|---|
MCP-Protocol-Version | Revision in use |
Cache-Control | Always no-store |
Retry-After | On 429 only — see Rate limiting |
Methods
| Method | 2026-07-28 era | 2025 era | 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 | The 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://dashboards/portal_users/{id}, …) this caller can read |
resources/read | yes | yes | Reads one document by URI |
prompts/list | yes | yes | The ready-made prompts this caller can use |
prompts/get | yes | yes | Renders one prompt with the given arguments |
completion/complete | yes | yes | Suggestions for a prompt or template argument |
Resources, prompts and completions are covered in Resources and prompts.
Any other method is -32601 Method not found — with HTTP 404 in the 2026-07-28 era, in the body of a 200 in the 2025 one. JSON-RPC batching is not supported — it was removed in the 2025-06-18 revision, and an array body is answered 400.
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 with the challenge that sends the client to the OAuth login — see Authentication.
server/discover (2026-07-28 era)
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 Magic Dashboard: ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "kobana-dashboards",
"title": "Magic Dashboard",
"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": "kobana-dashboards",
"title": "Magic Dashboard",
"version": "1.0.0"
},
"instructions": "Tools for Magic Dashboard: ..."
} }
tools/list
Returns only the tools the caller can run, filtered by the dashboard permissions of whoever authorized the client. Every tool carries annotations, which is what makes the client ask the user to confirm:
| Annotation | Meaning |
|---|---|
readOnlyHint | The tool only reads |
destructiveHint | The tool deletes, closes, suspends or deactivates a record, or sends an email or invitation. The server asks for confirmation on the 2026-07-28 era and refuses the tool (confirmation_required) to a client that cannot confirm |
idempotentHint | Repeating the call has the same effect |
openWorldHint | Always false — the tool reaches nothing outside Magic Dashboard |
tools/call
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "dashboards_portal_users_list",
"arguments": { "status": "active", "per_page": 10,
"fields": ["email", "status", "created_at"] } } }
The result carries the endpoint's JSON response as text content:
{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "{ \"data\": [ ... ], \"meta\": { \"total\": 42, ... } }" }]
} }
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 implements the operation. There is no second HTTP round trip and no second implementation: listing filters, upsert matching, validation, soft-delete and tenant isolation are the same code the REST API runs. A tool cannot drift from the endpoint it mirrors.