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

  1. Expecting prompts/get to validate the room against ROOM_RATE_CENTS. It doesn't, on purpose, as you already saw in M5: room is free text for the prompt, with no enum. You can confirm it yourself by calling client.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 tried get_quote with that made-up room (Exercise 2 of this module's lesson 03).

  2. 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._recv is the correct way to inspect an error.

  3. Forgetting this capstone's version of plan_booking uses 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
  1. With room="Boardroom": "Before booking the Boardroom room, first read the cancellation policy resource..."
  2. 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_booking ran inside the complete server with and without its optional room argument, producing the same text (except the room) you already saw in M6, lesson 05.
  • The -32602 Unknown prompt error confirms prompts use the same generic error code as tools for "not found" — unlike resources, which uses the specific -32002 code.
  • 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

  1. Model Context Protocol — Specification 2025-06-18: Promptsprompts/list, prompts/get, run in this lesson.
  2. 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.
  3. Python — f-stringsbuild_plan_booking_text's substitution mechanism, with no additional templating language.
  4. Python — subprocess — The basis of all client↔server communication in this lesson.