Lesson 05 Tools: Design, Schemas, Errors
Theme:

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.

View solutions →