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 ya responde de inmediato y termina en un worker en segundo plano, no en el canal MCP.
Versiones del protocolo
El servidor habla dos eras del protocolo en el mismo endpoint:
| Versión | Era | Estado |
|---|---|---|
2026-07-28 | Stateless, sin handshake — se descubre con server/discover | Soportada |
2025-06-18 | Handshake con initialize — la versión en la que initialize cierra por defecto | Soportada |
2025-03-26 | Handshake con initialize | Soportada |
La era se decide por el cuerpo de la petición, con las cabeceras como verificación:
params._metatraeio.modelcontextprotocol/protocolVersion→ era2026-07-28(ve 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 a la era antigua en silencio: 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, en cualquier era — se responde con 400 y el error JSON-RPC -32022, listando 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.
Era 2026-07-28
No hay handshake: cada petición lleva su propia versión y las capacidades del cliente en params._meta, y el servidor no recuerda nada de una petición a otra.
Clave de params._meta | Obligatoria | Significado |
|---|---|---|
io.modelcontextprotocol/protocolVersion | sí | 2026-07-28 |
io.modelcontextprotocol/clientCapabilities | sí | Capacidades del cliente (un objeto; {} sirve) |
io.modelcontextprotocol/clientInfo | recomendada | name y version del cliente |
Las cabeceras repiten lo que dice 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 discrepancia es 400 con -32020 (Header mismatch: …). Si falta clientCapabilities, es 400 con -32602.
Los resultados de esta era siempre traen resultType: "complete" y el servidor en _meta["io.modelcontextprotocol/serverInfo"]. server/discover, tools/list, resources/list, resources/templates/list, resources/read y prompts/list traen además ttlMs: 0 y cacheScope: "private": lo que vuelve se filtra por 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" — ve Confirmación de tools destructivas.
Cabeceras de la petición
| Cabecera | Obligatoria | Significado |
|---|---|---|
Authorization | sí | Bearer <access token> emitido para el MCP — ve 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 tools/call de la era 2026-07-28 | El nombre de la tool |
Origin | condicional | Enviada por clientes de navegador; validada contra una allowlist |
No hay cabecera de selección de inquilino: la organización viene del propio token, y ninguna tool acepta un argumento de organización.
El cuerpo de la petición tiene un límite de 1 MB. Por encima de eso — por el Content-Length declarado o por lo que de hecho 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 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 | Solo en 429 — ve 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 su 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://dashboards/portal_users/{id}, …) que este llamador puede leer |
resources/read | sí | sí | Lee un documento por su 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 — con HTTP 404 en la era 2026-07-28; en el cuerpo de un 200 en la de 2025. El batching JSON-RPC no es soportado — fue removido en la revisión 2025-06-18, y un cuerpo en array se responde con 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, incluido server/discover: sin token, la respuesta es el 401 con el desafío que lleva al cliente al login OAuth — ve 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 Dashboard Mágico: ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "kobana-dashboards",
"title": "Dashboard Mágico",
"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": "kobana-dashboards",
"title": "Dashboard Mágico",
"version": "1.0.0"
},
"instructions": "Tools for Dashboard Mágico: ..."
} }
tools/list
Devuelve solo las tools que el llamador puede ejecutar, filtradas por los permisos de panel de quien autorizó el cliente. Cada tool lleva annotations, que es lo que hace que el cliente pida confirmación al usuario:
| Anotación | Significado |
|---|---|
readOnlyHint | La tool solo lee |
destructiveHint | La tool borra, cierra, suspende o desactiva un registro, o envía un email o una invitación. El servidor pide confirmación en la era 2026-07-28 y rechaza la tool (confirmation_required) a un cliente que no puede confirmar |
idempotentHint | Repetir la llamada tiene el mismo efecto |
openWorldHint | Siempre false — la tool no alcanza nada fuera de Dashboard Mágico |
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"] } } }
El resultado trae la respuesta JSON del endpoint como contenido de texto:
{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "{ \"data\": [ ... ], \"meta\": { \"total\": 42, ... } }" }]
} }
Los errores sobre los que el llamador puede actuar vuelven con isError: true dentro del resultado, no como error JSON-RPC — ve Errores.
Despacho in-process
Cada tool despacha in-process al handler de /api/v1 que implementa la operación. No hay segundo viaje HTTP ni segunda implementación: filtros de listado, reglas de upsert, validación, soft-delete y aislamiento por organización son el mismo código que corre la API REST. Una tool no puede divergir del endpoint que refleja.