Módulo 2: El protocolo de cable — JSON-RPC sobre stdio
Módulo 2: El protocolo de cable — JSON-RPC sobre stdio
Descripción
En el Módulo 1 viste, ejecutado, un servidor MCP de juguete y un cliente mínimo que le pedía su lista de tools. Funcionó — pero quedó una caja negra a propósito: ¿qué es exactamente lo que viaja entre el cliente y el servidor? ¿Cómo sabe el servidor que le llegó un mensaje completo y no la mitad de otro? ¿Por qué el cliente y el servidor tienen que "presentarse" antes de poder pedirse nada?
Este módulo abre esa caja. MCP no inventa un formato de mensajes propio: usa JSON-RPC 2.0, un estándar de más de una década, para darle forma a cada request, cada respuesta y cada notificación. Y no inventa tampoco cómo viajan esos mensajes en el caso local: usa stdio — los mismos stdin/stdout que cualquier proceso de Unix ya tiene — con una regla de disciplina no negociable. Y antes de que cliente y servidor puedan pedirse nada útil, tienen que completar un handshake: initialize → respuesta con capacidades → notifications/initialized. Vas a implementar las tres piezas a mano, con Python 3.14 puro, y vas a ejecutarlas de verdad — un proceso hijo real, hablándole a su padre por sus pipes, en el mismo protocolo que usa Claude Code para hablarle a cualquier servidor MCP.
Al terminar las 8 lecciones vas a poder: nombrar los tres tipos de mensaje de JSON-RPC 2.0 y sus campos exactos; explicar por qué stdout es territorio exclusivo del protocolo y qué pasa cuando alguien lo ensucia (lo vas a romper a propósito, y lo vas a arreglar); ejecutar el handshake initialize/initialized byte a byte contra un servidor real; y distinguir un request de una notification con la seguridad de quien ya vio la diferencia en el código, no solo en la teoría.
Regla dura de este módulo (léela antes de seguir)
El wire protocol se ejecuta de verdad. No hay ninguna simulación en este módulo: cada JSON que vas a leer en un bloque "Qué esperar" salió de correr subprocess.Popen, escribir una línea en el stdin de un proceso hijo, y leer la línea que ese proceso hijo escribió en su stdout. Lo único que sigue siendo concepto en toda esta guía es la decisión de un modelo (claude-sonnet-5) de "quiero hablar con este servidor" o "quiero usar esta tool" — algo que en este módulo ni siquiera aparece todavía, porque aquí no hay tools. Este módulo es puro protocolo: el formato de los mensajes y el ritual de saludo antes de poder pedir nada.
Un detalle que se repite en cada lección: el JSON-RPC que escribimos a mano con json.dumps/json.loads es exactamente el mismo que produce y consume el SDK oficial mcp (PyPI). No es una versión simplificada para enseñar — es el protocolo real, byte a byte. Lo que el SDK aporta encima es azúcar sintáctica: una clase de servidor que ya sabe hacer el handshake por ti, para que no escribas el if method == "initialize" a mano en producción. Cada lección señala el punto exacto donde esa azúcar reemplazaría tu código.
Dónde estamos en la guía
MCP a fondo — el servidor y cliente MCP de Reservo
├── Módulo 1: Qué es MCP y por qué existe
│ → El problema M×N, la arquitectura host/client/server, tu primer servidor de juguete
├── Módulo 2: El protocolo de cable — JSON-RPC sobre stdio ← ESTÁS AQUÍ
│ → JSON-RPC 2.0, el transporte stdio, el handshake initialize/initialized
├── Módulo 3: Tools sobre MCP
├── Módulo 4: Resources — contexto y datos
├── Módulo 5: Prompts — plantillas reutilizables
├── Módulo 6: Construir un cliente MCP y descubrimiento
├── Módulo 7: Conectar con Claude Code y servidores de terceros
└── Módulo 8: Proyecto — el servidor MCP de Reservo completo
Este es el Módulo 2 de 8. Es la base de cable sobre la que se paran los tres primitivos que vienen después. tools/list, resources/read, prompts/get — cada uno de esos métodos que vas a ver en M3, M4 y M5 es, por debajo, exactamente el mismo tipo de mensaje JSON-RPC que vas a construir a mano en este módulo, viajando por el mismo transporte stdio, después del mismo handshake. Dominar este módulo significa que M3-M5 ya no tienen que enseñarte "cómo viaja un mensaje" — solo "qué dice cada mensaje nuevo".
Analogía: dos radios que acuerdan frecuencia antes de hablar
Imagina dos operadores de radio que necesitan coordinarse, cada uno con su propio equipo. Antes de que uno le diga al otro "recibido, cambio", tienen que hacer tres cosas, en orden:
- Acordar la frecuencia y el código. Uno propone: "te hablo en la frecuencia X, con el protocolo Y, ¿puedes recibirme así?" — eso es
initialize. - El otro confirma qué frecuencia y qué capacidades tiene disponibles, y si puede trabajar con la propuesta — eso es la respuesta a
initialize, con sus propiascapabilities. - El primero confirma que escuchó la respuesta y que ya está listo para operar — un breve "recibido, empezamos" que no espera respuesta — eso es
notifications/initialized.
Recién después de esos tres pasos, cualquiera de los dos puede transmitir información real. Si alguien intentara pedir algo antes del tercer paso, sería como gritar por la radio antes de que el otro confirmase que te escucha: técnicamente el mensaje sale, pero nadie garantiza que del otro lado están preparados para procesarlo.
MCP hace exactamente esto. initialize es la propuesta de frecuencia (versión de protocolo + capacidades + quién eres). La respuesta del servidor es la confirmación con sus propias capacidades. notifications/initialized es el "recibido, empezamos" — una notificación, no una pregunta, así que no espera respuesta. Esta lección y las siguientes son ese ritual, byte a byte.
El caso que seguimos: Reservo, ahora por el protocolo de cable
agent-fundamentals-and-tool-calling-guide te dejó un agente con cuatro tools de Reservo (get_quote, list_rooms, book_room, cancel_booking) viviendo hardcodeadas dentro del proceso del agente. M1 de esta guía te mostró por qué conviene exponerlas por un servidor reutilizable en vez de eso. Este módulo no toca todavía esas cuatro tools — eso es M3 —, pero sí construye la infraestructura que las va a cargar: un servidor MCP de Reservo (reservo-mcp-server, versión 1.0.0) y un cliente MCP de Reservo (reservo-mcp-client, versión 1.0.0) que se dan la mano por stdio, hablando el protocolo 2025-06-18.
Cada handshake que ejecutes en este módulo va a usar esos mismos nombres fijos — son el ancla de identidad de todo el resto de la guía.
Prerequisitos
Conocimiento requerido:
- ✅ Haber completado (o leído) el Módulo 1: qué es MCP, la arquitectura host/client/server, por qué existe.
- ✅ Python básico: funciones, diccionarios, manejo de archivos/streams (
for line in stream). - ✅ Saber qué es JSON como formato de datos (objetos, listas, strings, números).
Recomendado:
- ✅ Haber usado
subprocessalguna vez, aunque sea para correr un comando simple.
NO requerido:
- ❌ No necesitas conocer JSON-RPC de antemano: esta lección lo enseña desde cero.
- ❌ No necesitas el SDK oficial
mcpinstalado — no lo vamos a instalar (requiere red). Todo el código de este módulo es stdlib puro. - ❌ No necesitas saber todavía qué son tools/resources/prompts en detalle: eso es M3-M5.
Entorno:
- ✅ Python 3.14.0 con su librería estándar:
json,subprocess,sys,itertools. Nada que instalar. - ✅ Una terminal donde puedas correr scripts de Python y ver su salida.
Roadmap del módulo
Lección 01 — Introducción al módulo (esta)
El mapa del módulo, la analogía de las dos radios, la regla dura de que todo el wire protocol se ejecuta de verdad.
Lección 02 — JSON-RPC 2.0: el formato de los mensajes
Los tres tipos de mensaje (request, response, notification) y sus campos exactos (jsonrpc, id, method, params, result/error). Construidos y parseados a mano, ejecutado.
Lección 03 — El transporte stdio
Cómo viajan los mensajes entre procesos: uno por línea, stdout exclusivo para JSON-RPC válido, stderr libre para logs. Se ejecuta un servidor real por subprocess.Popen, y se rompe (¡a propósito!) la regla del stdout para ver exactamente qué pasa.
Lección 04 — El handshake initialize
El primer mensaje real de MCP: qué lleva el request (protocolVersion, capabilities, clientInfo) y qué devuelve el servidor (capabilities, serverInfo). Ejecutado contra el servidor de Reservo.
Lección 05 — La notificación notifications/initialized
Por qué el tercer paso del handshake es una notification y no un request: sin id, sin respuesta, fire-and-forget. Ejecutado, y por qué importa el orden.
Lección 06 — Requests, responses y notifications a fondo
Cierra la taxonomía: cómo se emparejan requests con responses por id, la forma exacta de un error JSON-RPC, y dos errores reales provocados contra el servidor de Reservo.
Lección 07 — Un handshake completo, ejecutado
El handshake entero (initialize → respuesta → initialized) en una sola corrida, con el JSON citado byte a byte de principio a fin, y el log del servidor por stderr al lado.
Lección 08 — Mini-proyecto: un handshake MCP mínimo
Armas tu propio servidor y cliente que hacen el handshake completo, con menos código que el de las lecciones — la versión mínima que sigue siendo protocolo correcto.
Mapa de progresión
Lección 01 (esta) → El mapa: JSON-RPC + stdio + handshake
Lección 02 → El formato de los mensajes
Lección 03 → El transporte y su regla dura
Lección 04 → initialize (el request)
Lección 05 → notifications/initialized (la notification)
Lección 06 → Requests/responses/notifications + errores
Lección 07 → El handshake completo, de punta a punta
Lección 08 → Proyecto: tu propio handshake mínimo
Dificultad: ⭐⭐ ──────────────────▶ ⭐⭐⭐
Qué lograrás en este módulo
Al completar las 8 lecciones, podrás:
- Nombrar los tres tipos de mensaje de JSON-RPC 2.0 y explicar qué campo los distingue.
- Construir y parsear un mensaje JSON-RPC a mano, con
json.dumps/json.loads. - Explicar la regla del transporte stdio: por qué
stdoutes solo para JSON-RPC válido y qué rompe si no se respeta — habiéndolo roto y arreglado tú mismo. - Ejecutar el handshake
initializecontra un servidor real y leer su respuesta campo por campo. - Explicar por qué
notifications/initializedno llevaidy qué diferencia una notification de un request. - Provocar y leer un error JSON-RPC (
Method not found,Invalid params) con sucodeymessageexactos. - Correr el handshake completo de punta a punta, con dos procesos reales hablándose por stdio.
El antes y después
ANTES del módulo:
→ "MCP tiene su propio formato de mensajes"
→ "El cliente y el servidor simplemente empiezan a hablar"
→ "stdout es donde el proceso escribe lo que quiera"
→ "Una notification es un request con menos código"
DESPUÉS del módulo:
→ MCP usa JSON-RPC 2.0, un estándar de más de una década, sin reinventarlo
→ Hay un handshake obligatorio: initialize -> respuesta -> initialized, en ese orden
→ stdout es territorio EXCLUSIVO de mensajes MCP válidos; los logs van a stderr, siempre
→ Una notification NO lleva "id" y NUNCA espera respuesta -- es una categoría distinta
Trampas a evitar al cursar este módulo
1. "JSON-RPC es algo específico de MCP"
No. JSON-RPC 2.0 es un estándar independiente, anterior a MCP, usado por muchos otros protocolos. MCP lo eligió como formato de mensaje base porque ya resuelve bien el problema de "cómo estructuro un pedido, una respuesta y un aviso" — no tuvo que inventar nada ahí. La lección 02 muestra la especificación tal cual es, sin adornos de MCP.
2. "Puedo meter un print() de debug donde quiera, ya lo voy a limpiar después"
No en stdout del proceso servidor. La lección 03 te lo muestra roto de verdad: un solo print() sin file=sys.stderr corrompe la primera línea que el cliente intenta leer, y el json.loads explota con una excepción real. No es una advertencia teórica — vas a ver el traceback.
3. "El handshake es un detalle de implementación, puedo saltarlo e ir directo a pedir tools"
No. El handshake negocia qué versión de protocolo y qué capacidades tienen ambas partes antes de que tenga sentido pedir nada. El servidor de este módulo, de hecho, rechaza cualquier método que no sea initialize/notifications/initialized con un error Method not found — vas a provocar ese error tú mismo en la lección 06.
4. "Una notification es solo un request al que no le importa la respuesta"
No del todo: una notification no tiene id, y por eso el que la recibe no puede (ni debe) responderla — no hay a qué id responder. Es una categoría de mensaje distinta en la especificación, no un request "relajado". La lección 05 lo aísla.
5. "Ya puedo usar tools/list o resources/read, los vi en la documentación de MCP"
Todavía no en este módulo. Este servidor implementa únicamente el handshake; cualquier otro método devuelve un error. Los primitivos llegan en M3 (tools), M4 (resources) y M5 (prompts). Aquí es protocolo base, nada más.
Cómo trabajar este módulo
- Corre cada script tú mismo. Cada "Qué esperar" es la salida real de correr el código con Python 3.14. Reproducirlo con tus manos vale más que leerlo.
- No te saltes la lección del error a propósito (03). Ver el
JSONDecodeErrorreal, con su traceback completo, es lo que hace que la regla delstdoutdeje de ser un consejo abstracto. - El mini-proyecto es la síntesis. La lección 08 te pide reconstruir el handshake completo con tus propias manos, más chico que el ejemplo guiado. Es la base sobre la que M3 va a agregar
tools/list.
Tiempo estimado:
Lección 01 (esta) → 15 min lectura
Lección 02 → 20 min + correr la demo
Lección 03 → 25 min + correr la demo (incl. el error a propósito)
Lección 04 → 20 min + correr la demo
Lección 05 → 15 min + correr la demo
Lección 06 → 20 min + correr la demo
Lección 07 → 15 min + correr la demo
Lección 08 → 30 min + armar tu propio handshake
Total: ~2.5-3 horas
Evidencia de éxito
Antes de avanzar al Módulo 3 (Tools sobre MCP), deberías poder:
- ✅ Nombrar los tres tipos de mensaje JSON-RPC y el campo que distingue a cada uno.
- ✅ Explicar, con tus palabras, por qué
stdoutes exclusivo del protocolo — y qué viste romperse cuando no lo fue. - ✅ Ejecutar el handshake
initialize/initializedcontra un servidor propio y leer la respuesta. - ✅ Provocar un error
Method not foundy un errorInvalid params, y explicar qué significa cadacode. - ✅ Construir, desde cero, un servidor y un cliente mínimos que completen el handshake.
Resumen
- Este módulo abre el protocolo de cable de MCP: JSON-RPC 2.0 como formato de mensajes, stdio como transporte local, y el handshake
initialize/initializedcomo ritual obligatorio antes de operar. - Todo se ejecuta de verdad: procesos reales hablándose por
subprocess.Popeny pipes, sin ninguna simulación. Lo único que sigue siendo concepto es la decisión de un LLM — que en este módulo ni siquiera interviene. - La analogía que sostiene el módulo: dos radios que acuerdan frecuencia (
initialize), confirman que se oyen (respuesta) y avisan que ya operan (initialized), en ese orden, antes de transmitir nada real. - El caso: el servidor
reservo-mcp-servery el clientereservo-mcp-client, ambos versión1.0.0, hablandoprotocolVersion: "2025-06-18". - Regla dura:
stdoutes territorio exclusivo de mensajes JSON-RPC válidos; los logs van SIEMPRE astderr. Vas a romperla y arreglarla con tus manos.
Siguiente lección: 02 — JSON-RPC 2.0: el formato de los mensajes. Construimos y parseamos, a mano, los tres tipos de mensaje que va a usar todo el resto de la guía.
Recursos adicionales
- JSON-RPC 2.0 Specification — La especificación completa del formato de mensajes base que usa MCP; requests, responses, notifications y sus códigos de error.
- Model Context Protocol — Specification 2025-06-18: Base Protocol — La especificación de MCP que este módulo implementa a mano, mensaje por mensaje.
- MCP — Lifecycle — El handshake
initialize/initializeden detalle: qué negocia, en qué orden, qué pasa si se salta un paso. - MCP — Transports — La sección stdio: framing por línea, la regla de
stdoutexclusivo para mensajes MCP ystderrlibre para logs. - Python —
subprocess—Popen, pipes y cómo un proceso padre le habla a un proceso hijo; la base de todo el código ejecutado en este módulo. - Python —
json—dumps/loads, la serialización que convierte un diccionario de Python en una línea JSON-RPC y de vuelta.