English · Español
02 — Arquitectura MCP: servidor, cliente, transporte¶
🇪🇸 MCP es JSON-RPC 2.0 con cuatro verbos que importan, dos transportes principales (stdio y SSE/HTTP), y una negociación inicial de capacidades. Eso es todo. La complejidad aparente del SDK es plumbing alrededor de esa idea minúscula.
Esta página deriva MCP (Model Context Protocol) desde primeros principios. Al final, Borja puede leer la especificación oficial de MCP y el código fuente del SDK de Anthropic sin pestañear.
Tres actores¶
+----------+ +-----------+ +------------+
| Agent | <--> | Client | <--> | Server |
| (Phase | | (Phase 31 | | (Phase 31 |
| 32) | | mcp_ | | mcp_ |
| | | client) | | server) |
+----------+ +-----------+ +------------+
^ |
| JSON-RPC 2.0 |
+----- over stdio -----+
- Agente (Fase 32): la cosa que toma decisiones. Usa el cliente como API.
- Cliente (Fase 31): traduce "quiero llamar a la tool X" a peticiones JSON-RPC; envía/recibe sobre un transporte; devuelve resultados.
- Servidor (Fase 31): expone un registro de tools (y opcionalmente recursos, prompts); responde peticiones JSON-RPC.
Para la Fase 31, el agente todavía no existe. El cliente del lab 02 es un script que finge ser el agente: lista tools, llama a una, imprime el resultado, sale. La Fase 32 sustituye el script por un bucle de agente real usando la misma interfaz de cliente.
La capa de transporte¶
MCP define dos transportes principales:
-
stdio. El servidor es un subproceso del cliente. El cliente escribe mensajes JSON-RPC al stdin del servidor; el servidor escribe respuestas a stdout. Los logs y errores van a stderr. Este es el transporte más simple y seguro: no hay red, no hay auth, no hay puerto que filtrar. El cliente controla el ciclo de vida del proceso del servidor. La Fase 31 usa este exclusivamente.
-
HTTP en streaming / SSE. El servidor es un servicio HTTP. El cliente envía peticiones vía POST; las respuestas vuelven vía Server-Sent Events en streaming. Este es el transporte usado cuando el host de la tool es remoto (otra máquina, servicio de otro equipo). La Fase 33 usa esto para servir el agente en sí — pero el host de tools se queda en stdio.
La elección del transporte no afecta al contenido de los mensajes. Los mismos envoltorios JSON-RPC en cualquier caso.
Framing de JSON-RPC 2.0 (stdio)¶
Cada mensaje es un objeto JSON precedido por una cabecera Content-Length (estilo LSP):
La cabecera dice al receptor cuántos bytes del cuerpo JSON siguen. Sin ella, el receptor no sabe dónde termina un mensaje y empieza el siguiente. JSON delimitado por newline sería más simple pero se rompe con strings multi-línea dentro del cuerpo JSON.
El lab 01 de la Fase 31 implementa exactamente este framing. Borja escribe un par read_message(stream) y write_message(stream, msg) que maneja el protocolo cabecera + cuerpo. Esta es la parte más propensa a error de la fase.
Los cuatro verbos que importan¶
MCP tiene muchos métodos. Para la Fase 31, solo estos cuatro:
initialize¶
El primer mensaje que envía un cliente. Negocia la versión del protocolo y anuncia capacidades.
// Client → Server
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"clientInfo": {"name": "miniagent-client", "version": "0.1"}
}
}
// Server → Client
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "miniagent-server", "version": "0.1"}
}
}
Después de initialize, el cliente envía una notification initialized (sin id, sin respuesta esperada) para señalar que está listo para recibir mensajes.
tools/list¶
El cliente pregunta al servidor "¿qué tools expones?".
// Client → Server
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }
// Server → Client
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "conjugate",
"description": "Return the conjugated form of an English verb.",
"inputSchema": {
"type": "object",
"properties": {
"verb": {"type": "string", "enum": [...20 verbs...]},
"tense": {"type": "string", "enum": [...5 tenses...]},
"person": {"type": "string", "enum": [...3 persons...]}
},
"required": ["verb", "tense", "person"]
}
},
{ "name": "lookup_irregular_verb", ... },
{ "name": "lookup_spanish", ... },
{ "name": "check_subject_verb_agreement", ... }
]
}
}
El cliente ahora conoce el catálogo de tools. Usará estos schemas para construir llamadas y (en la Fase 32) para alimentárselos al modelo como declaraciones de tools.
tools/call¶
El cliente invoca una tool específica con argumentos.
// Client → Server
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "conjugate",
"arguments": {"verb": "eat", "tense": "past_simple", "person": "3sg"}
}
}
// Server → Client (success)
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{"type": "text", "text": "ate"}],
"isError": false
}
}
// Server → Client (tool-level error)
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{"type": "text", "text": "verb 'run' is out of scope (§A13)"}],
"isError": true
}
}
// Server → Client (protocol-level error: unknown tool)
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Unknown tool: 'unkn_tool'"
}
}
Las dos formas de error importan (theory/01-function-calling-formats.md §"Errores"). La forma result.isError es recuperable; la forma del campo error no.
Notifications¶
Las notifications son mensajes sin id y sin respuesta esperada. Las dos que usamos:
notifications/initialized— cliente → servidor, tras acusar recibo deinitialize.notifications/tools/list_changed— servidor → cliente, si el catálogo de tools cambia en tiempo de ejecución. (La Fase 31 no cambia el catálogo en tiempo de ejecución; lo mencionamos por completitud.)
La traza del wire de una sesión típica¶
1. Client spawns server subprocess.
2. Client → Server: initialize (gets back capabilities)
3. Client → Server: notifications/initialized
4. Client → Server: tools/list (gets back the 4 tools)
5. Client → Server: tools/call conjugate (gets back "ate")
6. Client → Server: tools/call lookup_spanish english_form="ate" (gets back "comió")
7. Client closes server's stdin.
8. Server exits.
Este es exactamente el transcript que produce el lab 02 de la Fase 31. El report de la fase incluye el byte stream literal como prueba.
Qué es una "capability"¶
En initialize, ambos lados anuncian capabilities. La única capability anunciada por la Fase 31 es tools. Otras capabilities de MCP (resources, prompts, sampling) no son anunciadas por nuestro servidor, lo que significa que el cliente no debería enviar esas llamadas a método. Si lo hace, el servidor devuelve error.code = -32601 (Method not found).
Esta negociación es lo que hace a MCP compatible hacia adelante: un cliente más nuevo hablando con un servidor más viejo solo usa capabilities que ambos lados anunciaron.
Recursos y prompts (mencionados, no implementados)¶
- Recursos. Datos de solo lectura que el servidor expone — ficheros, URIs, etc. Útil para "dame la tabla de verdad §A13 como un documento markdown". No implementamos esto; la tabla de verdad vive en código, no como recurso.
- Prompts. Plantillas de prompt reutilizables que el servidor sugiere. Útiles para herramientas que quieren prompts pre-cocinados ("explica el past simple de un verbo regular"). No implementamos esto; el agente en la Fase 32 tiene sus propios prompts.
Qué hace el SDK por ti¶
El SDK Python mcp de Anthropic proporciona:
- Modelos Pydantic para cada tipo de mensaje.
- Una clase
Serverque subclasificas y sobre la que registras tools con decoradores. - Un context manager
ClientSessionque maneja spawn, initialize y cleanup. - I/O asíncrono (está construido sobre
anyio).
La Fase 31 hace nada de esto en la implementación a mano. Lo hacemos explícitamente, en ~200 líneas por proceso, para ver los bytes. El lab 02 tiene un objetivo opcional: portar al SDK y comparar el número de líneas.
Síncrono vs async¶
El SDK es async. La implementación a mano de la Fase 31 es síncrona (bloqueando en sys.stdin.read). Esto es aceptable para stdio con un cliente; los servidores de producción que manejan muchos clientes sobre HTTP deben ser async (Fase 33).
Qué NO cubre esta página¶
- Authn/authz. Eso es
theory/03-authn-authz.md. - Detalles del transporte HTTP / SSE. Fase 33.
- La lista completa de métodos MCP. Cubrimos los cuatro que importan; la especificación en https://modelcontextprotocol.io tiene el resto si pica la curiosidad.
- Peticiones iniciadas por el servidor. Algunas variantes de MCP permiten al servidor llamar de vuelta al cliente (
sampling/createMessage). No implementamos esto; nuestro servidor es puramente reactivo.
Siguiente: theory/03-authn-authz.md — modelos de permisos y qué nos da stdio gratis.