Módulo 8: Proyecto — el servidor MCP de Reservo completo
Registrar con Claude Code
Descripción
Las seis lecciones anteriores de este módulo hablaron con reservo_full_mcp_server.py desde scripts propios, con subprocess.Popen escrito a mano. Esta lección da el último paso hacia la vida real: registrar ese mismo servidor en .mcp.json, exactamente como lo haría un proyecto real que use Claude Code, y confirmar — con protocolo real, no solo leyendo el archivo — que el proceso que ese registro lanzaría es, byte a byte, el mismo servidor completo que ejecutaste durante todo este módulo.
Conexión con el módulo
Esta lección no introduce ningún campo nuevo de .mcp.json — la forma completa (mcpServers, type, command, args, env) ya la fijó M7, lección 02, y la entrada específica de Reservo ya la escribió M7, lección 03. Lo que agrega esta lección es el contexto final: el servidor que se registra aquí es el capstone completo (tools + resources + prompts juntos), no el servidor de un solo módulo.
La entrada de .mcp.json
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": ["reservo_full_mcp_server.py"],
"env": {}
}
}
}
Cada campo, tal como ya lo justificó M7, lección 03: la clave ("reservo-mcp-server") coincide con SERVER_INFO["name"] dentro del servidor; "type": "stdio" porque el servidor corre siempre local, como subproceso; "command": "python3.14" es el ejecutable literal, no sys.executable; "args": ["reservo_full_mcp_server.py"] apunta al archivo completo de la lección 02 de este módulo; "env": {} queda vacío porque el servidor no necesita ninguna credencial — guarda todo su estado (BOOKINGS, los contadores) en memoria del propio proceso.
Guarda esto como mcp_config.json, en el mismo directorio que reservo_full_mcp_server.py.
Ejemplo trabajado: validar el archivo, y confirmar que lanza el servidor correcto
Primero, la validación de forma —la misma de M7, lección 02—, con json.load puro:
# validate_mcp_json.py
import json
with open("mcp_config.json", "r", encoding="utf-8") as file:
config = json.load(file)
print("tipo de config:", type(config).__name__)
print("claves de nivel superior:", list(config.keys()))
servers = config["mcpServers"]
print("servidores registrados:", list(servers.keys()))
for name, entry in servers.items():
print(f"--- {name} ---")
print(" type:", entry["type"])
print(" command:", entry["command"])
print(" args:", entry["args"])
print(" env:", entry.get("env", {}))
Qué esperar:
tipo de config: dict
claves de nivel superior: ['mcpServers']
servidores registrados: ['reservo-mcp-server']
--- reservo-mcp-server ---
type: stdio
command: python3.14
args: ['reservo_full_mcp_server.py']
env: {}
Segundo — y esto es lo nuevo de esta lección, más allá de repetir M7 —: confirmar que lanzar command+args de esa entrada produce, de verdad, el servidor completo de este módulo, con sus tres primitivos, no una versión distinta:
# verify_registration.py
"""Simula lo que Claude Code haria al leer .mcp.json: lanzar el servidor con
command+args EXACTOS de la entrada, hacer el handshake, y confirmar que el
servidor lanzado tiene el catalogo COMPLETO -- 4 tools, no solo el handshake."""
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"
proc = subprocess.Popen(
[entry["command"], *entry["args"]],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1,
)
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"}}}
proc.stdin.write(json.dumps(send) + "\n")
proc.stdin.flush()
init_response = json.loads(proc.stdout.readline())
negotiated_name = init_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.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
proc.stdin.flush()
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": next(request_ids), "method": "tools/list",
"params": {}}) + "\n")
proc.stdin.flush()
tools_response = json.loads(proc.stdout.readline())
tool_names = [tool["name"] for tool in tools_response["result"]["tools"]]
assert "list_rooms" in tool_names
print(f"tools descubiertas desde .mcp.json: {tool_names}")
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):
tipo de config: dict
claves de nivel superior: ['mcpServers']
servidores registrados: ['reservo-mcp-server']
[.mcp.json] nombre registrado: 'reservo-mcp-server'
[handshake] serverInfo.name: 'reservo-mcp-server'
coinciden: True
tools descubiertas desde .mcp.json: ['list_rooms', 'get_quote', 'book_room', 'cancel_booking']
Este script no importó nada de Claude Code ni simuló ninguna aplicación completa — solo tomó entry["command"] y entry["args"], dos valores que salieron de json.load, y los pasó a subprocess.Popen([entry["command"], *entry["args"]], ...), exactamente el mismo mecanismo que MCPClient.__init__ usa en las seis lecciones anteriores de este módulo (con la diferencia de que ahí el comando venía fijo en el código como sys.executable, y aquí viene de un archivo externo). Las cuatro tools descubiertas confirman algo que la validación de forma, por sí sola, no podía confirmar: que el proceso que arranca desde esta entrada de .mcp.json es el servidor completo de este módulo — no un servidor de juguete con el mismo nombre, ni una versión con menos primitivos.
El claude mcp add equivalente
# CONTENIDO -- se explica, NUNCA se ejecuta en esta guia.
claude mcp add reservo-mcp-server --scope project -- python3.14 reservo_full_mcp_server.py
Igual que en M7, lección 03: reservo-mcp-server es el nombre; --scope project guarda el .mcp.json resultante en la raíz del repositorio, para compartirlo con todo el equipo vía git; -- separa los flags de claude mcp add del comando real a ejecutar; python3.14 reservo_full_mcp_server.py es exactamente lo que termina, separado en dos campos, dentro de command/args. Correr este comando en un proyecto real con Claude Code instalado produciría la misma entrada que escribiste a mano al principio de esta lección — ni un campo distinto.
Qué pasa después del registro (concepto, no ejecutado)
Con reservo-mcp-server en .mcp.json, lo que Claude Code hace a continuación es, estructuralmente, exactamente el Host de M6, lección 06: se comporta como un MCPClient más — lanza el proceso, hace initialize/notifications/initialized, y ejecuta el mismo flujo de descubrimiento que corriste tú mismo en la lección 02 de este módulo (tools/list + resources/list + prompts/list). La diferencia real no está en el protocolo —es idéntico— sino en quién decide, después del descubrimiento, qué invocar: en los seis scripts anteriores de este módulo, tu propio código 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, lee sus description y inputSchema, y decide en el momento si conviene llamar get_quote para responder lo que le estés preguntando. Esa decisión del modelo es exactamente lo que esta guía nombra como concepto, sin ejecutarlo — ninguna lección de esta guía hace una llamada real a la API de Claude.
Errores comunes
-
Registrar solo el servidor de un módulo anterior, no el completo. Si por error apuntas
argsa, por ejemplo, un servidor de solo-tools de M3 en vez dereservo_full_mcp_server.pyde este módulo, Claude Code descubriría 4 tools pero 0 resources y 0 prompts — un catálogo real, pero incompleto respecto a lo que este capstone construyó. El script de verificación de esta lección (verify_registration.py) es exactamente la herramienta para detectar este error antes de confiar en el registro. -
Usar
sys.executabledentro de.mcp.json. Sigue siendo el mismo error que M7 ya advirtió:.mcp.jsones texto plano, no un script Python en ejecución —commandnecesita el nombre literal del ejecutable. -
Pensar que registrar el servidor en
.mcp.jsoncambia algo del servidor mismo. No lo hace, y no podría:reservo_full_mcp_server.pyes exactamente el mismo archivo, sin importar si lo lanza tu propiosubprocess.Popen(lecciones 02-06 de este módulo) o Claude Code leyendo.mcp.json(esta lección).
Ejercicios
Ejercicio 1: Encuentra el error en esta entrada (Fácil)
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": ["reservo_tools_mcp_server.py"]
}
}
}
Esta entrada es JSON perfectamente válido y type/command/args tienen la forma correcta. ¿Qué problema tiene de todas formas, y qué herramienta de esta lección lo detectaría?
Ver solución
args apunta a reservo_tools_mcp_server.py (el servidor de solo-tools de M3), no a reservo_full_mcp_server.py (el servidor completo de este módulo). validate_server_entry de M7, lección 02, no detectaría este problema — la entrada tiene todos los campos requeridos con los tipos correctos, así que pasaría la validación de forma sin ningún error. Lo que sí lo detectaría es verify_registration.py de esta lección: al lanzar el proceso y llamar resources/list/prompts/list (si se extendiera para chequear los tres primitivos, no solo tools/list), encontraría 0 resources y 0 prompts en vez de los 2 y 1 esperados — la evidencia de que el archivo registrado no es el servidor completo. Esta es la razón por la que "el JSON tiene la forma correcta" y "el JSON registra el servidor correcto" son dos preguntas distintas, y solo la segunda requiere ejecutar protocolo real.
Ejercicio 2: Extiende verify_registration.py para los tres primitivos (Medio)
Modifica el script de esta lección para que, después de confirmar tools/list, también llame resources/list y prompts/list, y confirme con assert que trae exactamente 2 resources y 1 prompt — no solo las 4 tools.
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"}}})
recv()
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
send({"jsonrpc": "2.0", "id": next(request_ids), "method": "tools/list", "params": {}})
tools = recv()["result"]["tools"]
send({"jsonrpc": "2.0", "id": next(request_ids), "method": "resources/list", "params": {}})
resources = recv()["result"]["resources"]
send({"jsonrpc": "2.0", "id": next(request_ids), "method": "prompts/list", "params": {}})
prompts = recv()["result"]["prompts"]
assert len(tools) == 4
assert len(resources) == 2
assert len(prompts) == 1
print(f"OK -- servidor completo confirmado: {len(tools)} tools, {len(resources)} resources, {len(prompts)} prompts")
proc.stdin.close()
proc.wait(timeout=5)
Salida esperada:
OK -- servidor completo confirmado: 4 tools, 2 resources, 1 prompts
Explicación: este script es la versión completa de la verificación que el Ejercicio 1 identificó como necesaria — confirmar los tres primitivos, no solo uno, antes de confiar en que un .mcp.json registra el servidor correcto, no solo un servidor que responde al handshake.
Ejercicio 3: Un .mcp.json con Reservo y una entrada rota, detectada antes de lanzar nada (Difícil)
Escribe un mcp_config_two.json con dos entradas: reservo-mcp-server (correcta, como en esta lección) y una segunda, broken-server, con type: "stdio" pero sin el campo command. Usa validate_server_entry/validate_mcp_json de M7, lección 02, para detectar el problema de broken-server sin intentar lanzar ningún proceso — y confirma, por separado, que reservo-mcp-server sigue siendo lanzable con verify_registration.py.
Ver solución
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": ["reservo_full_mcp_server.py"],
"env": {}
},
"broken-server": {
"type": "stdio",
"args": ["broken_server.py"]
}
}
}
import json
REQUIRED_STDIO_FIELDS = {"type", "command", "args"}
def validate_server_entry(name, entry):
errors = []
if entry.get("type") == "stdio":
missing = REQUIRED_STDIO_FIELDS - entry.keys()
if missing:
errors.append(f"{name}: faltan campos requeridos para stdio: {sorted(missing)}")
return errors
with open("mcp_config_two.json", "r", encoding="utf-8") as file:
config = json.load(file)
all_errors = []
for name, entry in config["mcpServers"].items():
all_errors.extend(validate_server_entry(name, entry))
for error in all_errors:
print(f"FALLA -- {error}")
if not all_errors:
print("OK -- todas las entradas son validas")
Salida esperada:
FALLA -- broken-server: faltan campos requeridos para stdio: ['command']
Explicación: la validación de forma detecta el problema de broken-server sin lanzar ningún subproceso —ni siquiera existe broken_server.py como archivo, y no hace falta que exista para que esta capa de validación lo rechace—, exactamente la ventaja que M7, lección 02, ya señaló: una capa de validación anterior al intento real de arrancar el servidor. reservo-mcp-server, en la misma configuración, sigue teniendo los tres campos requeridos, así que verify_registration.py de esta lección lo lanzaría sin problema, de forma completamente independiente de que la otra entrada esté rota — cada entrada de mcpServers se valida y se lanza por separado.
Resumen y siguiente paso
reservo_full_mcp_server.py, el servidor completo de este módulo, se registró en.mcp.jsoncon la misma entrada exacta que ya fijó M7, lección 03:type: "stdio",command: "python3.14",args: ["reservo_full_mcp_server.py"],env: {}.verify_registration.pyconfirmó, con protocolo real, que lanzarcommand+argsde esa entrada produce el servidor completo — no solo que responde al handshake, sino que tiene las cuatro tools disponibles.claude mcp add reservo-mcp-server --scope project -- python3.14 reservo_full_mcp_server.pyproduciría esta misma entrada — mostrado como contenido, nunca ejecutado.- Lo que Claude Code hace después del registro es, estructuralmente, el mismo
Hostde M6: unMCPClientmás, con el modelo decidiendo qué invocar en el lugar donde antes decidía tu propio código.
Siguiente lección: 08 — Proyecto: entregar el servidor MCP de Reservo. El cierre de la guía: un reto final con solución de referencia, un checklist programático de nueve verificaciones sobre todo el sistema, y hacia dónde seguir.
Recursos adicionales
- Claude Code — Model Context Protocol (MCP) — La documentación oficial de
.mcp.jsonyclaude mcp add, con sus flags y alcances. - Model Context Protocol — Specification 2025-06-18: Lifecycle — El handshake que
verify_registration.pyejecuta contra el servidor lanzado desde.mcp.json. - Python —
subprocess.Popen— El mecanismo de arranque de procesos, idéntico al deMCPClienten las seis lecciones anteriores. - Python —
json— La lectura de.mcp.jsonque alimentacommand/argsen esta lección.