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.