In Java, a method signature is enforced by the compiler before your code ever runs. An agent calling a tool has no compiler — the patterns you just stepped through assumed every tool call just works. MCP (the Model Context Protocol) is the standard shape for the part that actually enforces the contract: a server declares each tool's arguments as a JSON schema, and a well-behaved implementation rejects a bad call with a clear error before the tool body runs at all — not a stack trace, and not a server that falls over on one unlucky request.
search_notes(query, limit=10) and add_note(title, body).SEARCH_SCHEMA, ADD_SCHEMA) tight enough
that a bad argument — wrong type, missing field — is rejected by the SDK's own
jsonschema.validate call before your function runs, not caught by you.call_tool's dispatch: the right function by name, and a clear
ValueError(f"Unknown tool: {name!r}") for anything else — a bare dict lookup
turns an unknown name into a confusing internal exception instead.The tests are ordinary pytest and ship in the public folder with the
starter — read them first; the names below are the check list. Solutions are not published.
pyproject.toml pins mcp==1.30.0: the SDK's shape (decorators,
exception handling, even which module has Server) has moved fast across
versions, and a schema-validation exercise needs one version to be honest against.
Needs git. uv installs the right Python itself, so nothing else is required.
# once, anywhere on your machine
git clone https://github.com/theDocWho/ai-ml-roadmap.git
cd ai-ml-roadmap
No git? Download the ZIP, unzip it, and cd into the unzipped folder instead.
From the repo root:
# one-time: uv (https://docs.astral.sh/uv/) manages the venv and pins Python ≥ 3.12 cd exercises/ex-mcp-server && uv sync && uv run pytest -q # the same bar the reference solution clears uv run ruff check . && uv run mypy src
Done when uv run pytest -q prints 6 passed. Each
test spawns your server as a real subprocess and talks to it over stdio with
mcp's own ClientSession — exactly how an agent would.
test_tools_list_has_both — tools/list reports exactly
search_notes and add_note.test_invalid_args_get_schema_error_not_a_crash — a non-integer
limit comes back as a clean "Input validation error", not a Python exception
leaking out of your function.test_note_round_trips — a note added with add_note is found by a
search_notes call matching its title.test_limit_is_honoured — five matching notes, limit=2, exactly
two results.test_unknown_tool_gets_a_proper_error — calling a tool that was never listed
returns an error naming the problem, not an internal lookup exception's repr.test_server_exits_cleanly_on_eof — closing stdin lets the process exit on its
own well under the client's force-kill timeout, instead of hanging until it gets killed.mcp version), ruff and mypy configThis is self-attestation — the site cannot see your terminal, so the box and the button are you telling The Path the suite went green on your machine.
mcp's own call_tool() decorator validates
arguments against the inputSchema you register in
list_tools before calling your function. An empty {"type": "object"}
schema validates everything, which is exactly the trap: write required and
type for every field.search_notes — lowercase both query and the note text
before comparing; slice the matches with [:limit].call_tool's unknown-tool branch — a bare dict lookup
(HANDLERS[name](**arguments)) turns an unknown name into a confusing
KeyError. Check the name yourself and raise a ValueError that says
what went wrong.uv run pytest -q -x --tb=short stops at the first
failure and shows which request got the wrong response, not the fix.