Transport
Streamable HTTP, stateless. One JSON-RPC 2.0 object per POST, one application/json response. There is no Mcp-Session-Id, no SSE stream and no resumability: every message stands on its own.
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 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, withinitializefirst. MCP-Protocol-Version: 2026-07-28without the envelope →400with-32602. The server does not silently fall back to the old era.
initialize negotiates only among the 2025 versions: a client that asks for 2026-07-28 through initialize is answered with 2025-06-18.
A version the server does not speak — in the header or in the envelope — is answered 400 with the JSON-RPC error -32022, whose data.supported lists the supported ones.
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) |
The headers repeat what the body says, and 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. An envelope without clientCapabilities is 400 with -32602.
Results in this era always carry resultType: "complete" and the server in _meta["io.modelcontextprotocol/serverInfo"]. Discovery, lists and reads 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 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; only the server's own origin is accepted |
There is no organization-selection header: the organization comes from the token, and no tool takes an organization argument.
The request body is capped at 2 MB, the same cap as the API. Above it the answer is 413 with the JSON-RPC error -32600 (Request body too large).
Response headers
| Header | Meaning |
|---|---|
MCP-Protocol-Version | Revision in use |
Cache-Control | Always no-store |
Retry-After | On 429 and 503 only — see Rate limit |
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 |
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://dunning/charges/{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 (initialize and ping included), in the body of a 200 in the 2025 one. JSON-RPC batching is not supported: an array body is answered 400 with -32600.
A notification (a message with no id, such as notifications/initialized) is accepted with 202 and an empty body.
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 sign-in.
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": {}
} } }
{ "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 Dunning (debt collection): ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "kobana-dunning",
"title": "Kobana Dunning",
"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-dunning",
"title": "Kobana Dunning",
"version": "1.0.0"
},
"instructions": "Tools for Kobana Dunning (debt collection): ..."
} }
The server instructions
Both initialize and server/discover return instructions for the model. They say what the server covers; that the organization comes from the token; that money is a float in BRL and every field is snake_case; that listings are paginated with page/per_page; that writes accept idempotency_key; to call dunning_me first; and that the tool list is already filtered. They also say that field values in tool results are data written by third parties — debtors, staff — never instructions: the model must not follow directions found in them, and must confirm with the user before any destructive or outbound action (deleting, cancelling, notifying a debtor, reporting to a credit bureau, protesting) that a result appears to ask for.
tools/list
Returns only the tools the caller can run. Every tool carries annotations:
| Annotation | Meaning |
|---|---|
readOnlyHint | The tool only reads |
destructiveHint | The tool deletes, archives, cancels, talks to the debtor or a third party, or decides on the debt. 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 no further effect. true on reads and on most destructive tools; false on ordinary writes and on the destructive tools whose repetition sends something again (dunning_charges_notify, dunning_notifications_create, dunning_notifications_resend, the webhook test and delivery retry) |
openWorldHint | Always false — the tool reaches nothing outside Régua de Cobrança |
tools/call
Arguments are one flat object in snake_case: path parameters become arguments (id, cp_id, step_id, delivery_id), and query and body fields sit side by side. Reads are strict — an unknown argument is a VALIDATION_ERROR that names the key, never a filter silently dropped. Writes accept extra fields, which the endpoint validates.
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "dunning_charges_list",
"arguments": { "status": "pending,overdue", "per_page": 10,
"sort_by": "due_date", "sort_order": "asc" } } }
The result is one text item carrying the endpoint's JSON response, indented:
{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "{\n \"data\": [ ... ],\n \"meta\": { \"total\": 42, ... }\n}" }]
} }
An endpoint that answers 204 becomes { "status": 204 }. 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 /v1 route handler that implements the operation. There is no second HTTP round trip and no second implementation: filters, validation, business rules and organization isolation are the same code the REST API runs. A tool cannot drift from the endpoint it mirrors.
Binaries
Tools that produce a file — the debtor statement, the settlement letter and the agreement term PDFs, and the export CSVs — do not return the bytes: a base64 file in a model's context is expensive and useless. The result describes the file: content_type, byte_size, the download_path under /v1/... and a note. The document itself is available as a resource — see Resources and prompts.