Lesson 08 A Multi-Tool Real Server
Theme:

Homework — A Multi-Tool Real Server

Lesson 08 · MCP Servers · three extensions to PKB: composition, elicitation, persistence

These three tasks layer on top of the lab. You'll keep editing the mcp-servers/sample-server/python/pkb/ package — add a destructive tool guarded by user confirmation, compose two existing tools into one, and finally swap the in-memory NOTES dict for a JSON file so PKB survives a restart.

Before you start

The lab must be done first — PKB needs to expose echo, search, summarize, fetch, write, and summarize_and_save. The editable install (pip install -e . from mcp-servers/sample-server/python/) should still be active. Tasks 1 and 2 add new tools; Task 3 changes the storage layer that all of them sit on.

Task 1 — Add a delete tool with confirmation

Add a destructive tool to PKB, but make it impossible for an LLM to invoke silently. In pkb/server.py, register:

@mcp.tool()
async def delete(id: str, ctx: Context) -> str:
    ...

Use the elicitation primitive (the same one you met in Lesson 07) to require explicit user confirmation before the deletion goes through. The user must type yes — anything else (including an empty payload, the word no, or the user clicking "decline" on the elicitation dialog) must abort the delete and return "deletion cancelled".

Raise an McpError(INVALID_PARAMS) for unknown ids, the same way the lab's fetch_note does.

✓ Self-check: in Inspector, calling delete(id="hello") triggers an elicit dialog asking "Confirm deletion of hello? (yes/no)". Typing yes removes the note (verify with fetch — it should now raise INVALID_PARAMS); typing no (or declining) returns "deletion cancelled" and the note is still readable.

Task 2 — Add search_and_fetch

Compose two existing tools into one. The new tool takes a query and an integer n and returns a dict mapping title → body for the top n matches. Reuse the existing search and NOTES directly — do not re-implement matching.

Cap n at 5. Enforce the cap at the schema layer (Pydantic Field(..., le=5)), not inside the function body. A careless LLM that asks for n=10000 should be rejected by FastMCP before your code runs.

✓ Self-check: calling search_and_fetch(query="hello", n=2) returns a dict with at most 2 key-value pairs, every key matching the query. Calling with n=99 returns a validation error (FastMCP rejects the call); the function body is never reached.

Task 3 — Persistence sketch

Replace the in-memory NOTES: dict[str, str] with a JSON file at ~/.pkb-notes.json. On import, load the file into NOTES; if the file doesn't exist yet, seed it with the three default notes (hello, todo, ideas). On every mutation — write_note, summarize_and_save, and the new delete from Task 1 — save the dict back to disk.

Do not save on reads (fetch_note, search, search_and_fetch). Writing on every read is wasteful and increases the window for partial-write corruption.

Heads up

This is in-process, single-writer persistence — fine for a local dev server, but it is not concurrency-safe. Two pkb processes pointed at the same file will race; one process's writes will silently overwrite the other's. Lesson 09 will add per-user scoping; a real production server would reach for SQLite or a real database here.

✓ Self-check: kill the pkb process (Ctrl-C in the terminal where mcp dev is running), restart it, fetch a previously-written note — it's still there. Open ~/.pkb-notes.json in a text editor: the JSON is human-readable and contains the seed notes plus anything you wrote during the session.

View solutions →