labs | 01 | hello
lab 01 | ~5 min | segment 1

Six lines. The server lives.

FastMCP turns a plain Python function into an agent-callable tool with one decorator. No SDK boilerplate, no schema written by hand. You drop six lines in a file, run it, and you have a real MCP server speaking JSON-RPC over stdio. This is the minimum legal server, and it is the spine of everything that follows tonight.

step 1

Install FastMCP.

One install. The package is fastmcp (v3.x). It pulls the MCP wire protocol and a CLI with it.

install
pip install fastmcp
step 2

Write the six lines.

Save this as server.py. Every line is load-bearing. There is no seventh line.

server.py
from fastmcp import FastMCP

mcp = FastMCP("hello-toolsmith")

@mcp.tool
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    return a + b

if __name__ == "__main__":
    mcp.run()
  • importFastMCP is the server. It owns the registry of tools, resources, and prompts, and it knows how to speak the wire protocol.
  • mcp = ...The string is the server name an agent sees on connect. Make it specific. It is the first piece of identity the agent reads.
  • @mcp.toolThis decorator is the whole trick. It reads your type hints and docstring and auto-generates the JSON schema the agent needs. You never write the schema by hand.
  • def addThe signature becomes the input schema (a: int, b: int). The docstring becomes the tool description, which is the agent's primary signal for when to call it.
  • mcp.run()Default transport is stdio. The server reads JSON-RPC requests on stdin and writes responses on stdout. That is all stdio is.
step 3

Boot it.

Run the file. It will sit silently waiting for JSON-RPC on stdin. Silence is correct: a stdio server has no banner, it is a pipe.

terminal
python server.py

Press Ctrl+C to stop it. Now let's actually talk to it.

step 4

Call the tool with a raw JSON-RPC ping.

You do not need a fancy client to prove the server works. Pipe three JSON-RPC messages into it and read the reply. This is exactly what an agent sends: initialize, then notifications/initialized, then tools/call.

bash | zsh
printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}' \
  | python server.py
powershell
@(
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
  '{"jsonrpc":"2.0","method":"notifications/initialized"}'
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}'
) | python server.py
ping.py (run alongside server.py)
import json, subprocess, sys

msgs = [
    {"jsonrpc": "2.0", "id": 1, "method": "initialize",
     "params": {"protocolVersion": "2025-06-18", "capabilities": {},
                "clientInfo": {"name": "ping", "version": "0"}}},
    {"jsonrpc": "2.0", "method": "notifications/initialized"},
    {"jsonrpc": "2.0", "id": 2, "method": "tools/call",
     "params": {"name": "add", "arguments": {"a": 2, "b": 3}}},
]
payload = "".join(json.dumps(m) + "\n" for m in msgs)

p = subprocess.run([sys.executable, "server.py"],
                   input=payload, capture_output=True, text=True)
print(p.stdout)
what you'll see (the tools/call reply, id 2)
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"5"}],
 "structuredContent":{"result":5},"isError":false}}

The initialize line also returns the server's capabilities and your tool list. The "text":"5" in the id-2 reply is your function's return value, marshalled back across the wire. The server ran your Python. That is an MCP call.

what just happened

You spoke the MCP handshake by hand. A real client always sends initialize first to negotiate protocol version and capabilities, then the notifications/initialized acknowledgement, and only then is it allowed to call tools. The decorator turned add into a tool whose schema (two integer args, returns an integer) was inferred from your type hints. No schema file. No SDK class. Six lines.

inspect it like a human (optional)

If you prefer a UI over raw JSON, FastMCP ships an inspector. It opens a local web view where you can see the tool list and fire add with a form.

terminal
fastmcp dev server.py

You can also drop this server into Claude Desktop or Cline by pointing their MCP config at python /full/path/server.py. Same server, real agent client.

the trap you just dodged

cp1252 on Windows. The instant your print() or docstring contains a fancy character (a curly quote, an arrow glyph, an em dash), a Windows stdout crashes with UnicodeEncodeError and the server dies on connect. Keep tool code ASCII: use -> not the arrow glyph, straight quotes, PASS / FAIL not check marks. If you genuinely need Unicode IO, add import sys; sys.stdout.reconfigure(encoding="utf-8") at the top. This single rule has killed live workshops.