Lesson 05 Tools: Design, Schemas, Errors
Theme:
Lesson 05 · Slide 1

Lesson 05 — Tools: Design, Schemas, Errors

Pydantic models, structured errors, and tool ergonomics.

MCP Servers Module 2

Lesson 05 · Slide 2

Why move beyond @mcp.tool() decorators

  • Multi-field inputs — one tool, several typed arguments.
  • Validation — reject bad inputs before they hit your code.
  • Optional fields — with sensible defaults.
  • Enums — constrain a field to a fixed set of values.
  • Defaults — declared once, applied everywhere.

FastMCP supports Pydantic models for input — your tool signature can take a typed model and the schema is derived from it.

Lesson 05 · Slide 3

The pkb.search tool we'll add

from pydantic import BaseModel

class SearchArgs(BaseModel):
    query: str
    limit: int = 10

@mcp.tool()
def search(args: SearchArgs) -> list[str]:
    """Search notes by full-text query."""
    ...

The LLM sees a schema with query (required) and limit (optional, default 10) — derived automatically from the Pydantic model.

Lesson 05 · Slide 4

What Pydantic gives you for free

Lesson 05 · Slide 5

Designing tool names

Why noun.verb? It matches the convention many official servers use, and the dot acts as a namespace — pkb.search and github.search are clearly different tools, even if the verb collides.

Lesson 05 · Slide 6

Designing tool descriptions

If the LLM picks the wrong tool, the description is almost always to blame — rewrite it before you blame the model.

Lesson 05 · Slide 7

Raising errors

from mcp.shared.exceptions import McpError
from mcp.types import ErrorData, INVALID_PARAMS

@mcp.tool()
def search(args: SearchArgs) -> list[str]:
    if args.limit > 100:
        raise McpError(ErrorData(code=INVALID_PARAMS, message="limit must be <= 100"))
    ...

Structured errors travel back to the LLM as JSON-RPC errors with proper codes — the client can distinguish "bad input" from "tool failed" from "server crashed", and surface the right message to the user.

Lesson 05 · Slide 8

Idempotency

Retries are a contract

A tool the client may retry should be idempotent or the server should reject retries explicitly. The client can't tell whether a transient timeout means the tool ran or didn't.

Lesson 05 · Slide 9

Returning structured content

Tools can return text, images, or resource references via TextContent, ImageContent, and EmbeddedResource. The return type doesn't have to be a string — FastMCP wraps the value in the right content block for you.

from mcp.types import TextContent

return [TextContent(type="text", text="hello")]

For most tools, returning a plain Python value (string, list, dict) is enough — FastMCP serialises it into a TextContent block automatically.

Lesson 05 · Slide 10

Naming the result fields

When returning structured objects (Python dicts), pick field names that are self-documenting — the LLM will use them in its reasoning. {"note_id": "hello", "matched_at": 12} reads better than {"id": "hello", "pos": 12}, and the LLM is more likely to thread the right value into its next step.

Lesson 05 · Slide 11

pkb.search implementation

from pydantic import BaseModel, Field
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData, INVALID_PARAMS

NOTES = {"hello": "Hello note body", "todo": "Pick up groceries"}

class SearchArgs(BaseModel):
    query: str = Field(..., description="case-insensitive substring match")
    limit: int = Field(10, ge=1, le=100)

def search(args: SearchArgs) -> list[str]:
    q = args.query.lower()
    hits = [k for k, v in NOTES.items() if q in v.lower() or q in k.lower()]
    return hits[: args.limit]
Lesson 05 · Slide 12

Testing tools in isolation

def test_search_finds_hello():
    result = search(SearchArgs(query="hello"))
    assert "hello" in result

With FastMCP-decorated tools, the underlying function is still callable as a plain Python function — you can unit-test it directly, no transport, no client, no JSON-RPC envelope. The decorator wraps the function for MCP but doesn't replace it.

Lesson 05 · Slide 13

Recap & what's next

Coming up: Lesson 06 — resources and prompts (the other two primitives).
1 / ?