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.