Chapter 3 · Part 2

Tools: let it do things

Tools are the primitive that lets an AI act — run a query, call an API, change a file. They're the POST-like verbs of your server. A calculator was a fine hello-world; now add tools that do something real.

More than one, and with real types

Add a couple of tools to server.py. Notice how much the type hints and docstring carry — a list[str], an optional argument with a default, all become the schema automatically:

server.py — more tools
import httpx

@mcp.tool()
def search_notes(query: str, limit: int = 5) -> list[str]:
  """Search the user's notes and return the matching lines.

  Use this whenever the user asks what they wrote or saved about a topic.
  """
  with open("notes.txt") as f:
      hits = [line.strip() for line in f if query.lower() in line.lower()]
  return hits[:limit]


@mcp.tool()
def word_count(text: str) -> int:
  """Count the number of words in a piece of text."""
  return len(text.split())

Each decorated function becomes a tool the AI can choose. query: str and limit: int = 5 generate the input schema — limit is optional because it has a default. The return annotation (list[str], int) tells the client what comes back.

The docstring is the interface

The model decides whether to call a tool almost entirely from its docstring. Treat it as the tool's user manual, and say when to reach for it, not just what it does.

Write the docstring for the model
  • Weak: """Search notes.""" — the model has to guess when it applies.
  • Better: """Search the user's notes and return matching lines. Use this whenever the user asks what they wrote or saved about a topic."""

The trigger condition is what turns a tool the model has into a tool the model uses.

Return something useful

Return plain Python — strings, numbers, lists, dicts — and FastMCP serializes it for you. A dict comes back as structured data the client can read field by field, which is usually friendlier than cramming everything into one string.

⚠️Tool inputs are untrusted

Whatever calls your server controls the arguments. search_notes only reads a fixed file — but the moment a tool opens an arbitrary path, runs a command, or writes data, validate the input first. We'll come back to this when we secure the server.

Tools cover doing. Next: the two primitives for reading data and reusing prompts.