Topic 5: Module 5 lab
5 min read·22 Sept 2026
The lab script runs everything this module built, against a temporary copy of the notes, and prints a one-screen report. It is the script to run after any change to server.py: if every line looks like the output below, the server still works in both eras, still validates at the boundary, still sanitizes crashes, and still honors cancellation.
python
"""Module 5 lab: exercise the packaged notes server end to end and print a report.
Run from the repository root: PYTHONPATH=. python examples/m05_lab.py
"""
from __future__ import annotations
import logging
import os
import shutil
import sys
import tempfile
import time
from pathlib import Path
import anyio
from examples.m05_reindex_server import build_reindex_server
from mcp import Client, MCPError, StdioServerParameters
from notes_assistant.config import load_settings
from notes_assistant.server import build_server
from notes_assistant.store import NoteStore
logging.basicConfig(level=logging.CRITICAL) # the report below is the output; server logs are covered in Part 3
async def check_era(store: NoteStore, mode: str) -> list[str]:
"""Run the same checks against one protocol era and return report lines."""
lines = []
async with Client(build_server(store), mode=mode) as client:
lines.append(f"[{mode}] negotiated {client.protocol_version}")
tools = {t.name: t for t in (await client.list_tools()).tools}
lines.append(f"[{mode}] tools={sorted(tools)} required={tools['search_notes'].input_schema['required']}")
found = await client.call_tool("search_notes", {"query": "sleep memory", "limit": 3})
lines.append(f"[{mode}] search hits={[h['note_id'] for h in found.structured_content['hits']]}")
bad = await client.call_tool("search_notes", {"query": "sleep", "limit": 0})
lines.append(f"[{mode}] limit=0 rejected at the boundary: is_error={bad.is_error}")
created = await client.call_tool("create_note", {"title": f"Lab check {mode}", "body": "Written by the lab."})
lines.append(f"[{mode}] created {created.structured_content['uri']}")
latin1 = store.root / "cafe-visit.md"
latin1.write_bytes(b"---\ntitle: Caf\xe9\n---\nLatin-1 bytes.\n")
try:
crash = await client.call_tool("search_notes", {"query": "anything"})
lines.append(f"[{mode}] unexpected error, client sees only: {crash.content[0].text!r}")
finally:
latin1.unlink()
try:
await client.read_resource("notes://no-such-note")
except MCPError as exc:
lines.append(f"[{mode}] missing note: code={exc.code} (see the xfail test)")
return lines
async def check_reindex(store: NoteStore, index: Path) -> list[str]:
"""Cancel one reindex part way, then let a second one finish."""
seen: list[float] = []
async def record(progress: float, total: float | None, message: str | None) -> None:
seen.append(progress)
async with Client(build_reindex_server(store, index, delay=0.1)) as client:
with anyio.move_on_after(0.35) as scope:
await client.call_tool("reindex_notes", {}, progress_callback=record)
first = f"[reindex] cancelled={scope.cancelled_caught} after {len(seen)} reports, index written={index.exists()}"
seen.clear()
done = await client.call_tool("reindex_notes", {}, progress_callback=record)
second = f"[reindex] full run: {len(seen)} reports, {done.structured_content}"
return [first, second]
async def time_cycle(target) -> float:
"""Milliseconds for connect, one tool call, and close."""
start = time.perf_counter()
async with Client(target) as client:
await client.call_tool("search_notes", {"query": "sleep"})
return (time.perf_counter() - start) * 1000
async def main() -> None:
os.environ.setdefault("NOTES_DIR", "notes")
settings = load_settings()
print(f"settings: transport={settings.transport} notes_dir={settings.notes_dir.name}/ log_level={settings.log_level}")
with tempfile.TemporaryDirectory() as tmp:
notes = Path(tmp) / "notes"
shutil.copytree(settings.notes_dir, notes) # never write into the real notes folder
store = NoteStore(notes)
for mode in ("auto", "legacy"):
print("\n".join(await check_era(store, mode)))
print("\n".join(await check_reindex(store, Path(tmp) / "index.json")))
stdio = StdioServerParameters(
command=sys.executable,
args=["-m", "notes_assistant.server"],
env={"PYTHONPATH": ".", "NOTES_DIR": str(notes), "NOTES_LOG_LEVEL": "CRITICAL"},
)
mem = [await time_cycle(build_server(store)) for _ in range(5)]
spawn = [await time_cycle(stdio) for _ in range(3)]
print(f"[timing] connect+call+close: in-memory {min(mem):.1f} ms, stdio spawn {min(spawn):.0f} ms (best of 5 and 3)")
print(f"[files] notes now in the temp copy: {len(store.list_notes())}")
anyio.run(main)Code explained