CourseModel Context Protocol · Module 5: Building Servers · part 32 of 83
Part 32 · Module 5: Building Servers

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

The rest of this course is yours to keep

This course is bought on its own, once, and stays readable afterwards, including the parts added to it later.