Transporte
Streamable HTTP, stateless. Un mensaje JSON-RPC 2.0 por POST, una
respuesta JSON. No hay Mcp-Session-Id, no hay reanudación y no hay stream
servidor→cliente: la aplicación corre varias réplicas, y toda operación larga
responde de inmediato y termina en segundo plano, no en el canal MCP. Lanzar
una tarea (kadu_tasks_run), por ejemplo, pone la ejecución en la cola y
responde al instante; la sigues con kadu_executions_get.
Versiones del protocolo
El servidor habla dos eras del protocolo en el mismo endpoint:
| Versión | Era | Estado |
|---|---|---|
2026-07-28 | Stateless, sin handshake — descubrimiento por server/discover | Soportada |
2025-06-18 | Handshake por initialize | Soportada |
2025-03-26 | Handshake por initialize | Soportada |
La era se decide por el cuerpo de la solicitud, con las cabeceras como verificación:
params._metatraeio.modelcontextprotocol/protocolVersion→ era2026-07-28(mira abajo).- Sin ese sobre, con
MCP-Protocol-Versionde 2025 o sin la cabecera → era 2025, coninitialize. MCP-Protocol-Version: 2026-07-28sin el sobre →400con-32602. El servidor no cae en silencio a la era antigua: pediste la semántica stateless y recibirías la otra.
Una versión que el servidor no habla — en la cabecera o en el sobre — recibe
400 y el error JSON-RPC -32022, con la lista de las soportadas:
{ "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" }
} }
La versión en uso vuelve en la cabecera MCP-Protocol-Version de la respuesta.
Un initialize que pida una versión fuera de la era 2025 recibe 2025-06-18;
la 2026-07-28 se descubre por server/discover.
Era 2026-07-28
No hay handshake: cada solicitud lleva su propia versión y las capacidades
del cliente en params._meta, y el servidor no recuerda nada entre una
solicitud y otra.
Clave de params._meta | Obligatoria | Significado |
|---|---|---|
io.modelcontextprotocol/protocolVersion | sí | 2026-07-28 |
io.modelcontextprotocol/clientCapabilities | sí | Capacidades del cliente (un objeto; {} sirve). Declarar elicitation es lo que permite confirmar una tool destructiva en el chat. |
io.modelcontextprotocol/clientInfo | recomendada | name y version del cliente. El nombre aparece en la pantalla de aprobación por enlace. |
Las cabeceras repiten lo que está en el cuerpo, para que los intermediarios puedan enrutar sin leerlo — y por eso tienen que coincidir con él:
| Cabecera | Tiene que ser igual a |
|---|---|
MCP-Protocol-Version | La versión del sobre |
Mcp-Method | El method del mensaje |
Mcp-Name | Solo en tools/call: el params.name, crudo o como =?base64?…?= |
Cualquier diferencia es 400 con -32020 (Header mismatch: …). Si falta
clientCapabilities, es 400 con -32602.
Los resultados de esta era siempre traen resultType y el servidor en
_meta["io.modelcontextprotocol/serverInfo"]. server/discover, tools/list,
resources/list, resources/templates/list, resources/read y
prompts/list traen también ttlMs: 0 y cacheScope: "private": lo que
vuelve está filtrado por los permisos de quien llama, así que no debe
reutilizarse ni compartirse. Un tools/call de una tool destructiva puede
devolver resultType: "input_required" — mira
Confirmación de tools destructivas.
Cabeceras de la solicitud
| Cabecera | Obligatoria | Significado |
|---|---|---|
Authorization | sí | Bearer <access token> emitido para el MCP — mira Autenticación |
Content-Type | sí | application/json |
Accept | recomendada | application/json |
MCP-Protocol-Version | sí en la era 2026-07-28; recomendada en la de 2025 | Revisión del protocolo |
Mcp-Method | sí en la era 2026-07-28 | El método del mensaje |
Mcp-Name | sí en el tools/call de la era 2026-07-28 | El nombre de la tool |
Origin | condicional | La envían los clientes de navegador; solo se acepta la del propio servidor o de localhost |
No hay cabecera de selección de organización: viene del propio token, y ninguna tool acepta un argumento de organización.
El cuerpo de la solicitud tiene un límite de 1 MB, el mismo de la API. Por
encima — por el Content-Length declarado o por lo que de verdad llega — la
respuesta es 413 con { "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": "Request body too large (limit 1048576 bytes)" } }.
Ninguna tool recibe el contenido de un archivo (mira Archivos), así
que una llamada legítima queda muy por debajo.
Cabeceras de la respuesta
| Cabecera | Significado |
|---|---|
MCP-Protocol-Version | Revisión en uso |
Cache-Control | Siempre no-store |
Retry-After | En 429 y 503 — mira Límite de Peticiones |
Métodos
| Método | Era 2026-07-28 | Era 2025 | Para qué |
|---|---|---|---|
server/discover | sí | — | Versiones soportadas, capacidades del servidor e instrucciones |
initialize | — | sí | Negociación de versión, capacidades del servidor e instrucciones |
ping | — | sí | Prueba de vida; responde un resultado vacío |
tools/list | sí | sí | Las tools que este llamador puede ejecutar, con JSON Schema y anotaciones |
tools/call | sí | sí | Ejecuta una tool |
resources/list | sí | sí | Los documentos de URI fija que este llamador puede leer |
resources/templates/list | sí | sí | Las plantillas de URI (kobana://kadu/tasks/{id}, …) que este llamador puede leer |
resources/read | sí | sí | Lee un documento por la URI |
prompts/list | sí | sí | Los guiones listos que este llamador puede usar |
prompts/get | sí | sí | Arma un guion con los argumentos dados |
completion/complete | sí | sí | Sugerencias para un argumento de prompt o de plantilla |
Resources, prompts y completions están en Resources y prompts.
Cualquier otro método es -32601 Method not found — en la era 2026-07-28,
con HTTP 404; en la de 2025, en el cuerpo de un 200. El batching JSON-RPC
no está soportado — se eliminó en la revisión 2025-06-18, y un cuerpo en
array recibe 400.
Una notificación (mensaje sin id, como notifications/initialized) se
acepta con 202 y cuerpo vacío, en las dos eras.
La autenticación va antes de cualquier método, incluso de
server/discover: sin token, la respuesta es el 401 con el desafío que
lleva al cliente al inicio de sesión OAuth — mira
Autenticación.
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 (handshake de la era 2025)
{ "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: ..."
} }
Las instructions orientan al modelo: la organización viene del token, los
campos son snake_case, los listados avanzan por cursor, lanzar una tarea solo
pone la ejecución en la cola, las escrituras aceptan idempotency_key y lo que
viene de una ejecución es dato, nunca instrucción.
tools/list
Devuelve solo las tools que el llamador puede ejecutar, filtradas por el
alcance del token y el rol de la persona. Cada tool lleva title,
description, inputSchema (JSON Schema) y annotations:
| Anotación | Significado |
|---|---|
readOnlyHint | La tool solo lee |
destructiveHint | La tool elimina, cancela, ejecuta el agente, reenvía un webhook, rota un secreto o cambia quién puede lanzar una tarea o adónde van los datos. El servidor pide confirmación en el chat en la era 2026-07-28 con elicitation; a cualquier otro cliente le responde un enlace de aprobación, y nada corre hasta que eliges Continuar |
idempotentHint | Repetir la llamada tiene el mismo efecto. Falso en kadu_tasks_run, kadu_executions_continue y kadu_webhook_deliveries_replay: cada llamada repetida es una ejecución o una entrega más |
openWorldHint | Siempre false — la tool solo llama a la API de Kadu |
tools/call
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "kadu_executions_list",
"arguments": { "status": "failed", "limit": 10 } } }
El resultado trae la respuesta JSON de la ruta como contenido de texto:
{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "{ \"data\": [ ... ], \"meta\": { \"next_cursor\": \"…\", \"limit\": 10 } }" }]
} }
Los errores sobre los que el llamador puede actuar vuelven con isError: true
dentro del resultado, no como error JSON-RPC — mira Errores.
Despacho in-process
Cada tool despacha in-process al handler de /api/v1 que implementa la
operación, con la identidad que el token ya probó. No hay un segundo viaje HTTP
ni una segunda implementación: validación, filtros, cola de ejecuciones, plan,
tope de costo, soft delete y aislamiento por organización son el mismo código
que corre la API. Una tool no tiene cómo divergir de la ruta que refleja. El
token del MCP nunca se reenvía a la API.
Archivos
Ninguna tool devuelve ni recibe bytes: un archivo en base64 en el contexto de un modelo es caro e inútil. Los archivos viajan por URLs prefirmadas:
- Subir un archivo de entrada:
kadu_files_createresponde con una URL de subida; haz elPUTdel contenido en ella y llama akadu_files_confirm. Después, pasa el id eninput_file_idsdekadu_tasks_run. - Descargar un archivo:
kadu_files_downloadresponde con una URL de descarga de vida corta.