Módulo 7: Conectar con Claude Code y servidores de terceros
Registrar el servidor Reservo
Descripción
La lección anterior te dio la forma general de .mcp.json. Esta la hace concreta: la entrada real, completa, que registra reservo_full_mcp_server.py —el servidor con sus cuatro tools (M3), sus dos resources (M4) y el prompt plan_booking (M5), tal como quedó armado en el mini-proyecto del Módulo 6— para que un host como Claude Code lo pueda lanzar. Vas a escribir esa entrada, validarla, y después demostrar algo que hasta ahora solo afirmaste: cuando un host lee .mcp.json y lanza el command/args que ahí encuentra, el proceso que arranca es, exactamente, el mismo servidor que ya conoces de memoria — no una simulación, no una versión distinta. También vas a ver el claude mcp add que produciría esta misma entrada, explicado campo por campo.
Conexión con el módulo
Esta lección cierra la primera mitad del módulo (registrar tu propio servidor, de confianza total) antes de que la lección 05 en adelante empiece a hablar de servidores que no escribiste tú. Todo lo que sigue en las lecciones 05-08 usa esta misma mecánica —una entrada de mcpServers— aplicada a un caso con trust profile distinto.
La entrada completa
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": ["reservo_full_mcp_server.py"],
"env": {}
}
}
}
Cada campo, justificado con lo que ya sabes del servidor:
"reservo-mcp-server"(la clave) — el mismo nombre queSERVER_INFO["name"]dentro del servidor, fijado desde el Módulo 2 y reutilizado sin cambios en todos los módulos siguientes. No es una coincidencia de estilo: es útil que el nombre con el que registras un servidor en.mcp.jsoncoincida con el que ese servidor declara en su propioserverInfodurante el handshake, para que no haya ambigüedad sobre qué proceso corresponde a qué entrada cuando estés depurando un.mcp.jsoncon varios servidores (la lección 08 arma justamente ese caso)."type": "stdio"—reservo_full_mcp_server.pycorre siempre local, como subproceso, tal como estableció la lección 04 del Módulo 1. Nunca"http"para este servidor, en esta guía."command": "python3.14"— el ejecutable, exactamente como lo escribirías en tu terminal para correr el script tú mismo. No essys.executable(eso es una expresión de Python en tiempo de ejecución, no un string fijo en un archivo de configuración) — es el nombre del intérprete tal como el sistema operativo lo va a buscar en elPATH."args": ["reservo_full_mcp_server.py"]— un único argumento: la ruta al script. Si el archivo no estuviera en el mismo directorio desde el que Claude Code arranca el proceso, esta ruta necesitaría ser absoluta o relativa a un directorio conocido — un detalle de despliegue real que esta guía nombra pero no profundiza, porque depende de dónde vive cada proyecto."env": {}— vacío, y a propósito:reservo_full_mcp_server.pyno necesita ninguna credencial ni configuración externa. Guarda todo su estado (BOOKINGS, los contadores deitertools.count(1)) en memoria del propio proceso. Un servidor MCP real que hablara con una base de datos, en cambio, típicamente necesitaría aquí algo como{"DATABASE_URL": "..."}.
Ejemplo trabajado: confirmar que Claude Code lanzaría el mismo servidor de siempre
Hasta ahora, este módulo solo validó la forma de .mcp.json — nunca confirmó que, si un host de verdad tomara command y args de esa entrada y lanzara el proceso, el resultado fuera el servidor real de Reservo. Esta lección lo demuestra: un script que simula exactamente lo que hace un host —leer .mcp.json, lanzar command+args con subprocess.Popen, hacer el handshake initialize— y confirma, con el propio protocolo, que el serverInfo.name que responde el proceso coincide con el nombre bajo el que lo registraste.
# verify_registration.py
"""Simula lo que Claude Code haria al leer .mcp.json: lanzar el servidor con
command+args EXACTOS de la entrada, y confirmar que el nombre negociado en el
handshake coincide con la clave usada para registrarlo. Wire protocol real,
sobre el mismo reservo_full_mcp_server.py de los Modulos 2-6, sin cambiar nada."""
import json
import subprocess
import itertools
with open("mcp_config.json", "r", encoding="utf-8") as file:
config = json.load(file)
request_ids = itertools.count(1)
for server_name, entry in config["mcpServers"].items():
assert entry["type"] == "stdio", "este chequeo es solo para stdio"
proc = subprocess.Popen(
[entry["command"], *entry["args"]],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1,
)
request = {"jsonrpc": "2.0", "id": next(request_ids), "method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "reservo-mcp-client", "version": "1.0.0"}}}
proc.stdin.write(json.dumps(request) + "\n")
proc.stdin.flush()
response = json.loads(proc.stdout.readline())
negotiated_name = response["result"]["serverInfo"]["name"]
print(f"[.mcp.json] nombre registrado: {server_name!r}")
print(f"[handshake] serverInfo.name: {negotiated_name!r}")
print(f"coinciden: {server_name == negotiated_name}")
proc.stdin.close()
proc.wait(timeout=5)
Qué esperar (ejecutando python3.14 verify_registration.py en un directorio con mcp_config.json y reservo_full_mcp_server.py):
[.mcp.json] nombre registrado: 'reservo-mcp-server'
[handshake] serverInfo.name: 'reservo-mcp-server'
coinciden: True
Nota lo que este script no hizo: no importó nada de Claude Code, no simuló ninguna aplicación completa. Solo tomó entry["command"] y entry["args"] —dos valores sacados de un diccionario que vino de json.load— y los pasó directo a subprocess.Popen([entry["command"], *entry["args"]], ...). Es, literalmente, el mismo mecanismo que ya usaste en el Módulo 6 dentro de MCPClient.__init__ (subprocess.Popen([sys.executable, server_path], ...)), con una diferencia mínima: ahí el comando estaba fijo en el código (sys.executable); aquí viene de un archivo externo, leído en tiempo de ejecución. Esa es, en esencia, toda la magia de .mcp.json — no hay ningún mecanismo nuevo de arranque de procesos, solo una fuente de configuración distinta para el mismo subprocess.Popen que ya conoces.
El claude mcp add equivalente
# CONTENIDO -- se explica en detalle, NUNCA se ejecuta en esta guia.
claude mcp add reservo-mcp-server --scope project -- python3.14 reservo_full_mcp_server.py
Desarmado, campo por campo:
reservo-mcp-server— el nombre del servidor, el mismo que terminaría siendo la clave dentro demcpServers.--scope project— decide dónde vive el.mcp.jsonresultante y quién lo ve. Claude Code define tres alcances:local(por defecto si se omite el flag — personal, no se comparte ni se versiona),project(un.mcp.jsonen la raíz del repositorio, pensado para versionarse con git y compartirse con todo el equipo que trabaje en ese proyecto) yuser(disponible para ti en cualquier proyecto que abras, sin importar el repositorio). Para un servidor de Reservo que todo el equipo va a usar de la misma forma,projectes la elección natural — así queda documentado, en el propio repositorio, qué servidores MCP asume el proyecto.--— el separador estándar que le dice a la CLI "todo lo que sigue después de esto es el comando a ejecutar, no más flags declaude mcp add". Sin este separador,claude mcp addpodría interpretar malpython3.14como si fuera un flag propio.python3.14 reservo_full_mcp_server.py— el comando y sus argumentos, en el mismo formato en que los escribirías en una terminal — esto es exactamente lo que termina, separado, encommandyargsdentro del JSON.
Correr este comando, dentro de un proyecto real con Claude Code instalado, produciría —o extendería, si el archivo ya existía con otras entradas— un .mcp.json con la entrada exacta que escribiste a mano al principio de esta lección. Las variables de entorno se agregarían con flags adicionales (--env DATABASE_URL=postgres://..., repetible por cada variable) que terminan poblando el campo env — no hacen falta aquí, porque reservo_full_mcp_server.py no necesita ninguna.
Qué pasa después del registro (concepto, no ejecutado)
Con reservo-mcp-server registrado en .mcp.json, lo que Claude Code hace a continuación es, estructuralmente, exactamente lo que construiste en el Módulo 6: se comporta como un MCPClient más. Lanza el proceso con command/args, hace initialize/notifications/initialized, y después ejecuta el flujo de descubrimiento completo (tools/list + resources/list + prompts/list) para saber qué le ofrece el servidor — el mismo orden, los mismos tres métodos, que ya ejecutaste de punta a punta en la lección 05 del Módulo 6. La diferencia real no está en el protocolo —ese es idéntico— sino en quién decide, después del descubrimiento, qué usar: en tus scripts de los módulos anteriores, tu propio código de cliente decidía qué tools/call hacer; dentro de Claude Code, es el modelo (claude-sonnet-5, en una conversación real) el que ve las cuatro tools descubiertas y decide, en el momento, si conviene llamar get_quote para responder lo que le estés preguntando. Esa decisión del modelo —qué invocar y cuándo, en una conversación real— es exactamente lo que esta guía nombra como concepto, sin ejecutarlo: no hay llamada a la API de Claude en ningún módulo de esta guía, solo el protocolo MCP que la hace posible.
Errores comunes
-
Usar
sys.executabledentro de.mcp.json..mcp.jsones un archivo de texto plano, no un script de Python —sys.executablees una expresión que solo tiene sentido dentro de un proceso Python ya corriendo. Encommand, siempre va el nombre (o la ruta) literal del ejecutable, como"python3.14"o"/usr/local/bin/python3.14". -
Olvidar que la ruta en
argses relativa al directorio desde el que Claude Code lanza el proceso, no a la ubicación de.mcp.json. Sireservo_full_mcp_server.pyno está en ese directorio de trabajo,args: ["reservo_full_mcp_server.py"]fallaría al arrancar con un error de "archivo no encontrado" — la solución es una ruta absoluta, o relativa de forma consistente con dónde corre el proyecto. -
Pensar que el
claude mcp addde esta lección modificaría el comportamiento del servidor. No lo hace, y no podría:claude mcp addsolo escribe (o edita).mcp.json— nunca tocareservo_full_mcp_server.py. El servidor sigue siendo exactamente el mismo código que ya construiste y probaste en los Módulos 2 a 6, sin importar si lo lanzas tú consubprocess.Popeno Claude Code lo lanza leyendo.mcp.json. -
Elegir
--scope localpara un servidor que todo el equipo necesita. Conlocal(o sin el flag, que es su valor por defecto), la configuración queda solo en tu máquina — un compañero de equipo que clone el mismo repositorio no veríareservo-mcp-serverregistrado, porque ese.mcp.jsonnunca se generó donde git lo versiona.--scope projectes la elección correcta cuando el objetivo es que el servidor esté disponible para cualquiera que trabaje en ese proyecto.
Ejercicios
Ejercicio 1: Encuentra el error en esta entrada (Fácil)
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": "reservo_full_mcp_server.py"
}
}
}
¿Qué está mal, y qué reportaría validate_server_entry de la lección 02 si corrieras esta entrada contra esa función?
Ver solución
args está escrito como un string ("reservo_full_mcp_server.py"), no como una lista (["reservo_full_mcp_server.py"]). validate_server_entry de la lección 02 verifica explícitamente isinstance(entry["args"], list), así que reportaría: reservo-mcp-server: args debe ser una lista. Aunque el archivo sigue siendo JSON perfectamente válido —no hay ningún error de sintaxis—, la forma que Claude Code espera para args es siempre una lista, incluso cuando hay un solo argumento.
Ejercicio 2: Escribe el claude mcp add para un scope distinto (Medio)
Reescribe el comando claude mcp add de esta lección para que registre reservo-mcp-server con --scope local en vez de --scope project (para probarlo solo en tu propia máquina, sin compartirlo con el equipo todavía), y agrégale una variable de entorno hipotética RESERVO_LOG_LEVEL=debug usando el flag --env.
Ver solución
# CONTENIDO -- se explica, no se ejecuta.
claude mcp add reservo-mcp-server --scope local --env RESERVO_LOG_LEVEL=debug -- python3.14 reservo_full_mcp_server.py
Esto produciría (si reservo_full_mcp_server.py leyera esa variable, cosa que la versión de esta guía no hace) una entrada equivalente a:
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": ["reservo_full_mcp_server.py"],
"env": {"RESERVO_LOG_LEVEL": "debug"}
}
}
}
Explicación: cambiar --scope project por --scope local no cambia ni un campo de la entrada JSON en sí —sigue siendo el mismo type/command/args— cambia únicamente dónde se guarda esa entrada (un .mcp.json personal, fuera del control de versiones, en vez de uno compartido en la raíz del repositorio). El flag --env es repetible: cada aparición agrega una clave más al objeto env final.
Ejercicio 3: Verifica el registro con una tool real, no solo el handshake (Difícil)
Extiende verify_registration.py de esta lección para que, después de confirmar que serverInfo.name coincide, también haga notifications/initialized, tools/list, y confirme con un assert que list_rooms aparece entre las tools descubiertas — demostrando que el servidor lanzado a partir de .mcp.json no solo responde al handshake, sino que tiene el catálogo completo de Reservo disponible.
Ver solución
import json
import subprocess
import itertools
with open("mcp_config.json", "r", encoding="utf-8") as file:
config = json.load(file)
request_ids = itertools.count(1)
entry = config["mcpServers"]["reservo-mcp-server"]
proc = subprocess.Popen(
[entry["command"], *entry["args"]],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1,
)
def send(message):
proc.stdin.write(json.dumps(message) + "\n")
proc.stdin.flush()
def recv():
return json.loads(proc.stdout.readline())
send({"jsonrpc": "2.0", "id": next(request_ids), "method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "reservo-mcp-client", "version": "1.0.0"}}})
init_response = recv()
assert init_response["result"]["serverInfo"]["name"] == "reservo-mcp-server"
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
send({"jsonrpc": "2.0", "id": next(request_ids), "method": "tools/list", "params": {}})
tools_response = recv()
tool_names = [tool["name"] for tool in tools_response["result"]["tools"]]
assert "list_rooms" in tool_names
print(f"OK -- servidor lanzado desde .mcp.json, {len(tool_names)} tools descubiertas: {tool_names}")
proc.stdin.close()
proc.wait(timeout=5)
Salida esperada:
OK -- servidor lanzado desde .mcp.json, 4 tools descubiertas: ['list_rooms', 'get_quote', 'book_room', 'cancel_booking']
Explicación: este ejercicio conecta, con código, las dos mitades del módulo hasta ahora: la validación de la lección 02 (¿el JSON tiene la forma correcta?) y la confirmación de esta lección (¿lanzar ese JSON produce, de verdad, el servidor completo de Reservo?). Los dos assert son la versión programática de una pregunta que cualquier host real necesita poder responder antes de confiar en una entrada de .mcp.json: no solo "¿arranca sin errores?", sino "¿arranca el servidor correcto, con el catálogo que se espera?".
Resumen y siguiente paso
- La entrada real de
reservo-mcp-serveren.mcp.json:type: "stdio",command: "python3.14",args: ["reservo_full_mcp_server.py"],env: {}— cada campo justificado por lo que ya sabes del servidor desde los Módulos 2 a 6. - Un host que lee esa entrada y lanza
command+argsconsubprocess.Popenestá corriendo el mismo mecanismo exacto queMCPClient.__init__del Módulo 6 — se confirmó con protocolo real: elserverInfo.namenegociado coincide con la clave del registro. claude mcp add reservo-mcp-server --scope project -- python3.14 reservo_full_mcp_server.pyproduce esta misma entrada —--scopedecide dónde vive el archivo resultante y quién lo comparte.- Lo que Claude Code hace después del registro —descubrir con
tools/list/resources/list/prompts/list, y dejar que el modelo decida qué invocar— es, estructuralmente, el mismo flujo del Módulo 6, con el modelo en el lugar donde antes estaba tu propio código de cliente.
Siguiente lección: 04 — Transportes stdio vs. HTTP en producción. Con Reservo ya registrado por stdio, el criterio completo para decidir cuándo un servidor nuevo debería usar, en cambio, Streamable HTTP.
Recursos adicionales
- Claude Code — Model Context Protocol (MCP) — La documentación oficial de
claude mcp add, sus flags (--scope,--env,--transport) y los tres alcances (local/project/user). - Model Context Protocol — Specification 2025-06-18: Lifecycle — El handshake que el script de esta lección ejecuta contra el servidor lanzado desde
.mcp.json. - Python —
subprocess.Popen— El mecanismo de arranque de procesos, idéntico al que ya usaste enMCPClientdel Módulo 6. - Python —
json— La lectura de.mcp.jsonque alimentacommand/argsen esta lección.