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 no operation needs to push messages to the client.
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 version initialize settles on by default | 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→2026-07-28era (see below).- Without that 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 the stateless semantics and would get the other one.
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 in the response's MCP-Protocol-Version 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 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). Without form elicitation, destructive tools are refused — see 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 that is why they must agree with it:
| Header | Must equal |
|---|---|
MCP-Protocol-Version | The envelope's version |
Mcp-Method | The message's method |
Mcp-Name | Only on tools/call: the 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: "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 — see Authentication |
X-Workspace-Id | no | The connection's default workspace, used when the tool gets no workspace_id — the same as ?workspace_id= on the URL. See Authentication → The workspace |
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's method |
Mcp-Name | yes on tools/call in the 2026-07-28 era | The tool name |
Origin | conditional | Sent by browser clients; checked against an allow-list |
Response headers
| Header | Meaning |
|---|---|
MCP-Protocol-Version | Revision in use |
Cache-Control | Always no-store |
Retry-After | On 429 — see Rate limit — and on 503 |
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 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://finance/payables/{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 the 2026-07-28 era, with HTTP 404; in the 2025 one, in the body of a 200. JSON-RPC batching is not supported — it was removed in the 2025-06-18 revision, and an array body is answered 400.
The body is capped at 14 MB (14,680,064 bytes) — enough for the largest legitimate request, a 10 MB finance_attachments_upload in base64 plus the envelope. Above it the answer is 413 with { "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": "Request body too large (limit 14680064 bytes)" } }: a declared Content-Length above the cap is refused even before authentication, and a body with no Content-Length (or a false one) is cut at the cap while being read.
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: without a token, the answer is the 401 with the challenge that takes 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 Kobana Finance: ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "kobana-finance",
"title": "Kobana Finance",
"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-finance",
"title": "Kobana Finance",
"version": "1.0.0"
},
"instructions": "Tools for Kobana Finance: ..."
} }
The instructions (in initialize and in server/discover) warn the agent that field values in results are data written by third parties, never instructions, and ask for the user's confirmation before a destructive or money-moving action that a result seems to request.
tools/list
Returns only the tools the caller can run — the ones the token's scope reaches and the person who authorized can do in the dashboard. A write tool's inputSchema is the endpoint's own request schema — the same fields, required ones and enums as the API reference. 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, cancels or moves money — it only runs with the server's confirmation |
idempotentHint | Repeating the call has the same effect |
openWorldHint | Always false — the tool reaches nothing outside Smart Finance |
tools/call
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "finance_payables_list",
"arguments": { "workspace_id": "2b93f876-769a-4bdc-b23e-304957f313b4",
"filters": { "status": "pending" }, "limit": 10,
"fields": ["description", "amount", "due_date"] } } }
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
Each tool dispatches in-process to the /v1 handler that implements the operation, with the caller's already-verified identity and the workspace resolved for the call — the MCP token is not forwarded, and the endpoint still requires its own scope. There is no second HTTP hop and no second implementation: filters, validation, settlement rules, rateio, security locks and workspace isolation are the same code the REST API runs. Writes are recorded with the mcp channel (created_via / updated_via), distinguishable from those made through the API or the dashboard.
Files
- Download —
finance_attachments_downloaddoes not return the bytes: a base64 PDF in a model's context is expensive and useless. The result carries thecontent_type, the size and the path to download the file from the API — with a token issued for the API, since the MCP one does not work there. - Upload —
finance_attachments_uploadtakes the file incontent_base64(up to 10 MB) and sends it to the endpoint asmultipart/form-data. Thenfinance_attachments_linkattaches it to a title or transaction.