Lesson 3 · Phase 1 · The Protocol

FastMCP: The Whole Server in 10 Lines

The @mcp.tool() decorator turns type hints into inputSchema and docstrings into descriptions. Build the same server from lesson 2 in a tenth of the code, then drive a real MCP session in memory with the FastMCP client.

What you'll learn

  • Register tools with @mcp.tool() decorators
  • See how type hints and docstrings become the tool contract
  • Run initialize / list / call through the FastMCP client

Same server, two ways to write it

Toggle between lesson 2's hand-rolled server and the FastMCP version. On the wire they are indistinguishable — a client speaking initialize / tools/list / tools/call cannot tell which one it is talking to.

from fastmcp import FastMCP

mcp = FastMCP("MyServer")

@mcp.tool()                        # <- registers this function as an MCP tool
def add(a: int, b: int) -> int:    # <- type hints  ->  "inputSchema"
    """Add two numbers together and return the sum."""   # <- docstring -> "description"
    return a + b

@mcp.tool()
def shout(text: str) -> str:
    """Return the given text in capital letters with an exclamation mark."""
    return text.upper() + "!"

Same server, same three methods on the wire. The decorator inspects each function and generates the descriptor: type hints become the inputSchema, the docstring becomes the description. The contract can no longer drift from the code, because the contract is the code.

Where did the 30 lines go?

They didn't disappear — FastMCP writes them for you, by reading your function:

type hints
def add(a: int, b: int)
inputSchema
"inputSchema": {"properties": {"a": {"type": "integer"}, ...}, "required": ["a", "b"]}
docstring
"""Add two numbers together and return the sum."""
description
"description": "Add two numbers together and return the sum."
decorator
@mcp.tool()  +  the function name
name
"name": "add"  — listed in every tools/list reply

A real MCP session, in memory

FastMCP ships a client too, so you can drive a full session — handshake, discovery, call — without ever opening a socket:

from fastmcp import Client

async with Client(mcp) as client:                 # entering the block = initialize

    for t in await client.list_tools():           # = tools/list
        print(t.name, "|", t.description)
        print("   schema:", t.inputSchema, "\n")

    out = await client.call_tool("add", {"a": 40, "b": 2})    # = tools/call
    print("result:", out.content[0].text)         # results come back as content BLOCKS

Notice how each line of the client maps onto one of the three protocol methods you stepped through last lesson. You now have a server and a client — next lesson we hand both to a model and get our first agent.