Transporte
Streamable HTTP, stateless. Un mensaje JSON-RPC 2.0 por POST, una respuesta JSON. No hay Mcp-Session-Id, no hay resumability y no hay stream servidor→cliente: la aplicación corre con varias réplicas, y toda operación larga ya responde 202 y termina en un worker en segundo plano, no en el canal MCP.
Versiones de 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 — es la predeterminada cuando el cliente no envía el header | Soportada |
2025-03-26 | Handshake por initialize | Soportada |
La era la decide el body de la solicitud, con los headers como verificación cruzada:
params._metallevaio.modelcontextprotocol/protocolVersion→ era2026-07-28(ver abajo).- Sin ese envelope, con un
MCP-Protocol-Versionde 2025 o sin header → era 2025, coninitialize. MCP-Protocol-Version: 2026-07-28sin el envelope →400con-32602. El servidor no vuelve en silencio a la era anterior: pediste la semántica stateless y recibirías la otra.
Una versión que el servidor no habla — en el header o en el envelope, en cualquiera de las eras — recibe 400 con 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 el header de respuesta MCP-Protocol-Version.
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 la siguiente.
Clave de params._meta | Obligatorio | Significado |
|---|---|---|
io.modelcontextprotocol/protocolVersion | sí | 2026-07-28 |
io.modelcontextprotocol/clientCapabilities | sí | Las capacidades del cliente (un objeto; {} sirve). Declarar elicitation activa la confirmación de tools destructivas |
io.modelcontextprotocol/clientInfo | recomendado | name y version del cliente |
Los headers repiten lo que está en el body, para que los intermediarios puedan enrutar sin leerlo — y por eso tienen que coincidir con él:
| Header | Debe ser igual a |
|---|---|
MCP-Protocol-Version | La versión del envelope |
Mcp-Method | El method del mensaje |
Mcp-Name | Solo en tools/call: params.name, tal cual o como =?base64?…?= |
Cualquier discrepancia es 400 con -32020 (Header mismatch: …). La ausencia de clientCapabilities es 400 con -32602.
Los resultados en esta era siempre llevan resultType: "complete" y el servidor en _meta["io.modelcontextprotocol/serverInfo"]. server/discover, tools/list, resources/list, resources/templates/list, resources/read y prompts/list también llevan ttlMs: 0 y cacheScope: "private": lo que vuelve está filtrado por el scope y los permisos de quien llama, así que no debe reutilizarse ni compartirse. Un tools/call de una tool destructiva puede volver con resultType: "input_required" — ver Confirmación de tools destructivas.
Headers de solicitud
| Header | Obligatorio | Significado |
|---|---|---|
Authorization | sí | Bearer <access token> emitido para el servidor MCP — ver Autenticación |
Content-Type | sí | application/json |
Accept | recomendado | application/json |
MCP-Protocol-Version | sí en la era 2026-07-28; recomendado en 2025 | Revisión del protocolo |
Mcp-Method | sí en la era 2026-07-28 | El método del mensaje |
Mcp-Name | sí en un tools/call de la era 2026-07-28 | El nombre de la tool |
X-Workspace-Id | condicional | Workspace sobre el que actuar, cuando la organización tiene más de uno |
Origin | condicional | Lo envían los clientes de navegador; se valida contra una lista permitida |
Headers de respuesta
| Header | Significado |
|---|---|
MCP-Protocol-Version | Revisión negociada |
X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset | Ver Límite de solicitudes |
Cache-Control | Siempre no-store |
Retry-After | En 429 y en 503 |
Métodos
| Método | Era 2026-07-28 | Era 2025 | Propósito |
|---|---|---|---|
server/discover | sí | — | Versiones soportadas, capacidades del servidor e instrucciones |
initialize | — | sí | Negociación de versión, capacidades del servidor e instrucciones |
ping | — | sí | Comprobación de vida; responde un resultado vacío |
tools/list | sí | sí | Tools que quien llama puede ejecutar, con su JSON Schema y sus annotations |
tools/call | sí | sí | Ejecuta una tool |
resources/list | sí | sí | Los documentos de URI fija que quien llama puede leer |
resources/templates/list | sí | sí | Las plantillas de URI (kobana://inbox/emails/{id}, …) que quien llama puede leer |
resources/read | sí | sí | Lee un documento por URI |
prompts/list | sí | sí | Los prompts listos que quien llama puede usar |
prompts/get | sí | sí | Renderiza un prompt 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 — con HTTP 404 en la era 2026-07-28, en el body de un 200 en 2025. El batching JSON-RPC no está soportado — se eliminó en la revisión 2025-06-18, y un body en array recibe 400.
El body de la solicitud está limitado a 1 MB (1.048.576 bytes); por encima, la respuesta es 413 con -32600 — ver Errores. Ninguna tool necesita más: los archivos nunca viajan por MCP.
Una notificación (mensaje sin id, como notifications/initialized) se acepta con 202 y cuerpo vacío, en ambas eras.
La autenticación va antes de cualquier método, server/discover incluido: sin token, la respuesta es el desafío 401 que lleva al cliente al login OAuth — ver Autenticación.
server/discover (era 2026-07-28)
{ "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 the Kobana Autonomous Inbox: ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "kobana-autonomous-inbox",
"title": "Kobana Autonomous Inbox",
"version": "0.1.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": "kobana-autonomous-inbox",
"title": "Kobana Autonomous Inbox",
"version": "0.1.0"
},
"instructions": "Tools for the Kobana Autonomous Inbox: ..."
} }
Las instructions (en initialize y en server/discover) advierten al agente de que los valores de los campos en los resultados — sobre todo el asunto y el cuerpo de un correo — son datos escritos por terceros, nunca instrucciones, y le piden confirmar con el usuario antes de una acción destructiva que un resultado parezca solicitar. También dicen que las tools destructivas exigen el protocolo 2026-07-28 con elicitation, y que confirmation_required significa que el cliente no puede ejecutarlas — no es algo para reintentar.
tools/list
Devuelve solo las tools que quien llama puede ejecutar, filtradas por el scope del token y por los permisos de la persona. Cada tool lleva annotations, que guían las confirmaciones que el cliente pide:
| Annotation | Significado |
|---|---|
readOnlyHint | La tool solo lee |
destructiveHint | La tool elimina datos, envía algo hacia fuera (prueba o reenvío de webhook, reprocesamiento de correo), rota un secreto o cambia quién puede enviar a una inbox. Solo se ejecuta tras la confirmación del servidor |
idempotentHint | Repetir la llamada tiene el mismo efecto |
openWorldHint | Siempre false — la tool no alcanza nada fuera de Inbox Autónomo |
tools/call
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "emails_list", "arguments": { "status": "processed", "limit": 10 } } }
El resultado lleva la respuesta JSON del endpoint como contenido de texto:
{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "{ \"data\": [ ... ], \"meta\": { \"next_cursor\": \"...\" } }" }]
} }
Los errores sobre los que quien llama puede actuar vuelven con isError: true dentro del resultado, no como error JSON-RPC — ver Errores.
Despacho in-process
Cada tool despacha in-process al route handler de /api/v1 dueño de la operación, con la identidad ya verificada — el token MCP no se reenvía, y el endpoint sigue exigiendo su propio scope y permiso. Los ids de los argumentos se codifican en el path, así que un id no puede alcanzar otra ruta. No hay round trip HTTP ni una segunda implementación: filtros de listado, emparejamiento de upsert, validación, soft-delete y aislamiento multi-tenant son el mismo código que ejecuta la API REST, así que una tool no puede divergir del endpoint que refleja.