Module 8: Project — Reservo's Complete MCP Server
Registering with Claude Code
Description
This module's previous six lessons talked to reservo_full_mcp_server.py from your own scripts, with a hand-written subprocess.Popen. This lesson takes the final step toward real life: registering that same server in .mcp.json, exactly as a real project using Claude Code would, and confirming — with real protocol, not just by reading the file — that the process that registration would launch is, byte for byte, the same complete server you ran throughout this module.
Connection to the module
This lesson introduces no new .mcp.json field — the complete shape (mcpServers, type, command, args, env) was already fixed by M7, lesson 02, and Reservo's specific entry was already written by M7, lesson 03. What this lesson adds is the final context: the server registered here is the complete capstone (tools + resources + prompts together), not a single module's server.
The .mcp.json entry
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": ["reservo_full_mcp_server.py"],
"env": {}
}
}
}
Every field, exactly as M7, lesson 03 already justified: the key ("reservo-mcp-server") matches SERVER_INFO["name"] inside the server; "type": "stdio" because the server always runs locally, as a subprocess; "command": "python3.14" is the literal executable, not sys.executable; "args": ["reservo_full_mcp_server.py"] points at this module's lesson 02's complete file; "env": {} stays empty because the server needs no credential — it keeps all its state (BOOKINGS, the counters) in the process's own memory.
Save this as mcp_config.json, in the same directory as reservo_full_mcp_server.py.
Worked example: validate the file, and confirm it launches the correct server
First, the shape validation — the same one from M7, lesson 02 — with pure json.load:
# validate_mcp_json.py
import json
with open("mcp_config.json", "r", encoding="utf-8") as file:
config = json.load(file)
print("config type:", type(config).__name__)
print("top-level keys:", list(config.keys()))
servers = config["mcpServers"]
print("registered servers:", 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", {}))
What to expect:
config type: dict
top-level keys: ['mcpServers']
registered servers: ['reservo-mcp-server']
--- reservo-mcp-server ---
type: stdio
command: python3.14
args: ['reservo_full_mcp_server.py']
env: {}
Second — and this is what's new in this lesson, beyond repeating M7 — confirming that launching command+args from that entry actually produces this module's complete server, with its three primitives, not a different version:
# verify_registration.py
"""Simulates what Claude Code would do reading .mcp.json: launch the server with
the EXACT command+args from the entry, do the handshake, and confirm the launched
server has the COMPLETE catalog -- 4 tools, not just the 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] registered name: {server_name!r}")
print(f"[handshake] serverInfo.name: {negotiated_name!r}")
print(f"match: {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 discovered from .mcp.json: {tool_names}")
proc.stdin.close()
proc.wait(timeout=5)
What to expect (running python3.14 verify_registration.py in a directory with mcp_config.json and reservo_full_mcp_server.py):
config type: dict
top-level keys: ['mcpServers']
registered servers: ['reservo-mcp-server']
[.mcp.json] registered name: 'reservo-mcp-server'
[handshake] serverInfo.name: 'reservo-mcp-server'
match: True
tools discovered from .mcp.json: ['list_rooms', 'get_quote', 'book_room', 'cancel_booking']
This script imported nothing from Claude Code nor simulated any complete application — it just took entry["command"] and entry["args"], two values that came out of json.load, and passed them to subprocess.Popen([entry["command"], *entry["args"]], ...), exactly the same mechanism MCPClient.__init__ used in this module's previous six lessons (with the difference that there the command came fixed in the code as sys.executable, and here it comes from an external file). The four discovered tools confirm something the shape validation alone couldn't: that the process launched from this .mcp.json entry is this module's complete server — not a toy server with the same name, nor a version with fewer primitives.
The equivalent claude mcp add
# CONTENT -- explained, NEVER run in this guide.
claude mcp add reservo-mcp-server --scope project -- python3.14 reservo_full_mcp_server.py
Same as in M7, lesson 03: reservo-mcp-server is the name; --scope project saves the resulting .mcp.json at the repository root, to share with the whole team via git; -- separates claude mcp add's flags from the real command to run; python3.14 reservo_full_mcp_server.py is exactly what ends up, split into two fields, inside command/args. Running this command in a real project with Claude Code installed would produce the same entry you wrote by hand at the start of this lesson — not a single field different.
What happens after registration (concept, not run)
With reservo-mcp-server in .mcp.json, what Claude Code does next is, structurally, exactly M6, lesson 06's Host: it behaves like just another MCPClient — it launches the process, does initialize/notifications/initialized, and runs the same discovery flow you ran yourself in this module's lesson 02 (tools/list + resources/list + prompts/list). The real difference isn't in the protocol — it's identical — but in who decides, after discovery, what to invoke: in this module's previous six scripts, your own code decided which tools/call to make; inside Claude Code, it's the model (claude-sonnet-5, in a real conversation) that sees the four discovered tools, reads their description and inputSchema, and decides in the moment whether it's worth calling get_quote to answer whatever you're asking it. That model decision is exactly what this guide names as concept, without running it — no lesson in this guide makes a real call to the Claude API.
Common mistakes
-
Registering only a previous module's server, not the complete one. If you mistakenly point
argsat, say, a tools-only server from M3 instead of this module'sreservo_full_mcp_server.py, Claude Code would discover 4 tools but 0 resources and 0 prompts — a real catalog, but incomplete relative to what this capstone built. This lesson's verification script (verify_registration.py) is exactly the tool to catch this error before trusting the registration. -
Using
sys.executableinside.mcp.json. Still the same mistake M7 already warned about:.mcp.jsonis plain text, not a running Python script —commandneeds the literal name of the executable. -
Thinking registering the server in
.mcp.jsonchanges something about the server itself. It doesn't, and it couldn't:reservo_full_mcp_server.pyis exactly the same file, no matter whether your ownsubprocess.Popenlaunches it (this module's lessons 02-06) or Claude Code launches it reading.mcp.json(this lesson).
Exercises
Exercise 1: Find the error in this entry (Easy)
{
"mcpServers": {
"reservo-mcp-server": {
"type": "stdio",
"command": "python3.14",
"args": ["reservo_tools_mcp_server.py"]
}
}
}
This entry is perfectly valid JSON and type/command/args have the correct shape. What's wrong with it anyway, and which tool from this lesson would catch it?
See solution
args points at reservo_tools_mcp_server.py (M3's tools-only server), not at reservo_full_mcp_server.py (this module's complete server). M7, lesson 02's validate_server_entry would not catch this problem — the entry has all required fields with the correct types, so it would pass shape validation with no error. What would catch it is this lesson's verify_registration.py: launching the process and calling resources/list/prompts/list (if extended to check all three primitives, not just tools/list), it would find 0 resources and 0 prompts instead of the expected 2 and 1 — the evidence that the registered file isn't the complete server. This is why "the JSON has the correct shape" and "the JSON registers the correct server" are two different questions, and only the second requires running real protocol.
Exercise 2: Extend verify_registration.py for all three primitives (Medium)
Modify this lesson's script so that, after confirming tools/list, it also calls resources/list and prompts/list, and confirms with assert it brings exactly 2 resources and 1 prompt — not just the 4 tools.
See solution
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 -- complete server confirmed: {len(tools)} tools, {len(resources)} resources, {len(prompts)} prompts")
proc.stdin.close()
proc.wait(timeout=5)
Expected output:
OK -- complete server confirmed: 4 tools, 2 resources, 1 prompts
Explanation: this script is the complete version of the check Exercise 1 identified as necessary — confirming all three primitives, not just one, before trusting a .mcp.json registers the correct server, not just a server that responds to the handshake.
Exercise 3: A .mcp.json with Reservo and a broken entry, caught before launching anything (Hard)
Write an mcp_config_two.json with two entries: reservo-mcp-server (correct, as in this lesson) and a second one, broken-server, with type: "stdio" but no command field. Use validate_server_entry/validate_mcp_json from M7, lesson 02, to catch broken-server's problem without attempting to launch any process — and confirm, separately, that reservo-mcp-server is still launchable with verify_registration.py.
See solution
{
"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}: missing required fields for 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"FAIL -- {error}")
if not all_errors:
print("OK -- all entries are valid")
Expected output:
FAIL -- broken-server: missing required fields for stdio: ['command']
Explanation: shape validation catches broken-server's problem without launching any subprocess — broken_server.py doesn't even need to exist as a file for this validation layer to reject it — exactly the advantage M7, lesson 02, already pointed out: a validation layer earlier than the real attempt to start the server. reservo-mcp-server, in the same configuration, still has all three required fields, so this lesson's verify_registration.py would launch it with no problem, completely independent of the other entry being broken — every mcpServers entry is validated and launched separately.
Summary and next step
reservo_full_mcp_server.py, this module's complete server, was registered in.mcp.jsonwith the exact same entry M7, lesson 03 already fixed:type: "stdio",command: "python3.14",args: ["reservo_full_mcp_server.py"],env: {}.verify_registration.pyconfirmed, with real protocol, that launchingcommand+argsfrom that entry produces the complete server — not just that it responds to the handshake, but that it has the four tools available.claude mcp add reservo-mcp-server --scope project -- python3.14 reservo_full_mcp_server.pywould produce this same entry — shown as content, never run.- What Claude Code does after registration is, structurally, the same
Hostfrom M6: just anotherMCPClient, with the model deciding what to invoke where your own code used to decide.
Next lesson: 08 — Project: ship the Reservo MCP server. The guide's closing: a final challenge with a reference solution, a nine-check programmatic checklist over the whole system, and where to go next.
Additional resources
- Claude Code — Model Context Protocol (MCP) — The official
.mcp.jsonandclaude mcp adddocumentation, with its flags and scopes. - Model Context Protocol — Specification 2025-06-18: Lifecycle — The handshake
verify_registration.pyruns against the server launched from.mcp.json. - Python —
subprocess.Popen— The process-launching mechanism, identical toMCPClient's in the previous six lessons. - Python —
json— The.mcp.jsonreading that feedscommand/argsin this lesson.