# ex-mcp-server — a tool server an agent can actually trust

An agent that calls a tool with the wrong arguments should get a clear, structured error back —
not a stack trace, and not a server that falls over. MCP (the Model Context Protocol) is the
standard shape for that: a server declares its tools with a JSON schema, and a well-behaved
implementation enforces it before the tool body ever runs.

## What to implement

`src/mcp_notes/server.py` — an MCP server, over stdio, exposing two tools against an in-memory
notes list:

- `search_notes(query, limit=10)` — notes whose title or body contains `query` (case-insensitive),
  capped at `limit`.
- `add_note(title, body)` — appends a note and returns it.

You edit four things:

- `SEARCH_SCHEMA` / `ADD_SCHEMA` — JSON Schema for each tool's arguments, tight enough that a bad
  call (wrong type, missing field) is rejected *before* it reaches your function.
- `search_notes` / `add_note` — the actual logic.
- `list_tools` — registers both tools, each with its schema, so `tools/list` shows them.
- `call_tool` — dispatches by name to one of the two functions above, and raises a clear
  `ValueError(f"Unknown tool: {name!r}")` for anything else.

`main` and the stdio wiring are done for you — the point of this exercise is the tool contract,
not the transport.

## Run it

```
cd exercises/ex-mcp-server
python3 -m venv /tmp/lbv-mcp && /tmp/lbv-mcp/bin/pip install -q "mcp==1.30.0" pytest
/tmp/lbv-mcp/bin/python -m pytest -q
```

(`uv sync && uv run pytest -q` works too.)

## If you get stuck

- **The schema** — `mcp`'s own `call_tool()` decorator validates `arguments` against the
  `inputSchema` you register in `list_tools` (via `jsonschema.validate`) *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` message. Check the name yourself and raise a
  `ValueError` that says what went wrong.
- **Reading a red row** — `pytest -q -x --tb=short` stops at the first failure; the message names
  the request that got the wrong response, not the fix.
