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.
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://billing/invoices/{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 Kobana Billing: ...",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "kobana-billing",
"title": "Kobana Billing",
"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-billing",
"title": "Kobana Billing",
"version": "1.0.0"
},
"instructions": "Tools for Kobana Billing: ..."
} }
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, cancela o suspende |
idempotentHint | Repetir la llamada tiene el mismo efecto |
openWorldHint | Siempre false — la tool no alcanza nada fuera de Facturación Automática |
tools/call
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "billing_invoices_list",
"arguments": { "status": "open", "per_page": 10,
"fields": ["number", "total", "due_date"] } } }
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, prorrateo y aislamiento por organización son el mismo código que corre la API REST. Una tool no puede divergir del endpoint que refleja.
Binarios
billing_invoices_pdf, billing_nfes_pdf y billing_nfes_xml no devuelven los bytes: un PDF en base64 en el contexto de un modelo es caro e inútil. El resultado trae el content_type, el tamaño y la ruta para buscar el archivo en el host de la API, con un token emitido para la API.