Homework — Tools: Design, Schemas, Errors
Lesson 05 · MCP Servers · ~45 min
Three tasks that build directly on the lab. You'll tighten the schema with a Literal enum,
pick the right JSON-RPC error code for a "not found" case, and prove that FastMCP doesn't get in the way
of plain pytest. All edits live in mcp-servers/sample-server/python/pkb/tools.py unless noted.
Setup
Run mcp dev pkb/server.py from mcp-servers/sample-server/python/ so Inspector
reloads as you edit. Keep its Tools tab open in your browser.
Task 1 — Add a Literal field
Modify SearchArgs to add a third field:
mode: Literal["title", "body", "both"] = "both"
Then teach search to honor it: when mode == "title" it should match against
note titles only, mode == "body" against bodies only, and mode == "both"
(the default) against either. Don't forget to from typing import Literal.
✓ Self-check: Inspector's schema panel for pkb.search shows
mode as an enum with exactly three string values. Calling the tool with
{"query": "hello", "mode": "title"} returns ["hello"] only — note that
ideas (whose body is "Build an MCP server tutorial") does not contain the
substring hello anywhere, so it stays out regardless of mode; this confirms the title
filter isn't accidentally falling through to the body.
Task 2 — Add a real error path
Add a new tool pkb.delete(title: str) that removes a note from NOTES. If the
title isn't present, raise McpError with code METHOD_NOT_FOUND instead of
INVALID_PARAMS:
from mcp.types import METHOD_NOT_FOUND
# ...
raise McpError(ErrorData(code=METHOD_NOT_FOUND, message=f"unknown note title: {title}"))
The point of this exercise is not that METHOD_NOT_FOUND is the "correct" code in some
absolute sense — it's that you get to make a deliberate choice between codes instead of mechanically
reaching for INVALID_PARAMS. A missing resource is closer to "the method/target doesn't
exist" than to "your arguments were malformed."
✓ Self-check: in Inspector, calling pkb.delete with
{"title": "nonexistent"} returns a JSON-RPC error whose code field is
-32601. Calling with {"title": "todo"} succeeds and a subsequent
pkb.search for "todo" returns an empty list.
Task 3 — Test the tool standalone
Create mcp-servers/sample-server/python/tests/test_tools.py with:
def test_search_respects_limit():
from pkb.tools import search, SearchArgs
result = search(SearchArgs(query="hello", limit=1))
assert len(result) <= 1
Run pytest from mcp-servers/sample-server/python/. No fixtures, no
mcp dev, no Inspector — just the function.
✓ Self-check: the test passes. You can articulate why: the
@mcp.tool() decorator registers search with the FastMCP server, but the
original function object is still bound to the module under its original name, so unit tests
import and call it like any other Python function.