Building Tools with FastMCP: Servers, Tools, Resources, Prompts, Transports
Learning Objectives
Explain the Model Context Protocol client-server architecture and how it decouples tools from any single application.
Build a FastMCP server exposing tools, resources, and prompts with typed Python decorators.
Choose correctly between stdio and HTTP (streamable) transports for a given deployment.
Distinguish MCP tools (model-controlled) from resources (application-controlled) and prompts (user-controlled).
Pre-Reading Check — Why MCP Exists
1. With N AI applications and M systems, how many integrations does a shared protocol like MCP require compared to bespoke connectors?
N × M with the protocol; N + M without it
N + M with the protocol; N × M without it
N + M in both cases; the protocol only changes latency
N × M in both cases; the protocol only changes security
2. In MCP terminology, which component is the AI application (e.g., Claude Desktop) that runs the model and hosts one or more clients?
The transport
The server
The host
The JSON-RPC layer
3. What is the relationship between an MCP client and an MCP server?
One client can multiplex many servers over a single session
Each client maintains a 1:1 session with a single server
Servers embed clients, not the other way around
Clients and servers share one process and never negotiate
4. Regardless of the transport used, what message format do MCP clients and servers exchange?
Raw HTTP form-encoded bodies
Protocol Buffers over gRPC
JSON-RPC 2.0, UTF-8 encoded
Plain YAML documents
5. How does MCP relate to Chapter 1's raw tool-use primitive?
It replaces tool-use with a completely different model that does not use schemas
It lifts the same tool-use idea into a decoupled, cross-application layer where tools live in a server
It removes the model's ability to decide when a tool runs
It requires tools to be re-implemented separately for each host
Section 1: Why MCP Exists
Key Points
Without a shared protocol, connecting N AI applications to M systems costs N × M bespoke connectors; MCP collapses this to N + M.
MCP is an open standard defining three roles: host (the AI application), client (a 1:1 in-host connector), and server (what you build).
All communication uses JSON-RPC 2.0 over a transport, beginning with an initialization handshake that negotiates version and capabilities.
MCP generalizes Chapter 1's inline tool-use primitive: the tool definition and its execution move out of your app and into a server any host can discover at runtime.
Because MCP is open, one well-built server works across Claude Desktop, Claude Code, Cursor, and other hosts with only a config change.
Imagine a world without a shared protocol. You have N AI applications (Claude Desktop, Cursor, a custom agent) and M systems they want to reach (GitHub, Postgres, Drive). If every application needs a bespoke connector for every system, you owe N × M integrations, and every team rebuilds the same adapter slightly differently.
MCP collapses this to N + M. Each application implements the MCP client side once; each system is wrapped in an MCP server once; any client can then talk to any server. This is the move USB made for peripherals and the Language Server Protocol made for editors: a universal port replaces a combinatorial mess of adapters.
graph TD
subgraph "Without MCP: N x M bespoke connectors"
A1["Claude Desktop"] --> S1["GitHub"]
A1 --> S2["Postgres"]
A1 --> S3["Drive"]
A2["Cursor"] --> S1
A2 --> S2
A2 --> S3
A3["Custom Agent"] --> S1
A3 --> S2
A3 --> S3
end
subgraph "With MCP: N + M via a shared port"
B1["Claude Desktop"] --> MCP["MCP (shared protocol)"]
B2["Cursor"] --> MCP
B3["Custom Agent"] --> MCP
MCP --> T1["GitHub server"]
MCP --> T2["Postgres server"]
MCP --> T3["Drive server"]
end
Host, Client, Server, Transport
MCP host — the AI application that runs the model, manages communication, and hosts one or more clients (Claude Desktop, Cursor, a custom agent).
MCP client — the connector inside the host that maintains a 1:1 session with a single server, handling protocol negotiation, capability discovery, and message routing. Three servers means three clients.
MCP server — an independent program that exposes capabilities (tools, resources, prompts). This is usually what you build.
Transport — the pipe over which client and server exchange messages. All communication uses JSON-RPC 2.0.
A useful analogy: the host is a restaurant dining room, the client is a waiter assigned to one kitchen, the server is a kitchen, and JSON-RPC is the standardized order-ticket format they all read. Swap in a new kitchen and the same waiters, tickets, and dining room work unchanged.
flowchart LR
subgraph Host["MCP Host (AI application)"]
Model["Model + User"]
C1["Client 1"]
C2["Client 2"]
C3["Client 3"]
Model --- C1
Model --- C2
Model --- C3
end
C1 -->|"JSON-RPC 2.0 over transport"| S1["Server: Weather"]
C2 -->|"JSON-RPC 2.0 over transport"| S2["Server: GitHub"]
C3 -->|"JSON-RPC 2.0 over transport"| S3["Server: Postgres"]
S1 --> Cap1["Tools / Resources / Prompts"]
S2 --> Cap2["Tools / Resources / Prompts"]
S3 --> Cap3["Tools / Resources / Prompts"]
[Animation slot: N×M web of connectors collapsing into an N+M hub as the MCP "port" slides into place]
How MCP Relates to the Raw Tool-Use Primitive
In Chapter 1, a tool was a schema passed inline to the API, and your application executed it. With MCP, the tool definition and its execution move out of your application into a server any host can discover at runtime. The mechanics rhyme — a tool still has a name, description, and JSON input schema, and the model still decides when to call it — but ownership changes. The host's client asks the server "what tools do you have?" during capability discovery, translates them into the tool-use format the model understands, and relays invocations back. MCP is the raw tool-use primitive lifted into a decoupled, reusable, cross-application layer.
[Animation slot: capability-discovery handshake — client asks "what tools?", server lists them, client translates to tool-use format]
Post-Reading Check — Why MCP Exists
1. With N AI applications and M systems, how many integrations does a shared protocol like MCP require compared to bespoke connectors?
N × M with the protocol; N + M without it
N + M with the protocol; N × M without it
N + M in both cases; the protocol only changes latency
N × M in both cases; the protocol only changes security
2. In MCP terminology, which component is the AI application (e.g., Claude Desktop) that runs the model and hosts one or more clients?
The transport
The server
The host
The JSON-RPC layer
3. What is the relationship between an MCP client and an MCP server?
One client can multiplex many servers over a single session
Each client maintains a 1:1 session with a single server
Servers embed clients, not the other way around
Clients and servers share one process and never negotiate
4. Regardless of the transport used, what message format do MCP clients and servers exchange?
Raw HTTP form-encoded bodies
Protocol Buffers over gRPC
JSON-RPC 2.0, UTF-8 encoded
Plain YAML documents
5. How does MCP relate to Chapter 1's raw tool-use primitive?
It replaces tool-use with a completely different model that does not use schemas
It lifts the same tool-use idea into a decoupled, cross-application layer where tools live in a server
It removes the model's ability to decide when a tool runs
It requires tools to be re-implemented separately for each host
Pre-Reading Check — A FastMCP Server From Scratch
1. When you decorate a function with @mcp.tool, what does FastMCP derive from the parameter type hints?
The tool's model-facing description
The validated JSON input schema
The transport the server runs on
The URI the tool is addressed by
2. What does the function's docstring become in a FastMCP tool?
Dead code that FastMCP ignores
The input validation rules
The human- and model-facing description the model uses to decide when to call the tool
The session ID for HTTP transport
3. Which import corresponds to the standalone FastMCP 2.0 package?
from mcp.server.fastmcp import FastMCP
from fastmcp import FastMCP
import mcp.fast as FastMCP
from anthropic import FastMCP
4. What is the purpose of the MCP Inspector (launched via fastmcp dev server.py)?
To compile the server into a binary for production
To interactively list and invoke a running server's tools, resources, and prompts for testing
To register the server with Claude Desktop automatically
To convert stdio servers into HTTP servers
5. Why is FastMCP said to cut setup time roughly 5x versus the raw SDK?
It uses a faster network protocol than JSON-RPC
It caches tool results so the model calls them less often
Schema, validation, and documentation are all derived from typed Python you would have written anyway
It skips input validation entirely to save code
Section 2: A FastMCP Server From Scratch
Key Points
FastMCP is the Pythonic, decorator-based framework: a decorated, type-hinted function is a tool, with no boilerplate.
Type hints become the validated JSON input schema; the docstring becomes the model-facing description; the return annotation shapes structured output.
FastMCP 1.0 lives in the MCP Python SDK (from mcp.server.fastmcp import FastMCP); FastMCP 2.0 is the standalone fastmcp package (from fastmcp import FastMCP).
Network-bound tools should be async def so they do not block the event loop.
Test in isolation with fastmcp dev and the MCP Inspector before wiring the server into a real host.
Every server starts with a FastMCP instance, then decorates functions to expose them. This is a complete, valid MCP tool:
from fastmcp import FastMCP
mcp = FastMCP("Demo 🚀")
@mcp.tool
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
The magic is that FastMCP reads the signature and doc: the parameter names and type hints become the JSON input schema (send a string where an int is required and the framework rejects it before your code runs); the docstring becomes the description the model reads to decide when to call the tool; and the return annotation informs how results are structured. Your type hints and docstrings are not decoration — they are the API contract the model reads.
A richer tool shows async support and a structured docstring whose Args section clarifies each parameter, drawn from the official quickstart with the SDK-bundled FastMCP:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
# ... call an upstream weather API here ...
return "No active alerts."
[Animation slot: a decorated Python function transforming into a JSON schema + description + structured-output shape as type hints and docstring are "read" by FastMCP]
Running and Inspecting the Server
A server needs an entry point that starts the transport. The canonical local pattern runs over stdio; run it with uv run weather.py. The recommended toolchain is uv (Python 3.10+, MCP SDK 1.2.0+ or the standalone fastmcp).
def main():
mcp.run(transport="stdio")
if __name__ == "__main__":
main()
Before wiring a server into a real host, test it in isolation. The MCP Inspector is a web-based tool that connects to a running server and lets you interactively list and invoke its tools, read its resources, and render its prompts. FastMCP integrates it through the CLI with fastmcp dev server.py, giving a tight click-a-tool, fill-args, see-the-result loop that beats round-tripping through Claude Desktop for every change.
On return handling: because FastMCP knows your return annotation, it serializes results into structured output automatically. Returning a plain int, str, or dict produces a well-typed tool result the client can present to the model. You focus on returning meaningful Python; the framework handles the wire format.
Post-Reading Check — A FastMCP Server From Scratch
1. When you decorate a function with @mcp.tool, what does FastMCP derive from the parameter type hints?
The tool's model-facing description
The validated JSON input schema
The transport the server runs on
The URI the tool is addressed by
2. What does the function's docstring become in a FastMCP tool?
Dead code that FastMCP ignores
The input validation rules
The human- and model-facing description the model uses to decide when to call the tool
The session ID for HTTP transport
3. Which import corresponds to the standalone FastMCP 2.0 package?
from mcp.server.fastmcp import FastMCP
from fastmcp import FastMCP
import mcp.fast as FastMCP
from anthropic import FastMCP
4. What is the purpose of the MCP Inspector (launched via fastmcp dev server.py)?
To compile the server into a binary for production
To interactively list and invoke a running server's tools, resources, and prompts for testing
To register the server with Claude Desktop automatically
To convert stdio servers into HTTP servers
5. Why is FastMCP said to cut setup time roughly 5x versus the raw SDK?
It uses a faster network protocol than JSON-RPC
It caches tool results so the model calls them less often
Schema, validation, and documentation are all derived from typed Python you would have written anyway
It skips input validation entirely to save code
Pre-Reading Check — Resources and Prompts
1. What is the central distinction among MCP's three primitives (tools, resources, prompts)?
Which programming language they are written in
Who controls each one — model, application, or user
Which transport they require
How much memory they consume
2. A @mcp.resource is best described as which of the following?
A model-controlled action that may change the world
Application-controlled, read-only, URI-addressed data (analogous to a GET endpoint)
A user-invoked message template
A background subprocess spawned per request
3. In the resource URI weather://{city}/current, what does {city} represent?
A fixed literal that never changes
A template variable, so one function serves a whole family of addressable resources
The transport name
A required authentication token
4. A @mcp.prompt template is controlled by whom?
The model, which decides when to fire it
The application, which loads it as context
The user, who deliberately invokes it (e.g., via a slash command)
The transport layer, automatically
5. Why is it a design mistake to expose a purely read-only data fetch as a @mcp.tool instead of a @mcp.resource?
Tools are slower than resources at runtime
It needlessly hands the model control over context loading that the application should own
Tools cannot return strings, only integers
Resources are the only primitive the model can call
Section 3: Resources and Prompts
Key Points
MCP has three primitives, distinguished by who controls each: tools are model-controlled, resources application-controlled, prompts user-controlled.
Resources (@mcp.resource) are read-only, URI-addressed, side-effect-free context — analogous to HTTP GET.
Resource templates embed path parameters in the URI (e.g., weather://{city}/current), so one function serves an entire dynamic family.
Prompts (@mcp.prompt) are reusable, parameterized message templates the user deliberately invokes to seed a structured interaction.
Matching each capability to the right primitive prevents handing the model control it should not have, or hiding an action inside a "read-only" resource.
A resource is read-only, file-like data identified by a URI that the host application decides to load into context: file contents, configuration, documentation, or an API response. Resources have no side effects — where a tool does something, a resource merely provides something to read. A static resource returns data at a fixed URI:
@mcp.resource("resource://greeting")
def get_greeting() -> str:
return "Hello from FastMCP Resources!"
Real context is rarely one fixed blob. Resource templates embed path parameters, so a single function serves an entire family. A request for weather://london/current calls get_weather("london"):
Table 3.1: Resource URI patterns supported by @mcp.resource.
@mcp.prompt — Reusable Parameterized Templates
A prompt template is a reusable, parameterized message template the user deliberately invokes — typically via a slash command or menu — to start a structured interaction. It captures your best-known phrasing for a recurring task.
@mcp.prompt
def summarize_pr(diff: str, tone: str = "concise") -> str:
"""Draft a pull-request summary from a diff."""
return (
f"Write a {tone} pull-request summary for the following diff. "
f"Group changes by theme and flag any breaking changes.\n\n{diff}"
)
The user picks summarize_pr from a menu, supplies the diff (optionally overriding tone), and the server returns the fully assembled message that seeds the conversation.
The Control Spectrum
Primitive
Decorator
Controlled by
Side effects?
Analogy
Tool
@mcp.tool
Model (with user approval)
Yes — can act
POST / RPC call
Resource
@mcp.resource
Application (host decides)
No — read-only
GET endpoint
Prompt
@mcp.prompt
User (invokes deliberately)
No — assembles a message
Slash command / saved template
Table 3.2: The MCP control spectrum.
graph TD
Spectrum["Who is in charge?"]
Spectrum --> Tool["@mcp.tool Model-controlled Side effects: YES (may act) Analogy: POST / RPC"]
Spectrum --> Resource["@mcp.resource Application-controlled Side effects: NO (read-only) Analogy: GET endpoint"]
Spectrum --> Prompt["@mcp.prompt User-controlled Side effects: NO (assembles message) Analogy: slash command / saved template"]
Getting this taxonomy right prevents a common mistake: exposing a data read as a tool (needlessly handing the model control over context loading the application should own), or hiding a genuine action inside a resource (silently breaking the "no side effects" contract that lets hosts load resources freely).
[Animation slot: a slider labeled "Who is in charge?" moving from Model (tool) to Application (resource) to User (prompt), highlighting each decorator]
Post-Reading Check — Resources and Prompts
1. What is the central distinction among MCP's three primitives (tools, resources, prompts)?
Which programming language they are written in
Who controls each one — model, application, or user
Which transport they require
How much memory they consume
2. A @mcp.resource is best described as which of the following?
A model-controlled action that may change the world
Application-controlled, read-only, URI-addressed data (analogous to a GET endpoint)
A user-invoked message template
A background subprocess spawned per request
3. In the resource URI weather://{city}/current, what does {city} represent?
A fixed literal that never changes
A template variable, so one function serves a whole family of addressable resources
The transport name
A required authentication token
4. A @mcp.prompt template is controlled by whom?
The model, which decides when to fire it
The application, which loads it as context
The user, who deliberately invokes it (e.g., via a slash command)
The transport layer, automatically
5. Why is it a design mistake to expose a purely read-only data fetch as a @mcp.tool instead of a @mcp.resource?
Tools are slower than resources at runtime
It needlessly hands the model control over context loading that the application should own
Tools cannot return strings, only integers
Resources are the only primitive the model can call
Pre-Reading Check — Transports: stdio vs HTTP
1. With the stdio transport, who owns the server's lifecycle?
The server runs independently and the client just connects
The client launches the server as a subprocess and starts/stops it
A load balancer spawns and reaps server instances
The operating system's init system manages it
2. What is the ironclad rule for a stdio server's stdout?
It may print debug logs freely; only stderr is reserved
Nothing but valid MCP messages may go to stdout; logs must go to stderr
All output must be base64-encoded before printing
stdout must be closed immediately after startup
3. Which security requirement is specific to Streamable HTTP servers?
Never write logs to stderr
MUST validate the Origin header (returning 403 if invalid) to prevent DNS-rebinding attacks
MUST run as a subprocess of the client
MUST disable JSON-RPC entirely
4. What is the memorable one-sentence rule for choosing a transport?
Always use stdio; HTTP is deprecated
If the user controls the machine the server runs on, use stdio; otherwise use Streamable HTTP
Use HTTP for local development and stdio for production
Choose whichever transport has lower latency, always stdio
5. In FastMCP, how do you switch a server between stdio and networked HTTP behavior?
Rewrite the tool functions for each transport
Change the single mcp.run(transport=...) call
Deploy to a different cloud provider
Recompile the MCP SDK with a flag
Section 4: Transports — stdio vs. HTTP
Key Points
MCP is transport-agnostic; both standard transports carry the same JSON-RPC, selected by one mcp.run(transport=...) switch. Clients SHOULD support stdio whenever possible.
stdio: the client launches the server as a subprocess over stdin/stdout — ~1 ms latency, client-owned lifecycle, one subprocess per client. Only valid MCP messages may hit stdout; logs go to stderr.
Streamable HTTP (spec 2025-03-26, replacing HTTP+SSE): an independent long-lived server serves many clients over one POST/GET endpoint with optional SSE streaming, sessions, and resumability.
HTTP servers MUST validate the Origin header (403 if invalid), SHOULD authenticate, and SHOULD bind to 127.0.0.1 locally. The stdout constraint is stdio-specific — HTTP servers may log to stdout.
Decision rule: if the user controls the server's machine, use stdio; otherwise use Streamable HTTP for shared, remote, serverless, and high-scale deployments.
With the stdio transport, the client launches the MCP server as a subprocess; the server reads newline-delimited JSON-RPC from stdin and writes to stdout (no embedded newlines). One rule dominates: the server MUST NOT write anything to stdout that is not a valid MCP message — a stray print() corrupts the stream. Logging goes to stderr. A mysteriously dead stdio server is almost always a rogue print to stdout. Characteristics: ultra-low latency (~1 ms, no network hop), one subprocess per client, and a client-owned lifecycle. Scaling means running more processes.
mcp.run(transport="stdio")
The streamable HTTP transport runs an independent, long-lived server that can serve many clients over a single endpoint (e.g., https://example.com/mcp) supporting POST and GET. The client POSTs each JSON-RPC message with an Accept header listing both application/json and text/event-stream; the server responds with a single JSON object or an SSE stream. Posted responses/notifications get 202 Accepted. A client MAY issue a GET to open a standalone SSE stream so the server can push messages. Sessions use an MCP-Session-Id header (missing → 400, terminated → 404, DELETE to end); resumability uses SSE event id plus Last-Event-ID replay. Security: MUST validate Origin (403 if invalid), SHOULD authenticate, SHOULD bind to 127.0.0.1 locally.
mcp.run(transport="http", host="127.0.0.1", port=8000)
# "streamable-http" is also accepted
Dimension
stdio
Streamable HTTP
Deployment locality
Local subprocess on the user's machine
Networked / remote long-lived service
Lifecycle owner
Client spawns and stops the server
Server runs independently
Latency
~1 ms, no network hop
Network round-trip (usually negligible vs. LLM)
Clients per server
One subprocess per client
Many clients on one endpoint
Scaling
Run more processes
Horizontal, stateless mode behind a load balancer
Auth / security
Inherits local trust boundary
MUST validate Origin (403), SHOULD auth, SHOULD bind 127.0.0.1
Logging to stdout
Forbidden — corrupts the stream
Allowed
Sessions / resumability
Process-scoped
MCP-Session-Id; SSE id + Last-Event-ID replay
Best for
Local dev, editor-spawned tools, testing
Production, team sharing, serverless, high-scale multi-client
Table 3.3: stdio versus Streamable HTTP — a decision matrix.
An analogy: stdio is a chef you hire to cook in your kitchen — instant, private, and you turn the lights on and off. Streamable HTTP is a restaurant across town — reachable by anyone with the address, always open, but you need a reservation system (sessions), a bouncer checking IDs (Origin validation and auth), and the ability to seat many parties at once.
sequenceDiagram
participant CL as Client (in Host)
participant Sub as stdio Server (subprocess)
participant HTTP as Streamable HTTP Server (remote)
Note over CL,Sub: stdio transport (local, 1:1)
CL->>Sub: spawn subprocess
CL->>Sub: JSON-RPC request via stdin
Sub-->>CL: JSON-RPC response via stdout
Note right of Sub: logs go to stderr only
CL->>Sub: stop subprocess
Note over CL,HTTP: Streamable HTTP transport (remote, multi-client)
CL->>HTTP: HTTP POST (Accept: json + event-stream)
HTTP-->>CL: 200 json OR text/event-stream (SSE)
CL->>HTTP: GET (open standalone SSE stream)
HTTP-->>CL: server-pushed messages via SSE
Note right of HTTP: MCP-Session-Id + Origin validation
For a local stdio deployment, register the server with the host and tell it how to launch the subprocess. For Claude Desktop, add an entry under mcpServers in claude_desktop_config.json (with the absolute path to the working directory and to uv), then restart the host. For remote/production, switch to Streamable HTTP and host it as a long-lived service (a PyPI package via CI/CD, or a Docker container), with production hardening: Origin validation, authentication, localhost binding when appropriate, input/path validation, rate limiting, and audit logging. A remote client connects to the endpoint URL rather than spawning a process — the same tools, resources, and prompts appear identically; only the pipe changed.
[Animation slot: side-by-side flow — left, client spawns a subprocess and pipes stdin/stdout with stderr logs; right, many clients POST/GET to one shared HTTP endpoint with SSE and session IDs]
Post-Reading Check — Transports: stdio vs HTTP
1. With the stdio transport, who owns the server's lifecycle?
The server runs independently and the client just connects
The client launches the server as a subprocess and starts/stops it
A load balancer spawns and reaps server instances
The operating system's init system manages it
2. What is the ironclad rule for a stdio server's stdout?
It may print debug logs freely; only stderr is reserved
Nothing but valid MCP messages may go to stdout; logs must go to stderr
All output must be base64-encoded before printing
stdout must be closed immediately after startup
3. Which security requirement is specific to Streamable HTTP servers?
Never write logs to stderr
MUST validate the Origin header (returning 403 if invalid) to prevent DNS-rebinding attacks
MUST run as a subprocess of the client
MUST disable JSON-RPC entirely
4. What is the memorable one-sentence rule for choosing a transport?
Always use stdio; HTTP is deprecated
If the user controls the machine the server runs on, use stdio; otherwise use Streamable HTTP
Use HTTP for local development and stdio for production
Choose whichever transport has lower latency, always stdio
5. In FastMCP, how do you switch a server between stdio and networked HTTP behavior?