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

  1. Registrar solo el servidor de un módulo anterior, no el completo. Si por error apuntas args a, por ejemplo, un servidor de solo-tools de M3 en vez de reservo_full_mcp_server.py de 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.

  2. Usar sys.executable dentro de .mcp.json. Sigue siendo el mismo error que M7 ya advirtió: .mcp.json es texto plano, no un script Python en ejecución — command necesita el nombre literal del ejecutable.

  3. Pensar que registrar el servidor en .mcp.json cambia algo del servidor mismo. No lo hace, y no podría: reservo_full_mcp_server.py es exactamente el mismo archivo, sin importar si lo lanza tu propio subprocess.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.json con 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.py confirmó, con protocolo real, que lanzar command+args de 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.py produciría esta misma entrada — mostrado como contenido, nunca ejecutado.
  • Lo que Claude Code hace después del registro es, estructuralmente, el mismo Host de M6: un MCPClient má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

  1. Claude Code — Model Context Protocol (MCP) — La documentación oficial de .mcp.json y claude mcp add, con sus flags y alcances.
  2. Model Context Protocol — Specification 2025-06-18: Lifecycle — El handshake que verify_registration.py ejecuta contra el servidor lanzado desde .mcp.json.
  3. Python — subprocess.Popen — El mecanismo de arranque de procesos, idéntico al de MCPClient en las seis lecciones anteriores.
  4. Python — json — La lectura de .mcp.json que alimenta command/args en esta lección.