Module 8: Project — Reservo's Complete MCP Server
The `plan_booking` prompt in the capstone
Description
This closes the module's primitive-by-primitive pass: prompts/list and prompts/get on plan_booking, with and without its optional room argument, inside the complete server. As in the previous two lessons, it ends with the error case — a prompt that doesn't exist — and confirms the third and final primitive also wasn't affected by coexisting with tools and resources in the same process.
Connection to the module
list_prompts/get_prompt are the same MCPClient methods since M6, lesson 04. build_plan_booking_text is the substitution function M6, lesson 05, used when first assembling the complete server — the same one fixed in this module's lesson 02. The prompt's identifiers (name: "plan_booking", its description, its single optional room argument) are the ones M5, lesson 03, fixed from the start of the guide.
Worked example: with argument, without argument, and the nonexistent prompt
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() with no arguments -> {without_room['messages'][0]['content']['text']}")
# unknown prompt -> protocol error
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 unknown prompt -> {error_response.get('error')}")
client.close()
What to expect (running python3.14 this_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() with no arguments -> 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 unknown prompt -> {'code': -32602, 'message': 'Unknown prompt: cancel_everything'}
---- stderr from reservo_full_mcp_server.py ----
[server] starting up, waiting for messages on stdin...
[server] initialize <- client {'name': 'reservo-mcp-client', 'version': '1.0.0'}
[server] notifications/initialized <- client confirms it can now operate
[server] prompts/list -> 1 prompts
[server] prompts/get <- plan_booking({'room': 'Boardroom'})
[server] prompts/get <- plan_booking({})
Five messages with id (1 through 5): the handshake, prompts/list, two successful prompts/get calls (with and without room), and one triggering the -32602 error for an unknown prompt name. Compare the two successful responses line by line, just as you did in M5, lesson 06: everything is identical between them — description, role, messages's structure — except the first sentence, "the Boardroom room" versus "the room the user wants", which appears exactly once in each text (unlike M5's original version, which repeated it twice — this version, assembled in M6, only uses it in the opening sentence).
What remains true: the prompt instructs, it doesn't execute
As M5, lesson 06 already established: plan_booking mentions reservo://policies/cancellation-policy (a resource) and get_quote/book_room (two tools) in its text, but doesn't invoke them on its own. This is the moment in the guide where that claim stops being a promise about the future: this module's complete server DOES implement the three pieces the prompt mentions — the policy really exists (this module's lesson 04), get_quote and book_room really exist (lesson 03) — and even so, handle_prompts_get calls none of them. prompts/get keeps returning nothing but text; whoever receives that text (typically a model, in a real conversation with Claude Code, lesson 07's topic) is who decides, afterward, whether to follow the instructions and call resources/read and tools/call on their own.
Common mistakes
-
Expecting
prompts/getto validate the room againstROOM_RATE_CENTS. It doesn't, on purpose, as you already saw in M5:roomis free text for the prompt, with noenum. You can confirm it yourself by callingclient.get_prompt("plan_booking", {"room": "Ballroom"})(a room that doesn't exist at Reservo) — the text is generated all the same, mentioning "the Ballroom room" with no complaint at all. The error, if any, would only show up if someone triedget_quotewith that made-up room (Exercise 2 of this module's lesson 03). -
Using
client.get_prompt()for the unknown-prompt case. The same pattern you've already seen twice in this module:MCPClient's "wide" methods assume success.client._send/client._recvis the correct way to inspect anerror. -
Forgetting this capstone's version of
plan_bookinguses the room only once in the text, not twice. If you compare with M5, lesson 06 (where the phrase appears twice), the version assembled in M6 — and reused here — builds a slightly different, shorter text, with the same intent but a single mention of the room. Both are valid implementations of the same anchor prompt — MCP defines no templating language that fixes the exact text, only the shape of the result (messages: [{role, content}]), as M5 already established.
Exercises
Exercise 1: Find the only difference between the two successful responses (Easy)
Without looking back at the transcript block above: in which exact sentence do the text with room="Boardroom" and the text with no argument differ? Write out both versions of that sentence.
See solution
- With
room="Boardroom":"Before booking the Boardroom room, first read the cancellation policy resource..." - Without
room:"Before booking the room the user wants, first read the cancellation policy resource..."
Only the opening sentence changes ("the Boardroom room" versus "the room the user wants") — the rest of the text, from "first read the cancellation policy resource" to the end, is identical, character for character, in both cases. This confirms what build_plan_booking_text does in code: room_phrase is computed once, at the start of the function, and substituted in a single spot in the text.
Exercise 2: Activate plan_booking for all three rooms and confirm only one word changes (Medium)
Call prompts/get with room="Focus", room="Studio", and room="Boardroom", in the same run. Confirm with assert that the three responses share the exact same text except for the room's name in the first sentence.
See solution
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]}...")
# the part after the first sentence is identical across all 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: all 3 responses share all the text except the room's name")
client.close()
Expected output:
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: all 3 responses share all the text except the room's name
Explanation: splitting each text at the phrase "first read" and comparing everything after it confirms, with code — not just visual reading — that the only variation across the three calls is contained in the first sentence. This is the same precision check you already used in M5, Exercise 2 of lesson 06 (str.count), applied here with a different technique (splitting the string) for the same purpose: confirming exactly where the generated text changes, not just that it "looks different."
Exercise 3: Add a second prompt without touching plan_booking (Hard)
Add a second prompt to the server, list_available_rooms (no arguments, description: "Ask the assistant to list every room and its hourly rate"), whose build_* returns fixed text instructing to call list_rooms and show the result. Run prompts/list and confirm it now brings two prompts; run prompts/get on the new one and confirm plan_booking still works exactly as before.
See solution
On the server:
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,
}
(This requires generalizing handle_prompts_get to use PROMPT_BUILDERS[name](arguments) instead of calling build_plan_booking_text directly — the same {name: function} registry pattern M5, lesson 06, already used.)
Verification:
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 unchanged")
client.close()
Expected output:
list_available_rooms -> Call list_rooms and show me every room with its hourly rate in cents.
OK: 2 prompts, plan_booking unchanged
Explanation: the PROMPT_BUILDERS registry (a generalized version of what M5 already used for a single prompt) lets you add a second prompt without touching build_plan_booking_text or handle_prompts_get beyond the line that does the name lookup — the same additive pattern from tools (Exercise 3, this module's lesson 02) and resources (Exercise 3, lesson 04), now confirmed for prompts too: all three MCP primitives scale by adding entries to a catalog, never by rewriting the existing dispatch.
Summary and next step
plan_bookingran inside the complete server with and without its optionalroomargument, producing the same text (except the room) you already saw in M6, lesson 05.- The
-32602 Unknown prompterror confirms prompts use the same generic error code as tools for "not found" — unlike resources, which uses the specific-32002code. - The prompt keeps instructing, not executing: it mentions a resource and two tools that DO exist in this complete server, but doesn't invoke them on its own — that decision is left to whoever receives the text, typically a model in Claude Code (lesson 07).
With this, the capstone's primitive-by-primitive pass is done: tools (L03), resources (L04), prompts (L05), each confirmed working with no friction inside the same process.
Next lesson: 06 — The complete client demo. The entire module's executed anchor: the complete MCPClient, connected to the complete server, discovering and using all three primitives in a single run with no interruptions — the final synthesis of lessons 02 through 05.
Additional resources
- Model Context Protocol — Specification 2025-06-18: Prompts —
prompts/list,prompts/get, run in this lesson. - Model Context Protocol — Specification 2025-06-18: Server features overview — The three interaction models (model-controlled, application-driven, user-controlled) contrasted one last time in this guide.
- Python — f-strings —
build_plan_booking_text's substitution mechanism, with no additional templating language. - Python —
subprocess— The basis of all client↔server communication in this lesson.