Módulo 8: Proyecto — el servidor MCP de Reservo completo
El prompt `plan_booking` en el capstone
Descripción
Cierra la vuelta primitivo-por-primitivo de este módulo: prompts/list y prompts/get sobre plan_booking, con y sin su argumento opcional room, dentro del servidor completo. Como en las dos lecciones anteriores, termina con el caso de error — un prompt que no existe — y confirma que el tercer y último primitivo tampoco se vio afectado por convivir con tools y resources en el mismo proceso.
Conexión con el módulo
list_prompts/get_prompt son los mismos métodos de MCPClient desde M6, lección 04. build_plan_booking_text es la función de sustitución que M6, lección 05, usó al ensamblar el servidor completo por primera vez — la misma que quedó fijada en la lección 02 de este módulo. Los identificadores del prompt (name: "plan_booking", su description, su único argumento opcional room) son los que M5, lección 03, fijó desde el principio de la guía.
Ejemplo trabajado: con argumento, sin argumento, y el prompt inexistente
from mcp_client import MCPClient
client = MCPClient("reservo_full_mcp_server.py")
client.connect()
prompts = client.list_prompts()
for p in prompts:
print(f"[client] prompts/list -> {p['name']}: {p['description']} (arguments={p['arguments']})")
with_room = client.get_prompt("plan_booking", {"room": "Boardroom"})
print(f"[client] prompts/get plan_booking(room=Boardroom) -> {with_room['messages'][0]['content']['text']}")
without_room = client.get_prompt("plan_booking")
print(f"[client] prompts/get plan_booking() sin argumentos -> {without_room['messages'][0]['content']['text']}")
# prompt desconocido -> error de protocolo
client._send({"jsonrpc": "2.0", "id": next(client.request_ids), "method": "prompts/get",
"params": {"name": "cancel_everything", "arguments": {}}})
error_response = client._recv()
print(f"[client] prompts/get prompt desconocido -> {error_response.get('error')}")
client.close()
Qué esperar (ejecutando python3.14 este_script.py):
[client -> reservo_full_mcp_server.py] {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "reservo-mcp-client", "version": "1.0.0"}}}
[reservo_full_mcp_server.py -> client] {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18", "capabilities": {"tools": {}, "resources": {}, "prompts": {}}, "serverInfo": {"name": "reservo-mcp-server", "version": "1.0.0"}}}
[client -> reservo_full_mcp_server.py] {"jsonrpc": "2.0", "method": "notifications/initialized"}
[client -> reservo_full_mcp_server.py] {"jsonrpc": "2.0", "id": 2, "method": "prompts/list", "params": {}}
[reservo_full_mcp_server.py -> client] {"jsonrpc": "2.0", "id": 2, "result": {"prompts": [{"name": "plan_booking", "description": "Guide the assistant through checking policy and quoting before booking a room", "arguments": [{"name": "room", "description": "Room to plan a booking for", "required": false}]}]}}
[client] prompts/list -> plan_booking: Guide the assistant through checking policy and quoting before booking a room (arguments=[{'name': 'room', 'description': 'Room to plan a booking for', 'required': False}])
[client -> reservo_full_mcp_server.py] {"jsonrpc": "2.0", "id": 3, "method": "prompts/get", "params": {"name": "plan_booking", "arguments": {"room": "Boardroom"}}}
[reservo_full_mcp_server.py -> client] {"jsonrpc": "2.0", "id": 3, "result": {"description": "Guide the assistant through checking policy and quoting before booking a room", "messages": [{"role": "user", "content": {"type": "text", "text": "Before booking the Boardroom room, first read the cancellation policy resource (reservo://policies/cancellation-policy) and summarize the refund window for the user. Then call get_quote with the room, tier, and hours the user wants, and show them the price in cents. Only call book_room after the user has seen the quote and confirmed they want to proceed."}}]}}
[client] prompts/get plan_booking(room=Boardroom) -> Before booking the Boardroom room, first read the cancellation policy resource (reservo://policies/cancellation-policy) and summarize the refund window for the user. Then call get_quote with the room, tier, and hours the user wants, and show them the price in cents. Only call book_room after the user has seen the quote and confirmed they want to proceed.
[client -> reservo_full_mcp_server.py] {"jsonrpc": "2.0", "id": 4, "method": "prompts/get", "params": {"name": "plan_booking", "arguments": {}}}
[reservo_full_mcp_server.py -> client] {"jsonrpc": "2.0", "id": 4, "result": {"description": "Guide the assistant through checking policy and quoting before booking a room", "messages": [{"role": "user", "content": {"type": "text", "text": "Before booking the room the user wants, first read the cancellation policy resource (reservo://policies/cancellation-policy) and summarize the refund window for the user. Then call get_quote with the room, tier, and hours the user wants, and show them the price in cents. Only call book_room after the user has seen the quote and confirmed they want to proceed."}}]}}
[client] prompts/get plan_booking() sin argumentos -> Before booking the room the user wants, first read the cancellation policy resource (reservo://policies/cancellation-policy) and summarize the refund window for the user. Then call get_quote with the room, tier, and hours the user wants, and show them the price in cents. Only call book_room after the user has seen the quote and confirmed they want to proceed.
[client -> reservo_full_mcp_server.py] {"jsonrpc": "2.0", "id": 5, "method": "prompts/get", "params": {"name": "cancel_everything", "arguments": {}}}
[reservo_full_mcp_server.py -> client] {"jsonrpc": "2.0", "id": 5, "error": {"code": -32602, "message": "Unknown prompt: cancel_everything"}}
[client] prompts/get prompt desconocido -> {'code': -32602, 'message': 'Unknown prompt: cancel_everything'}
---- stderr de reservo_full_mcp_server.py ----
[server] arrancando, esperando mensajes por stdin...
[server] initialize <- cliente {'name': 'reservo-mcp-client', 'version': '1.0.0'}
[server] notifications/initialized <- el cliente confirma que ya puede operar
[server] prompts/list -> 1 prompts
[server] prompts/get <- plan_booking({'room': 'Boardroom'})
[server] prompts/get <- plan_booking({})
Cinco mensajes con id (1 a 5): el handshake, prompts/list, dos prompts/get exitosos (con y sin room), y uno que dispara el error -32602 por nombre de prompt desconocido. Compara las dos respuestas exitosas línea por línea, igual que hiciste en M5, lección 06: todo es idéntico entre ambas —description, role, la estructura de messages— salvo la primera frase, "the Boardroom room" contra "the room the user wants", que aparece exactamente una vez en cada texto (a diferencia de la versión de M5 original, que la repetía dos veces — esta versión, ensamblada en M6, la usa solo en la frase inicial).
Lo que sigue siendo cierto: el prompt instruye, no ejecuta
Como ya estableció M5, lección 06: plan_booking menciona reservo://policies/cancellation-policy (un resource) y get_quote/book_room (dos tools) en su texto, pero no los invoca por su cuenta. Este es el momento de la guía donde esa afirmación deja de ser una promesa a futuro: el servidor completo de este módulo SÍ implementa las tres piezas que el prompt menciona — la política existe de verdad (lección 04 de este módulo), get_quote y book_room existen de verdad (lección 03) — y aun así, handle_prompts_get no llama a ninguna de ellas. prompts/get sigue devolviendo únicamente texto; quien reciba ese texto (típicamente un modelo, en una conversación real con Claude Code, tema de la lección 07) es quien decide, después, si sigue las instrucciones y llama resources/read y tools/call por su cuenta.
Errores comunes
-
Esperar que
prompts/getvalide la sala contraROOM_RATE_CENTS. No lo hace, y a propósito, tal como ya viste en M5:roomes texto libre para el prompt, sinenum. Puedes confirmarlo tú mismo llamandoclient.get_prompt("plan_booking", {"room": "Ballroom"})(una sala que no existe en Reservo) — el texto se genera igual, mencionando "the Ballroom room" sin ninguna queja. El error, si lo hay, aparecería recién si alguien intentaraget_quotecon esa sala inventada (Ejercicio 2 de la lección 03 de este módulo). -
Usar
client.get_prompt()para el caso del prompt desconocido. El mismo patrón que ya viste dos veces en este módulo: los métodos "anchos" deMCPClientasumen éxito.client._send/client._recves la forma correcta de inspeccionar unerror. -
Olvidar que la versión de
plan_bookingde este capstone usa la sala solo una vez en el texto, no dos. Si comparas con M5, lección 06 (donde la frase aparece dos veces), la versión ensamblada en M6 —y reutilizada aquí— construye un texto ligeramente distinto, más corto, con la misma intención pero una sola mención de la sala. Ambas son implementaciones válidas del mismo prompt ancla — MCP no define ningún lenguaje de plantillas que fije el texto exacto, solo la forma del resultado (messages: [{role, content}]), como ya estableció M5.
Ejercicios
Ejercicio 1: Encuentra la única diferencia entre las dos respuestas exitosas (Fácil)
Sin mirar de nuevo el bloque de transcripción de arriba: ¿en qué frase exacta difieren el texto con room="Boardroom" y el texto sin argumento? Escribe las dos versiones de esa frase.
Ver solución
- Con
room="Boardroom":"Before booking the Boardroom room, first read the cancellation policy resource..." - Sin
room:"Before booking the room the user wants, first read the cancellation policy resource..."
Solo la frase inicial cambia ("the Boardroom room" contra "the room the user wants") — el resto del texto, desde "first read the cancellation policy resource" hasta el final, es idéntico, carácter por carácter, en ambos casos. Esto confirma lo que build_plan_booking_text hace en código: room_phrase se calcula una sola vez, al principio de la función, y se sustituye en un único lugar del texto.
Ejercicio 2: Activa plan_booking para las tres salas y confirma que solo cambia una palabra (Medio)
Llama prompts/get con room="Focus", room="Studio" y room="Boardroom", en la misma corrida. Confirma con assert que las tres respuestas comparten exactamente el mismo texto salvo por el nombre de la sala en la primera frase.
Ver solución
from mcp_client import MCPClient
client = MCPClient("reservo_full_mcp_server.py", verbose=False)
client.connect()
texts = {}
for room in ["Focus", "Studio", "Boardroom"]:
result = client.get_prompt("plan_booking", {"room": room})
texts[room] = result["messages"][0]["content"]["text"]
print(f"{room}: {texts[room][:60]}...")
# la parte despues de la primera frase es identica en las 3
suffix_after_room = lambda text: text.split("first read", 1)[1]
assert suffix_after_room(texts["Focus"]) == suffix_after_room(texts["Studio"]) == suffix_after_room(texts["Boardroom"])
print("OK: las 3 respuestas comparten todo el texto salvo el nombre de la sala")
client.close()
Salida esperada:
Focus: Before booking the Focus room, first read the cancellation policy re...
Studio: Before booking the Studio room, first read the cancellation policy r...
Boardroom: Before booking the Boardroom room, first read the cancellation policy...
OK: las 3 respuestas comparten todo el texto salvo el nombre de la sala
Explicación: dividir cada texto en la palabra "first read" y comparar todo lo que viene después confirma, con código —no solo con lectura visual—, que la única variación entre las tres llamadas está contenida en la primera frase. Esta es la misma verificación de precisión que ya usaste en M5, Ejercicio 2 de la lección 06 (str.count), aplicada aquí con una técnica distinta (dividir el string) para el mismo propósito: confirmar exactamente dónde cambia el texto generado, no solo que "se ve distinto".
Ejercicio 3: Agrega un segundo prompt sin tocar plan_booking (Difícil)
Agrega un segundo prompt al servidor, list_available_rooms (sin argumentos, description: "Ask the assistant to list every room and its hourly rate"), cuyo build_* devuelva un texto fijo instruyendo llamar list_rooms y mostrar el resultado. Ejecuta prompts/list y confirma que ahora trae dos prompts; ejecuta prompts/get sobre el nuevo y confirma que plan_booking sigue funcionando exactamente igual que antes.
Ver solución
En el servidor:
PROMPTS["list_available_rooms"] = {
"name": "list_available_rooms",
"description": "Ask the assistant to list every room and its hourly rate",
"arguments": [],
}
def build_list_available_rooms_text(arguments):
return "Call list_rooms and show me every room with its hourly rate in cents."
PROMPT_BUILDERS = {
"plan_booking": build_plan_booking_text,
"list_available_rooms": build_list_available_rooms_text,
}
(Esto requiere generalizar handle_prompts_get para usar PROMPT_BUILDERS[name](arguments) en vez de llamar build_plan_booking_text directamente — el mismo patrón de registro {name: función} que M5, lección 06, ya usó.)
Verificación:
from mcp_client import MCPClient
client = MCPClient("reservo_full_mcp_server.py", verbose=False)
client.connect()
prompts = client.list_prompts()
assert len(prompts) == 2
assert {"plan_booking", "list_available_rooms"} == {p["name"] for p in prompts}
rooms_prompt = client.get_prompt("list_available_rooms")
print(f"list_available_rooms -> {rooms_prompt['messages'][0]['content']['text']}")
booking_prompt = client.get_prompt("plan_booking", {"room": "Focus"})
assert "the Focus room" in booking_prompt["messages"][0]["content"]["text"]
print("OK: 2 prompts, plan_booking sin cambios")
client.close()
Salida esperada:
list_available_rooms -> Call list_rooms and show me every room with its hourly rate in cents.
OK: 2 prompts, plan_booking sin cambios
Explicación: el registro PROMPT_BUILDERS (una versión generalizada de lo que M5 ya usó para un solo prompt) permite agregar un segundo prompt sin tocar build_plan_booking_text ni handle_prompts_get más allá de la línea que hace el lookup por nombre — el mismo patrón aditivo de tools (Ejercicio 3, lección 02 de este módulo) y resources (Ejercicio 3, lección 04), ahora confirmado también para prompts: los tres primitivos de MCP escalan agregando entradas a un catálogo, nunca reescribiendo el dispatch existente.
Resumen y siguiente paso
plan_bookingse ejecutó dentro del servidor completo con y sin su argumento opcionalroom, produciendo el mismo texto (salvo la sala) que ya viste en M6, lección 05.- El error
-32602 Unknown promptconfirma que prompts usa el mismo código de error genérico que tools para "no encontrado" — a diferencia de resources, que usa el código específico-32002. - El prompt sigue instruyendo, no ejecutando: menciona un resource y dos tools que SÍ existen en este servidor completo, pero no los invoca por su cuenta — esa decisión queda para quien reciba el texto, típicamente un modelo en Claude Code (lección 07).
Con esto termina la vuelta primitivo por primitivo del capstone: tools (L03), resources (L04), prompts (L05), cada uno confirmado funcionando sin fricción dentro del mismo proceso.
Siguiente lección: 06 — La demo completa del cliente. El ancla ejecutada de todo el módulo: MCPClient completo, conectado al servidor completo, descubriendo y usando los tres primitivos en una sola corrida sin interrupciones — la síntesis final de las lecciones 02 a 05.
Recursos adicionales
- Model Context Protocol — Specification 2025-06-18: Prompts —
prompts/list,prompts/get, ejecutados en esta lección. - Model Context Protocol — Specification 2025-06-18: Server features overview — Los tres modelos de interacción (model-controlled, application-driven, user-controlled) contrastados por última vez en esta guía.
- Python — f-strings — El mecanismo de sustitución de
build_plan_booking_text, sin ningún lenguaje de plantillas adicional. - Python —
subprocess— La base de toda la comunicación cliente↔servidor de esta lección.