Building Tools with FastMCP: Servers, Tools, Resources, Prompts, Transports

Learning Objectives

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

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

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

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

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"):

import json

@mcp.resource("weather://{city}/current")
def get_weather(city: str) -> str:
    return json.dumps({"city": city, "temperature": 22})
PatternExample URIUse
Staticresource://greetingOne fixed resource
Single path paramweather://{city}/currentParameterized family
Multiple path paramsrepos://{owner}/{repo}/infoCompound identifiers
Wildcardpath://{filepath*}Match a full path with slashes
Query paramdata://{id}{?format}Optional query-style options

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

PrimitiveDecoratorControlled bySide effects?Analogy
Tool@mcp.toolModel (with user approval)Yes — can actPOST / RPC call
Resource@mcp.resourceApplication (host decides)No — read-onlyGET endpoint
Prompt@mcp.promptUser (invokes deliberately)No — assembles a messageSlash 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

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
DimensionstdioStreamable HTTP
Deployment localityLocal subprocess on the user's machineNetworked / remote long-lived service
Lifecycle ownerClient spawns and stops the serverServer runs independently
Latency~1 ms, no network hopNetwork round-trip (usually negligible vs. LLM)
Clients per serverOne subprocess per clientMany clients on one endpoint
ScalingRun more processesHorizontal, stateless mode behind a load balancer
Auth / securityInherits local trust boundaryMUST validate Origin (403), SHOULD auth, SHOULD bind 127.0.0.1
Logging to stdoutForbidden — corrupts the streamAllowed
Sessions / resumabilityProcess-scopedMCP-Session-Id; SSE id + Last-Event-ID replay
Best forLocal dev, editor-spawned tools, testingProduction, 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?

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

Your Progress

Answer Explanations