The Complete Guide to MCP Protocol Errors in 2026 — AI Error Hub
AI Error Hub / Guides / MCP Protocol
Pillar guide · #9 of 10

The Complete Guide to MCP Protocol Errors in 2026

The definitive reference for engineers building against the Model Context Protocol after the 2026-07-28 stateless revision. Covers the client/server/host architecture and JSON-RPC wire format, both official transports (stdio and Streamable HTTP), the three primitives (tools, resources, prompts), sampling & MRTR, OAuth 2.1 authorization with PRM and CIMD, all major SDKs (Python v2, TypeScript, FastMCP), and integration patterns across Claude, OpenAI, Gemini, LangChain, and LlamaIndex. Links to every one of our 30 MCP error-fix pages.

Chapter 01

Introduction

The Model Context Protocol (MCP) turned two years old in 2026 and shipped its largest revision since launch. The 2026-07-28 specification makes the protocol core stateless — no more Mcp-Session-Id at the transport level — and adds an Extensions framework, Tasks for long-running operations, MCP Apps for server-rendered UIs, and MRTR (SEP-2322) replacing the old server-initiated elicitation/sampling flows. This is the pillar for every engineer building against MCP in 2026, whether you're writing a server, wiring a client, or debugging the wire between them.

This guide is the definitive reference for MCP as it exists today. It covers the client/server/host architecture and JSON-RPC 2.0 wire format, both official transports (stdio and Streamable HTTP), the three primitives (tools, resources, prompts) plus extended primitives (sampling, roots, elicitation via MRTR), OAuth 2.1 authorization with PRM and CIMD, all major SDKs (Python v2, TypeScript, FastMCP), and how to consume MCP tools from Claude, OpenAI, Gemini, LangChain, and LlamaIndex clients.

Who this guide is for

The guide assumes you have:

  • Built or attempted to build at least one MCP server or client
  • Hit a protocol-version, transport, or auth error that sent you searching
  • Understand why MCP exists — you want AI models to consume tools/data through a standard protocol rather than a proprietary integration per provider

If that's you, read this end-to-end once to build a full map of the protocol surface; return to specific chapters when you hit a related error. Each chapter links to specific error-fix pages that answer narrower questions with tested code and JSON-RPC message examples.

What MCP is (and isn't) in 2026

MCP is a JSON-RPC 2.0 protocol between an MCP client and an MCP server. The client runs inside a host application (Claude Desktop, an IDE, an agent framework); the server exposes tools, resources, and prompts to the client; the model uses them through the client. Think of MCP as USB for AI — a single protocol that lets any AI runtime talk to any tool builder's server without either side knowing the other's implementation details.

What MCP is not: an agent framework, a model provider, or a workflow engine. It doesn't tell you how to build agents (see the LangGraph and LlamaIndex pillars). It doesn't tell you how to call models (see the Claude, OpenAI, Gemini pillars). It's the interface layer that sits between them.

The spec revision timeline

Revision Highlights
2024-11-05 Initial release. Client/server model, JSON-RPC wire format, tools/resources/prompts primitives, stdio transport, HTTP+SSE transport.
2025-03-26 Streamable HTTP transport introduced, replacing HTTP+SSE (which stays supported).
2025-06-18 Structured tool output, elicitation, resource links in tool results. MCP servers classified as OAuth Resource Servers; RFC 8707 Resource Indicators required. JSON-RPC batching removed. MCP-Protocol-Version header required on HTTP requests.
2025-11-25 First-anniversary release. Session-based Streamable HTTP formalized with Mcp-Session-Id header.
2026-07-28 (current) Largest revision since launch. Stateless protocol core (no protocol-level session). Extensions framework. Tasks extension. MCP Apps. MRTR (SEP-2322) replacing elicitation/create + sampling/createMessage + roots/list held-open streams. Mcp-Method / Mcp-Name headers required (SEP-2243). ttlMs / cacheScope on List/Resource results (SEP-2549). CIMD replacing deprecated DCR. Formal deprecation policy.

Governance moved to the Agentic AI Foundation (AAIF) — a Linux Foundation directed fund — in December 2025. Anthropic remains the creator and largest contributor; AAIF steering includes Anthropic, Google, OpenAI, and other major model providers.

The four things that changed in 2026-07-28

1. Stateless protocol core. Dropped Mcp-Session-Id from the transport layer. Any server instance behind a load balancer can answer any request. State that used to live in session — carried across held-open streams — now either lives in your application (mint a handle from a tool and have the model pass it back) or in extensions built on top.

2. Mandatory routing headers. Streamable HTTP requests must include Mcp-Method (the JSON-RPC method name, e.g. tools/call) and Mcp-Name (the tool or resource name being invoked). Load balancers, gateways, and rate-limiters can route on these without inspecting the body. Servers reject requests where headers and body disagree.

3. MRTR replaces held-open elicitation/sampling. Model Response Task Registry (SEP-2322): server returns resultType: "input_required" with the requests it needs answered; client retries the original call with inputResponses attached. Works over the stateless protocol.

4. HTTP-style caching for List and Resource reads. ttlMs and cacheScope on tools/list, resources/list, prompts/list, and resources/read results (SEP-2549). Modeled on HTTP Cache-Control. Clients can cache tool catalogs across requests instead of re-listing on every model turn.

What's not in this guide

  • Building specific MCP servers for specific integrations — this is a reference for the protocol, not a catalog of servers
  • Agent framework specifics — see the LangGraph and LlamaIndex pillars
  • Model provider quirks — see the model pillars
  • Legacy spec revisions before 2025-11-25 in depth — covered only where relevant to migration
Spec versions for this guide

All examples reference the 2026-07-28 specification, with migration notes from 2025-11-25 (the last session-based revision) where relevant. SDK versions verified: mcp 2.x (official Python), @modelcontextprotocol/sdk current, fastmcp 1.x (Prefect HQ, both Python and TypeScript). Pin the spec revision your client and server both support — mixed-revision deployments work via version negotiation but the sharp edges live in negotiation edge cases.

Chapter 02

Architecture — hosts, clients, servers, JSON-RPC

MCP has a three-actor model: host (the application the user interacts with — Claude Desktop, an IDE, an agent framework), client (the MCP runtime inside the host that speaks the protocol), and server (a separate process, local or remote, that exposes tools/resources/prompts). Messages flow as JSON-RPC 2.0 requests, responses, and notifications. Understanding this triangle and the wire format is the foundation for every downstream chapter.

The three actors

Actor What it does Examples
Host The application the user interacts with. Owns the LLM connection. Instantiates and manages one or more clients. Claude Desktop, Cursor, VS Code with Copilot Chat, an agent framework's runtime
Client The MCP runtime inside the host. One client per server connection. Handles the JSON-RPC protocol, version negotiation, capability exchange. SDK client instance — e.g. ClientSession in Python, Client in TypeScript
Server A separate process exposing tools/resources/prompts. Can run locally (stdio subprocess) or remotely (HTTP endpoint). May invoke client-side capabilities like sampling. Filesystem server, GitHub server, Notion server, custom internal-API server

JSON-RPC 2.0 — the wire format

MCP encodes every message as JSON-RPC 2.0. UTF-8 encoded. Three message types:

  • Request — has an id, expects a response
  • Response — matches a request by id, contains either result or error
  • Notification — no id, no response expected (fire-and-forget)

Example — client calls a tool:

// Request from client to server
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "search_web",
    "arguments": {"query": "MCP 2026 spec"}
  }
}

// Response from server to client
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [
      {"type": "text", "text": "Top results: ..."}
    ]
  }
}

Standard error codes

JSON-RPC defines standard error codes; MCP extends with protocol-specific ones:

Code Name Meaning
-32700 Parse error Invalid JSON received
-32600 Invalid Request Not a valid JSON-RPC 2.0 request
-32601 Method not found Method doesn't exist or isn't available
-32602 Invalid params Invalid method parameters (e.g. tool argument validation failure)
-32603 Internal error Server implementation error
-32000 to -32099 Server error range Reserved for implementation-specific errors (auth failures, rate limits, etc.)

The initialize handshake (legacy 2025 era)

Before 2026-07-28, every connection began with an initialize handshake:

// Client → Server
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
      "sampling": {},
      "roots": {"listChanged": true}
    },
    "clientInfo": {"name": "my-app", "version": "1.0"}
  }
}

// Server → Client
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
      "tools": {"listChanged": true},
      "resources": {"subscribe": true, "listChanged": true},
      "prompts": {"listChanged": true}
    },
    "serverInfo": {"name": "my-server", "version": "1.0"}
  }
}

// Client → Server (notification, no id)
{"jsonrpc": "2.0", "method": "notifications/initialized"}

This gave client and server a chance to negotiate protocol version and capabilities before any real work. Server returned an Mcp-Session-Id that every subsequent request had to carry, pinning the client to whichever server instance handled the initialize.

Stateless 2026-07-28 — no more initialize

In the current spec, there is no handshake and no session. Every request stands alone. Protocol version and client capabilities travel in the message body as _meta.io.modelcontextprotocol/* fields on every request:

// Every request carries version and capabilities in _meta
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "search_web",
    "arguments": {"query": "..."},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {"sampling": {}}
    }
  }
}

Streamable HTTP mirrors these into HTTP headers (Mcp-Method, Mcp-Name, and version headers) so intermediaries can route without parsing the body. Body remains the source of truth; the binding defines how mismatches are rejected. See header vs body mismatch.

Capabilities — the negotiation surface

Both clients and servers declare capabilities. Only capabilities both sides support may be used. Client capabilities (what the server can request from the client):

  • sampling — server can ask client's LLM to generate completions
  • roots — server can ask client for filesystem/data boundaries
  • elicitation (deprecated in 2026-07-28) — server can request user input mid-call. Replaced by MRTR.

Server capabilities (what the client can invoke on the server):

  • tools — server exposes tools (with optional listChanged notifications)
  • resources — server exposes resources (with optional subscribe and listChanged)
  • prompts — server exposes prompts (with optional listChanged)
  • logging — server sends log messages to client
  • experimental — vendor-specific extensions

Cancellation

Every binding defines how to abandon an in-flight request:

  • stdio — client sends a notifications/cancelled notification with the target request's id
  • Streamable HTTP — client closes the request's response stream

Protocol-level rules are the same everywhere. Servers should stop work on cancellation and free resources; they may still send a response for the cancelled request, but the client should ignore it.

Why "USB for AI" is the right analogy

Before MCP, connecting a model to a tool required a proprietary integration per model provider. Each vendor invented its own function-calling schema, its own way to expose a database, its own contract for tool authors. MCP is the standardization layer that turns those N × M integrations into N + M: any MCP-compatible client can consume any MCP-compatible server. Adoption in 2026 across Anthropic, OpenAI, Google, and thousands of third-party server authors is why the protocol matters.

Chapter 03

Transports — stdio, Streamable HTTP, deprecations

A transport carries JSON-RPC messages between client and server. MCP defines two official transports: stdio for local subprocess servers (the most common and most interoperable) and Streamable HTTP for remote servers. The 2026-07-28 revision made Streamable HTTP stateless. HTTP+SSE from the original spec is deprecated but still supported by many implementations. Getting the transport right is the single most impactful choice in an MCP deployment.

stdio — the local default

The client launches the server as a subprocess. Server reads JSON-RPC messages from stdin, writes responses to stdout. Newline-delimited — one JSON-RPC message per line, messages MUST NOT contain embedded newlines.

# Python client launching an stdio server
from mcp.client.stdio import stdio_client, StdioServerParameters
from mcp import ClientSession

params = StdioServerParameters(
    command="python",
    args=["-m", "my_mcp_server"],
    env={"API_KEY": os.environ["API_KEY"]},
)

async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        # In 2026-07-28 (stateless), no initialize needed
        # In earlier revs:
        # await session.initialize()

        tools = await session.list_tools()
        result = await session.call_tool("my_tool", {"arg": "value"})

Client launches the server subprocess with environment variables and command-line args. Server inherits stdin/stdout/stderr from the client. Anything the server writes to stderr flows through to the client's stderr (useful for logs); anything the server writes to stdout that isn't valid JSON-RPC corrupts the transport. See non-JSON on stdout.

Common stdio failures:

  • Command not found — the command isn't in PATH or isn't executable. See command not found.
  • Embedded newline in message — a tool result or log line included a literal newline. Fix: escape newlines in JSON strings; never print() unstructured content directly.
  • Server logs to stdout — print() statements from your server code corrupt the JSON-RPC stream. Fix: log to stderr (sys.stderr) instead. See embedded newline.
  • Server dies on startup — check server stderr for the actual error; the client just sees a broken pipe.

Use stdio when: local integrations (filesystem, git, local databases); development and testing; single-user desktop applications like Claude Desktop or Cursor.

Streamable HTTP — the remote transport

Single endpoint (typically /mcp) supporting POST and GET. POST for client→server messages; GET opens an optional SSE stream for server→client messages when the client needs to receive server-initiated content over a long-lived connection.

# Client POST → Server (2026-07-28 stateless)
POST /mcp HTTP/1.1
Host: mcp-server.example.com
Content-Type: application/json
Accept: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search_web
Authorization: Bearer <token>

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "search_web",
    "arguments": {"query": "..."}
  }
}

Response is either JSON (immediate result) or SSE (streaming result). The client's Accept header signals which it can handle. For long-running work, the SSE stream keeps the connection open while the server emits partial results and notifications.

The mandatory Mcp-Method / Mcp-Name headers (SEP-2243)

In 2026-07-28, every Streamable HTTP request MUST include:

  • Mcp-Method — the JSON-RPC method name (tools/call, resources/read, prompts/get, etc.)
  • Mcp-Name — for methods that operate on a named entity, the tool/resource/prompt name being invoked

Load balancers, API gateways, and rate limiters can route on these headers without parsing the JSON body. Servers reject requests where Mcp-Method or Mcp-Name disagree with the body (the body is source of truth; headers must mirror it). See Mcp-Method header required.

Stateless mode — no more Mcp-Session-Id

In 2025-11-25 and earlier, calling a tool over Streamable HTTP began with an initialize request that returned an Mcp-Session-Id. Every subsequent request had to carry that session id, pinning the client to a specific server instance. This broke horizontal scaling — you couldn't sit MCP servers behind a normal load balancer.

In 2026-07-28, sessions live at the application layer, not the protocol layer. Any server instance can answer any request. If your server needs to carry state across calls, mint an explicit handle from a tool (e.g. return an opaque cursor from a paginated tool result) and have the model pass it back as an argument in the next call. The model can see the handle and thread it between tools — often better than session state hidden in the transport.

Common migration bugs:

  • Client still sending Mcp-Session-Id header — 2026-07-28 servers ignore it, but if the client waits for a session id in the response it hangs. See Mcp-Session-Id in stateless mode.
  • Server storing state in memory keyed by session id — with stateless routing, subsequent requests hit a different instance and state is gone. See server state lost.
  • Legacy server behind a stateless load balancer — session pinning breaks; either add sticky sessions at the LB or upgrade to 2026-07-28. See load balancer pinning.

HTTP+SSE — deprecated but supported

The original 2024-11-05 remote transport used two separate endpoints: POST for client→server, SSE (Server-Sent Events) for server→client. Deprecated in 2025-03-26 in favor of Streamable HTTP but still supported by many clients and servers for backward compatibility. Fine for existing deployments; don't build new ones on it. Migration path: swap the SSE server for a Streamable HTTP server — most SDKs support both concurrently during transition.

Custom transports

The spec allows custom transports — WebSocket, Unix domain sockets, message queues, whatever fits. Implementations exist for each. Trade-off: interoperability. Only stdio and Streamable HTTP are guaranteed to work across every MCP client and server. If you need to interop with Claude Desktop, ChatGPT, LlamaIndex, and LangChain simultaneously, stick to the two official transports.

Choosing between stdio and Streamable HTTP

Situation Best fit
Local integration (filesystem, git, local DB, dev tools) stdio
Desktop app (Claude Desktop, Cursor, VS Code) stdio
Multi-user hosted service, load-balanced Streamable HTTP (stateless mode)
Enterprise remote server with OAuth 2.1 auth Streamable HTTP
Serverless deployment (Lambda, Cloud Run) Streamable HTTP stateless — pairs naturally with serverless
Long-running tools with streaming intermediate results Streamable HTTP with SSE response
Development / testing / quick prototypes stdio — zero infrastructure needed
Version negotiation across mixed deployments

Not every client and server supports 2026-07-28 yet. Well-behaved SDKs probe the server on connect and negotiate down to a version both support — FastMCP does this by default. If you pin to 2026-07-28 on the client but the server only speaks 2025-11-25, you either fall back to session mode or fail depending on your negotiation setting. Explicitly pin the spec revision in production environments where both sides are under your control; leave negotiation open when consuming third-party servers of unknown vintage.

Chapter 04

Every MCP error category

MCP errors span ten categories, most tied to the protocol's surface (versions, transports, primitives, auth) rather than to underlying models. This chapter maps each category with links to specific error-fix pages.

1. Protocol version errors

Fires at: Connect (stdio) or first request (Streamable HTTP).
Common causes: Client pinned to a version the server doesn't support; missing MCP-Protocol-Version header on HTTP requests (required since 2025-06-18); legacy 2025 server behind a stateless 2026 client that doesn't negotiate down.

Well-behaved SDKs auto-negotiate. Manual pinning is a common source of production-only failures. See version mismatch and version header missing.

2. stdio transport errors

Fires at: Subprocess spawn or during message exchange.
Common causes: Command not found in PATH; server writes non-JSON to stdout (usually stray print() statements); embedded newline in message body (JSON string containing literal \n); server dies during startup and client sees broken pipe.

Chapter 3 covers stdio in depth. See non-JSON on stdout.

3. Streamable HTTP transport errors

Fires at: Request send or SSE stream.
Common causes: Missing Mcp-Method / Mcp-Name headers (required in 2026-07-28 SEP-2243); headers disagree with body (server rejects); SSE stream timeout from idle intermediary; Content-Type or Accept header mismatch.

Chapter 3 covers Streamable HTTP. See Mcp-Method header required.

4. Session vs stateless errors

Fires at: Any request after mode confusion.
Common causes: Client sending Mcp-Session-Id to a stateless 2026-07-28 server (server ignores; client hangs waiting for session); server storing state in memory keyed by session id but running behind a stateless load balancer (state lost between requests to different instances); legacy client on Streamable HTTP without sticky sessions.

Migration from session-based to stateless is the biggest 2026-07-28 upgrade risk. Chapter 3 covers the migration pattern. See state lost across requests.

5. Tool errors

Fires at: tools/list, tools/call, or tool execution.
Common causes: Invalid JSON Schema in inputSchema; argument validation failure at tools/call (client sent wrong types); structured tool output content block format wrong; tool raising an exception the server doesn't wrap into a proper error response.

Chapter 5 covers tools. See tool schema invalid.

6. Resource errors

Fires at: resources/list, resources/templates/list, or resources/read.
Common causes: Resource URI template parameter not bound (e.g. echo://{message} called without providing message); MIME type in response doesn't match the actual content; server ignoring ttlMs / cacheScope hints (client re-reads resources on every turn).

Chapter 6 covers resources. See URI template parameter.

7. Prompt errors

Fires at: prompts/get.
Common causes: Required argument not provided by client; prompt messages contain an invalid role (only user, assistant, system are valid); prompt template references an undefined variable.

Chapter 7 covers prompts. See prompt required argument.

8. Sampling, elicitation, MRTR errors

Fires at: sampling/createMessage, legacy elicitation/create, or MRTR response handling.
Common causes: Server calling sampling/createMessage against a client that doesn't declare the sampling capability; legacy elicitation/create in a 2026-07-28 environment where it's deprecated in favor of MRTR; malformed MRTR input_required response shape.

Chapter 8 covers all three. See elicitation deprecated → MRTR.

9. Authorization errors

Fires at: HTTP requests to authenticated servers.
Common causes: 401 response missing WWW-Authenticate header (client can't discover auth server); PRM (Protected Resource Metadata) endpoint returning wrong shape; DCR (Dynamic Client Registration) deprecated but still called instead of CIMD; PKCE code_challenge_method mismatch; missing RFC 8707 resource parameter in token request.

Chapter 9 covers auth. See WWW-Authenticate missing.

10. SDK-specific errors

Fires at: Import, build, or runtime.
Common causes: Python SDK v1 → v2 migration import path changes; TypeScript SDK Zod schema inference failure on complex types; FastMCP package confusion (punkpeye's fastmcp vs Prefect HQ's fastmcp — same name, different projects, different capabilities).

Chapter 10 covers SDKs. See FastMCP package confusion.

Chapter 05

Tools — schema, structured output, annotations

Tools are the primary primitive most MCP servers expose. A tool has a name, description, input schema (JSON Schema), and — since 2025-06-18 — optionally a structured output schema. The model picks tools to call based on their descriptions; the client validates arguments against the input schema before invoking; the server returns content blocks or structured data. Getting the schema right is the single highest-leverage thing a tool author does.

The tool lifecycle

  1. Discovery — client calls tools/list, server returns the tool catalog
  2. Selection — model chooses which tool to call based on descriptions
  3. Invocation — client calls tools/call with name and arguments
  4. Execution — server validates arguments, runs the tool, returns result
  5. Result — client feeds the result back to the model as tool output

Defining a tool with the Python SDK

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-server")

@mcp.tool()
def get_weather(city: str, units: str = "celsius") -> str:
    '''Get current weather for a city.

    Args:
        city: City name (e.g. "London", "Tokyo")
        units: Temperature units — "celsius" or "fahrenheit"
    '''
    return fetch_weather(city, units)

FastMCP derives the input schema from the function signature via Pydantic. Description from the docstring. Type hints become the JSON Schema types. Sensible defaults for 90% of tools.

What tools/list returns

// Client → Server
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}

// Server → Client
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "tools": [
      {
        "name": "get_weather",
        "description": "Get current weather for a city.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "city": {"type": "string"},
            "units": {"type": "string", "default": "celsius"}
          },
          "required": ["city"]
        }
      }
    ],
    "_meta": {
      "io.modelcontextprotocol/ttlMs": 300000,
      "io.modelcontextprotocol/cacheScope": "private"
    }
  }
}

Since 2026-07-28 (SEP-2549), tools/list results carry ttlMs and cacheScope. Clients that honor these can cache the tool catalog for the specified duration instead of re-listing on every model turn. Big savings on high-traffic remote servers. See ttlMs / cacheScope not honored.

tools/call — the argument validation gate

// Client → Server
{
  "jsonrpc": "2.0", "id": 42,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {"city": "London", "units": "celsius"}
  }
}

// Server validates arguments against inputSchema, then runs the tool
// Success:
{
  "jsonrpc": "2.0", "id": 42,
  "result": {
    "content": [
      {"type": "text", "text": "London: 14°C, cloudy"}
    ]
  }
}

// Validation failure (invalid params):
{
  "jsonrpc": "2.0", "id": 42,
  "error": {
    "code": -32602,
    "message": "Invalid arguments: 'city' is required"
  }
}

Common bugs at this stage:

  • Model sending arguments with wrong types (string instead of int) — server should validate and return -32602
  • Optional arguments not marked as such in schema — model refuses to call without them
  • Schema references ($ref) that the client's validator can't resolve
  • Enum values in the schema not matching what the model was told about

See argument validation failed.

Content blocks — how tool results reach the model

Tool results are lists of content blocks. Four block types:

Type Shape Use case
text {"type": "text", "text": "..."} Standard textual output
image {"type": "image", "data": "<base64>", "mimeType": "image/png"} Screenshots, charts, generated images
audio {"type": "audio", "data": "<base64>", "mimeType": "audio/wav"} TTS output, recordings
resource {"type": "resource", "resource": {"uri": "...", ...}} Link to an MCP resource the model can then read (added 2025-06-18)

A tool can return multiple content blocks in one result — e.g. text describing what it found plus an image showing the result plus a resource link to the raw data. Client decides which blocks the model sees based on its capabilities.

Structured tool output (2025-06-18+)

In addition to content blocks (unstructured, for the model), tools can return structured output — typed data for the client's application logic:

@mcp.tool()
def get_weather(city: str) -> dict:
    '''Get current weather.'''
    return {
        "structuredContent": {
            "city": city,
            "temperature_c": 14,
            "condition": "cloudy",
            "wind_kph": 12,
        },
        "content": [
            {"type": "text", "text": f"{city}: 14°C, cloudy"}
        ]
    }

structuredContent is typed data your application can pass to a UI, log system, or downstream code. content is what the model sees. Ship both when the caller needs both. Structured output should be described by a JSON Schema in the tool's outputSchema field so clients can validate. See structured output format error.

Tool annotations — behavior hints for clients

Since 2025-06-18, tools can declare annotations that hint at their behavior:

@mcp.tool(annotations={
    "title": "Send Email",
    "readOnlyHint": False,
    "destructiveHint": True,    # side effects the user might regret
    "idempotentHint": False,
    "openWorldHint": True,      # touches external systems
})
def send_email(to: str, subject: str, body: str) -> str:
    '''Send an email.'''
    ...

Clients can use annotations to decide whether to prompt the user for approval before invoking (destructive tools), how aggressively to cache results (idempotent tools), and how to render the tool in a UI (title). Annotations are hints, not enforced; server behavior is source of truth.

Notifications — tools/list_changed

A server that adds or removes tools at runtime can notify clients:

{"jsonrpc": "2.0", "method": "notifications/tools/list_changed"}

Client responds by re-fetching tools/list. Requires the server to declare listChanged: true in its tools capability. Uncommon in practice — most servers have a static tool catalog — but useful for servers that discover tools dynamically (plugin systems, per-tenant tool sets).

Tool descriptions are prompts

The model chooses tools based on their descriptions. Vague or misleading descriptions lead to wrong tool selection. Treat descriptions as prompt engineering: state clearly what the tool does, what arguments it takes, what it returns, and — critically — when to use it vs when to use an adjacent tool. "Search the web" is worse than "Search the public web for recent news articles from mainstream sources; do not use for internal company docs (use search_docs instead)."

Chapter 06

Resources & resource templates

Resources are read-only data addressable by URI. Where tools are function calls (the model decides when to invoke), resources are attachments the client picks up and passes to the model. Templates parameterize URIs so a single server exposes a family of resources without listing millions of them. In 2026-07-28, resources gained HTTP-style cache semantics via ttlMs and cacheScope.

The resource lifecycle

  1. Static discovery — client calls resources/list for concrete resources
  2. Template discovery — client calls resources/templates/list for parameterized URIs
  3. Selection — client (or user) chooses resources to include as context
  4. Read — client calls resources/read with the URI
  5. Context — resource content is passed to the model as part of the conversation

Defining a resource

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("company-docs")

@mcp.resource("docs://policies/refund")
def refund_policy() -> str:
    '''The company refund policy document.'''
    return open("./policies/refund.md").read()

URI is opaque to MCP — you pick the scheme. Conventions: file:// for filesystem, docs://, db://, http(s):// for URLs, custom schemes for domain resources.

Resource templates — parameterized URIs

@mcp.resource("docs://policies/{name}")
def policy(name: str) -> str:
    '''Get a policy document by name.'''
    path = f"./policies/{name}.md"
    if not os.path.exists(path):
        raise ValueError(f"No policy named {name}")
    return open(path).read()

Templates follow RFC 6570 URI Template syntax. {name} is a template variable; the client binds it at read time. Server declares the template shape; client provides values.

// Client → Server
{
  "jsonrpc": "2.0", "id": 5,
  "method": "resources/templates/list"
}

// Server → Client
{
  "jsonrpc": "2.0", "id": 5,
  "result": {
    "resourceTemplates": [
      {
        "uriTemplate": "docs://policies/{name}",
        "name": "Company policy",
        "description": "Company policy documents by name (refund, privacy, etc.)",
        "mimeType": "text/markdown"
      }
    ]
  }
}

// Client → Server (read a specific instance)
{
  "jsonrpc": "2.0", "id": 6,
  "method": "resources/read",
  "params": {"uri": "docs://policies/refund"}
}

Common bug: client calling resources/read with an unbound template URI (docs://policies/{name} instead of docs://policies/refund). Server returns a resource-not-found error. See URI template parameter not bound.

The resources/read response shape

// Server → Client
{
  "jsonrpc": "2.0", "id": 6,
  "result": {
    "contents": [
      {
        "uri": "docs://policies/refund",
        "mimeType": "text/markdown",
        "text": "# Refund Policy\n\n..."
      }
    ],
    "_meta": {
      "io.modelcontextprotocol/ttlMs": 3600000,
      "io.modelcontextprotocol/cacheScope": "shared"
    }
  }
}

contents is a list — a single resource read can return multiple content items (e.g. reading a directory returns entries for each file). Each has:

  • uri — the resolved URI (template variables bound)
  • mimeType — the content type; must match the actual content or clients that respect MIME fail
  • text for text content, or blob (base64) for binary

Common bug: server says mimeType: "application/json" but returns Markdown in the text field. Client renders wrongly or fails validation. See MIME type mismatch.

ttlMs and cacheScope (SEP-2549)

New in 2026-07-28, modeled on HTTP Cache-Control:

Field Semantic
ttlMs Milliseconds the client may cache this result before re-fetching. 0 means don't cache.
cacheScope: "shared" Can be cached in shared caches (proxies, CDNs); safe to serve to any user.
cacheScope: "private" Client-local cache only; contains user-specific or sensitive data.
cacheScope: "no-store" Do not cache anywhere. Regenerate every read.

Server sets these based on the nature of the resource. Client SDKs that respect them (all Tier-1 SDKs do in 2026) cache aggressively — big wins on high-traffic remote servers where the tool/resource catalog changes slowly.

Subscriptions — resources that change

If the server declares resources: {"subscribe": true}, clients can subscribe to individual resource URIs:

// Client → Server
{
  "jsonrpc": "2.0", "id": 7,
  "method": "resources/subscribe",
  "params": {"uri": "docs://policies/refund"}
}

// Server → Client (notification when resource changes)
{
  "jsonrpc": "2.0",
  "method": "notifications/resources/updated",
  "params": {"uri": "docs://policies/refund"}
}

// Client responds by re-reading the resource

Useful for live-updating content (a dashboard, a monitored file). Requires the transport to support server-to-client messages (stdio always; Streamable HTTP via SSE stream).

When to use resources vs tools

Situation Best fit
Read-only data the user or client picks up as context Resource
Action with side effects or arguments the model chooses Tool
Content addressable by a URI the user might paste Resource
Function the model needs to invoke mid-conversation Tool
Large corpus indexable by parameter — million-URI space Resource template
Small enumerable set of things (10-100) Individual resources listed via resources/list
Chapter 07

Prompts — parameterized system messages

Prompts are the least-used MCP primitive, and the most misunderstood. They're not the system prompt the client uses to steer the model — they're parameterized message templates the server exposes so users can trigger workflows the server author designed. Think slash commands in Claude Desktop or Cursor: /analyze-code, /review-pr, /summarize-meeting. The server ships the template; the user picks it; the client fills the arguments and injects the messages into the conversation.

Defining a prompt

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base

mcp = FastMCP("code-review")

@mcp.prompt()
def review_code(code: str, language: str = "python") -> list[base.Message]:
    '''Review a code snippet for bugs and style issues.'''
    return [
        base.UserMessage(f"Review this {language} code:\n\n```{language}\n{code}\n```"),
        base.UserMessage("Focus on: correctness, edge cases, and Pythonic style."),
    ]

The prompt function returns a list of messages. Client injects these into the conversation when the user selects the prompt. Server owns the template; user provides the arguments; client does the substitution and injection.

prompts/list — discovering prompts

// Client → Server
{"jsonrpc": "2.0", "id": 1, "method": "prompts/list"}

// Server → Client
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "prompts": [
      {
        "name": "review_code",
        "description": "Review a code snippet for bugs and style issues.",
        "arguments": [
          {"name": "code", "description": "The code to review", "required": true},
          {"name": "language", "description": "Programming language", "required": false}
        ]
      }
    ]
  }
}

prompts/get — invoking a prompt

// Client → Server
{
  "jsonrpc": "2.0", "id": 2,
  "method": "prompts/get",
  "params": {
    "name": "review_code",
    "arguments": {"code": "def add(a, b): return a+b", "language": "python"}
  }
}

// Server → Client
{
  "jsonrpc": "2.0", "id": 2,
  "result": {
    "description": "Review a code snippet for bugs and style issues.",
    "messages": [
      {
        "role": "user",
        "content": {"type": "text", "text": "Review this python code:\n\n```python\ndef add(a, b): return a+b\n```"}
      },
      {
        "role": "user",
        "content": {"type": "text", "text": "Focus on: correctness, edge cases, and Pythonic style."}
      }
    ]
  }
}

Common bugs:

  • Required argument missing — client calls prompts/get without providing an argument marked required: true. Server should return -32602. See required argument not provided.
  • Invalid role — only user, assistant, and system are valid. Custom roles are rejected. See invalid role.
  • Prompt returning code blocks with wrong format — the model sees whatever text you put in; be intentional about markdown fencing, whitespace, and message boundaries.

Prompts vs system prompts vs tool descriptions

Three related concepts, often confused:

Concept Who defines it When it's used
MCP prompt Server author User selects (like a slash command); client injects the templated messages
System prompt Host / client author Set at conversation start to steer the model's overall behavior
Tool description Server author (per tool) Model sees it while deciding which tool to invoke; not a message

Prompts are workflow scaffolding for humans (or agents mimicking human intent). Not every server needs them. If your server is model-consumed (agents calling tools), prompts are usually skippable. If it's human-consumed via a UI with slash-command-like affordances, prompts are the right primitive.

Argument completion — helping the user fill prompt params

Servers can implement completion/complete to suggest argument values as the user types:

// Client → Server (user is typing an argument value)
{
  "jsonrpc": "2.0", "id": 3,
  "method": "completion/complete",
  "params": {
    "ref": {"type": "ref/prompt", "name": "review_code"},
    "argument": {"name": "language", "value": "pyt"}
  }
}

// Server → Client
{
  "jsonrpc": "2.0", "id": 3,
  "result": {
    "completion": {
      "values": ["python"],
      "total": 1,
      "hasMore": false
    }
  }
}

Optional. Nice for constrained-value arguments (language enums, tenant IDs, resource types). Skip for open-ended text arguments.

When to reach for prompts

If your MCP server ships with a UI in mind — Claude Desktop, Cursor, or a custom host — and you want to give users quick access to canonical workflows, prompts are the right primitive. If your server is going to be consumed by agents that make their own decisions about what to do next, prompts are usually the wrong abstraction — expose tools with rich descriptions instead and let the agent orchestrate.

Chapter 08

Sampling, elicitation & MRTR

Most MCP traffic flows client → server (list tools, call a tool). Three primitives flow the other way — server needs something from the client. Sampling asks the client's LLM to generate a completion. Roots asks the client for filesystem/data boundaries. Elicitation asks the user for input mid-call. All three used to require held-open streams. In 2026-07-28, MRTR replaces them for the stateless world.

Sampling — server asks client's LLM to complete something

Some server-side tools need LLM reasoning that the server doesn't want to (or can't) do itself. Sampling lets the server ask the client's LLM. Example: a research server that finds sources and asks the client's LLM to synthesize them.

// Server → Client (over a held-open stream in legacy mode; via MRTR in 2026-07-28)
{
  "jsonrpc": "2.0", "id": 100,
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {"role": "user", "content": {"type": "text", "text": "Summarize these sources: ..."}}
    ],
    "maxTokens": 500,
    "modelPreferences": {
      "hints": [{"name": "claude-sonnet-4-6"}],
      "costPriority": 0.3,
      "speedPriority": 0.4,
      "intelligencePriority": 0.9
    }
  }
}

// Client (if it supports sampling) → Server
{
  "jsonrpc": "2.0", "id": 100,
  "result": {
    "role": "assistant",
    "content": {"type": "text", "text": "Summary: ..."},
    "model": "claude-sonnet-4-6",
    "stopReason": "endTurn"
  }
}

Client capability flag: sampling: {} on initialize (legacy) or in _meta (stateless). If the client didn't declare sampling, the server calling sampling/createMessage gets a -32601 method-not-found or capability-not-supported error. See client does not support sampling.

modelPreferences lets the server hint at what kind of model it needs. Client decides which model to actually use — the server is a guest.

Roots — server asks for filesystem/data boundaries

Filesystem servers, code analysis servers, and similar tools need to know what directories the user has authorized them to access. roots/list asks the client:

// Server → Client
{"jsonrpc": "2.0", "id": 200, "method": "roots/list"}

// Client → Server
{
  "jsonrpc": "2.0", "id": 200,
  "result": {
    "roots": [
      {"uri": "file:///Users/sara/projects/my-app", "name": "my-app"},
      {"uri": "file:///Users/sara/notes", "name": "notes"}
    ]
  }
}

Server treats roots as authorized boundaries — MUST NOT access paths outside them. Client capability: roots: {"listChanged": true} means the client will also send notifications/roots/list_changed when the user modifies roots (added a project, removed one).

Elicitation — server asks the user for input mid-call (legacy)

A tool that needs additional info from the user — a confirmation, a missing parameter, a disambiguation — used to interrupt mid-call:

// Legacy 2025-11-25 and earlier
// Server → Client (in the middle of handling a tools/call)
{
  "jsonrpc": "2.0", "id": 300,
  "method": "elicitation/create",
  "params": {
    "message": "You're about to delete 42 files. Confirm?",
    "requestedSchema": {
      "type": "object",
      "properties": {"confirm": {"type": "boolean"}},
      "required": ["confirm"]
    }
  }
}

// Client shows the prompt to the user, sends response
{
  "jsonrpc": "2.0", "id": 300,
  "result": {"action": "accept", "content": {"confirm": true}}
}

Worked over the held-open Streamable HTTP stream. In stateless 2026-07-28, there is no held-open stream — elicitation/create is deprecated. Use MRTR instead. See elicitation deprecated.

MRTR — Model Response Task Registry (SEP-2322)

The 2026-07-28 replacement for elicitation/sampling/roots that need to happen mid-tool-call. Instead of the server pausing to ask the client something, the server returns a special result indicating it needs input, and the client re-invokes the original request with the inputs attached.

// Client → Server (initial tool call)
{
  "jsonrpc": "2.0", "id": 42,
  "method": "tools/call",
  "params": {
    "name": "delete_files",
    "arguments": {"pattern": "*.tmp"}
  }
}

// Server → Client (needs input to proceed)
{
  "jsonrpc": "2.0", "id": 42,
  "result": {
    "resultType": "input_required",
    "requests": [
      {
        "id": "confirm-delete",
        "type": "elicitation",
        "message": "You're about to delete 42 files. Confirm?",
        "requestedSchema": {
          "type": "object",
          "properties": {"confirm": {"type": "boolean"}},
          "required": ["confirm"]
        }
      }
    ]
  }
}

// Client re-invokes with inputResponses attached
{
  "jsonrpc": "2.0", "id": 43,
  "method": "tools/call",
  "params": {
    "name": "delete_files",
    "arguments": {"pattern": "*.tmp"},
    "inputResponses": [
      {"id": "confirm-delete", "content": {"confirm": true}}
    ]
  }
}

// Server → Client (now proceeds and returns the actual result)
{
  "jsonrpc": "2.0", "id": 43,
  "result": {
    "content": [{"type": "text", "text": "Deleted 42 files"}]
  }
}

MRTR works over the stateless protocol because there's no held-open stream — every message is a normal request/response cycle. Server maintains no state between the first response and the retry; the retry carries everything the server needs.

MRTR handles all three legacy server-initiated flows:

  • type: "elicitation" — need input from the user
  • type: "sampling" — need an LLM completion from the client
  • type: "roots" — need filesystem/data boundaries

Common bug: server response shape wrong (missing resultType, malformed requests array, or IDs not stable across retry). Client can't parse it. See MRTR response format.

The stateless migration and mid-call interactivity

MRTR is elegant on paper but adds latency — every input requirement costs a client → server → client → server round trip. In practice, well-designed tools batch input requirements (ask for confirmation + missing parameter + boundary at once, not one at a time) or push interactivity to the tool selection layer (let the model decide before calling). If you're finding yourself needing multiple MRTR round trips per tool call, the tool is probably doing too much — split it.

Chapter 09

Authorization — OAuth 2.1, PRM, CIMD, PKCE

Local stdio servers don't need auth — the client is a subprocess you control, and env vars carry secrets. Remote MCP servers are a different world: the 2025-06-18 revision classified them as OAuth Resource Servers and made RFC 8707 Resource Indicators required. The 2026-07-28 revision hardened this further with formal PRM discovery, CIMD replacing deprecated DCR, and clearer PKCE requirements. This chapter walks the full OAuth 2.1 flow as MCP applies it.

The auth model at a glance

Three actors:

  • MCP Server (Resource Server) — the thing being called; hosts the tools/resources/prompts
  • MCP Client — the runtime inside the host that needs to call the server
  • Authorization Server — separate service that issues tokens; can be shared with other resource servers

MCP client obtains a token from the authorization server; MCP server validates the token; requests flow with Authorization: Bearer <token>. Standard OAuth 2.1. What makes MCP interesting is how the client discovers which authorization server to use.

Step 1 — Unauthenticated request, 401 with WWW-Authenticate

// Client attempts request without credentials
POST /mcp HTTP/1.1
Host: mcp-server.example.com
Content-Type: application/json
Mcp-Method: tools/list

// Server responds 401 with WWW-Authenticate pointing to PRM
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp-server.example.com/.well-known/oauth-protected-resource"

The WWW-Authenticate header MUST include a resource_metadata parameter pointing to the server's Protected Resource Metadata endpoint. Without this header (or with the parameter missing), the client can't discover where to authenticate. See WWW-Authenticate header missing.

Step 2 — Fetch Protected Resource Metadata (PRM)

GET /.well-known/oauth-protected-resource HTTP/1.1
Host: mcp-server.example.com

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "resource": "https://mcp-server.example.com/mcp",
  "authorization_servers": [
    "https://auth.example.com"
  ],
  "scopes_supported": ["mcp:tools:read", "mcp:tools:call"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://mcp-server.example.com/docs"
}

PRM tells the client: this resource lives here, use these authorization servers, these scopes are available. RFC 9728. Common failure: PRM endpoint returns wrong shape or wrong content-type. See PRM discovery error.

Step 3 — Fetch authorization server metadata

GET /.well-known/oauth-authorization-server HTTP/1.1
Host: auth.example.com

// Response — standard OAuth 2.0 authorization server metadata (RFC 8414)
{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/oauth/authorize",
  "token_endpoint": "https://auth.example.com/oauth/token",
  "registration_endpoint": "https://auth.example.com/oauth/register",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  ...
}

Step 4 — Client registration (CIMD, replacing deprecated DCR)

Historically, MCP clients registered dynamically via Dynamic Client Registration (RFC 7591): POST to the registration_endpoint with metadata, get back a client_id (and optionally client_secret). This worked but was awkward — every MCP client host had to register anew per authorization server, and the client metadata (name, URIs) came from the client at registration time with no verification.

In 2026-07-28, DCR is formally deprecated in favor of Client-Initiated Metadata Discovery (CIMD): the client presents metadata about itself at token request time via a signed document, and the authorization server validates the metadata against a known source (well-known URI at the client's domain).

Migration reality: DCR still works for backward compatibility. Most MCP clients and authorization servers in 2026 support both. New auth server implementations should ship with CIMD; new clients should prefer CIMD but fall back to DCR when the server doesn't advertise CIMD support. See CIMD required.

Step 5 — Authorization code + PKCE

MCP requires PKCE (RFC 7636) with the S256 method — no plain-text challenge, no skipping PKCE for confidential clients:

# Client generates verifier and challenge
verifier = urlsafe_b64_random(64)   # cryptographically random
challenge = urlsafe_b64(sha256(verifier))

# Redirect user to authorization endpoint
GET /oauth/authorize?
  response_type=code
  &client_id=my-mcp-client
  &redirect_uri=http://localhost:8765/callback
  &scope=mcp:tools:call
  &state=<csrf_random>
  &code_challenge={challenge}
  &code_challenge_method=S256
  &resource=https://mcp-server.example.com/mcp   # ← RFC 8707 required

# User authorizes; authorization server redirects with code
GET /callback?code=<auth_code>&state=<csrf_random>

# Client exchanges code for token, sending verifier
POST /oauth/token
  grant_type=authorization_code
  &code=<auth_code>
  &redirect_uri=http://localhost:8765/callback
  &client_id=my-mcp-client
  &code_verifier={verifier}
  &resource=https://mcp-server.example.com/mcp   # ← RFC 8707 required

Common bugs:

  • code_challenge_method mismatch — client uses plain where server requires S256. See PKCE code_challenge_method.
  • Missing resource parameter — RFC 8707 Resource Indicators are required by MCP servers; omitting the resource parameter yields tokens the MCP server refuses
  • Verifier not stored across the redirect — user round-trips to the auth server, client has to remember which verifier goes with which state; storing verifier in state cookie is common
  • State parameter not validated — CSRF vulnerability

Step 6 — Authenticated requests

POST /mcp HTTP/1.1
Host: mcp-server.example.com
Authorization: Bearer <access_token>
Mcp-Method: tools/call
Mcp-Name: search_web
Content-Type: application/json

{"jsonrpc": "2.0", "id": 42, "method": "tools/call", "params": {...}}

Server validates the bearer token (typically via introspection or JWT signature verification), checks that the token's audience matches this resource server's URI, checks scopes cover the requested method. Reject with 401 if invalid; 403 if scopes insufficient.

Token refresh and expiration

Access tokens should be short-lived (an hour or less). Refresh tokens optionally issued at initial token grant, exchanged at the token endpoint for a new access token. When a refresh token is issued alongside an access token, the client stores it securely (encrypted, per-user); refresh happens transparently without user interaction.

Alternative auth methods

OAuth 2.1 is the standard, but MCP servers may accept:

  • API keys — Authorization: Bearer <api_key> or custom header. Simpler; less rotatable; fine for machine-to-machine.
  • mTLS — mutual TLS. Client certificate identifies the client. Common in enterprise settings.
  • IAM SigV4 — AWS-flavored signing. Used by AWS-hosted MCP servers.
  • None (stdio) — local subprocess; env vars carry secrets

Servers should document which auth methods they accept via PRM's bearer_methods_supported field (for OAuth) plus custom documentation for non-OAuth methods.

Confused deputies and RFC 8707

The resource parameter (RFC 8707 Resource Indicators) matters. It binds the token to a specific MCP server. Without it, tokens issued for one MCP server could be replayed against another — the classic "confused deputy" attack. This is why MCP 2025-06-18 made it required. Any auth server that ignores the resource parameter, or any MCP server that doesn't verify the token's audience against its own resource URI, breaks this protection.

Chapter 10

SDKs — Python v2, TypeScript, FastMCP

Three SDK families dominate MCP development in 2026: the official Python SDK (v2, released to support 2026-07-28), the official TypeScript SDK (Tier 1, 66M+ npm downloads), and FastMCP (higher-level framework, now maintained by Prefect HQ with both Python and TypeScript editions). Naming is a minefield — there are two unrelated projects called "fastmcp" that people confuse constantly. This chapter walks the landscape.

Official Python SDK v2

Current stable line. Major rework of v1 both to support the 2026-07-28 stateless spec and to fix long-standing architectural issues. Ships with two APIs:

  • Low-level SDK — direct protocol handling. Maximum control. More boilerplate.
  • FastMCP high-level API — decorator-based, built on top of the low-level SDK. FastMCP 1.0 was incorporated into the official SDK in 2024; the high-level API lives at mcp.server.fastmcp.
# Install
pip install "mcp>=2.0"

# Simple server via the high-level FastMCP API
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("echo")

@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
    '''Echo a message as a resource.'''
    return f"Resource echo: {message}"

@mcp.tool()
def echo_tool(message: str) -> str:
    '''Echo a message as a tool.'''
    return f"Tool echo: {message}"

@mcp.prompt()
def echo_prompt(message: str) -> str:
    '''Create an echo prompt.'''
    return f"Please process this message: {message}"

if __name__ == "__main__":
    mcp.run()   # stdio by default; supports --transport http

v1 → v2 migration

pip install mcp now installs 2.x. If you have v1 code and aren't ready to migrate, pin with an upper bound: mcp>=1.28,<2. The v1.x branch continues to receive critical bug fixes and security patches. The v2 migration guide covers every breaking change; the big ones:

  • Import paths reorganized — mcp.server.fastmcp replaces some legacy paths
  • ClientSession API surface changed — some methods renamed for consistency
  • 2026-07-28 stateless mode is the default — session-based mode is opt-in

See v1 → v2 import migration.

Client-side Python usage

from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters

params = StdioServerParameters(command="python", args=["-m", "my_server"])

async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        # 2026-07-28: no initialize needed
        # Earlier revs: await session.initialize()

        tools = await session.list_tools()
        resources = await session.list_resources()
        prompts = await session.list_prompts()

        result = await session.call_tool("my_tool", {"arg": "value"})
        content = await session.read_resource("docs://policies/refund")
        prompt = await session.get_prompt("review_code", {"code": "..."})

Official TypeScript SDK

Tier 1 official SDK. Most feature-complete, best-supported, largest ecosystem (66M+ npm downloads, 27,000+ dependents). Slightly more boilerplate than FastMCP but maximum control.

// Install
// npm install @modelcontextprotocol/sdk zod

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "greet-server",
  version: "1.0.0",
});

server.tool(
  "greet",
  { name: z.string() },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}!` }],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

Zod schemas infer JSON Schema automatically. Any Standard-Schema-compatible library works — Zod, Valibot, ArkType. Common bug: Zod schema uses features that don't cleanly infer to JSON Schema (complex refinements, transformations). See Zod schema inference error.

FastMCP (Prefect HQ) — Python and TypeScript

Higher-level framework than the official SDK. Same three primitives (tools, resources, prompts) with cleaner ergonomics for common patterns. Ships with:

  • Automatic schema inference from Python type hints or TypeScript types
  • First-class OAuth 2.1 helpers
  • Both legacy (2025-11-25) and modern (2026-07-28) protocol generations, negotiated per-connection
  • A CLI (fastmcp run, fastmcp inspect, fastmcp call, fastmcp dev)
  • Deployment helpers
# Python — Prefect HQ FastMCP
# pip install fastmcp

from fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool
def add(a: int, b: int) -> int:
    return a + b

if __name__ == "__main__":
    mcp.run()
// TypeScript — Prefect HQ FastMCP
// npm install fastmcp zod

import { FastMCP } from "fastmcp";
import { z } from "zod";

const server = new FastMCP({ name: "my-server", version: "1.0.0" });

server.addTool({
  name: "add",
  parameters: z.object({ a: z.number(), b: z.number() }),
  execute: async ({ a, b }) => String(a + b),
});

server.start();

The FastMCP naming trap

There are two distinct npm/PyPI packages named fastmcp:

Package Maintainer Supports 2026-07-28? Notes
fastmcp (npm, PyPI) — the Prefect HQ version Prefect HQ / official Yes (both legacy and modern) The recommended framework in 2026. Both Python and TypeScript editions.
fastmcp (npm) — punkpeye's version @punkpeye (community) No — only supports 2025-11-25 and earlier Preceded the Prefect HQ edition. Only TypeScript. Being superseded.

Symptom of the mix-up: you install fastmcp expecting 2026-07-28 support, get the punkpeye version, and your stateless client can't handshake. Fix: verify the maintainer via npm info fastmcp or pip show fastmcp. Prefect HQ's version is the one to use in 2026. See FastMCP package confusion.

Other frameworks

Framework Language When to use
mcp-framework TypeScript Third-party CLI-oriented framework, 3.3M+ downloads. Alternative to FastMCP for TS teams.
FastAPI-MCP Python Wraps existing FastAPI routes as MCP tools. Great if you have FastAPI plumbing already.
ViteMCP TypeScript New framework targeting the stateless 2026-07-28 spec only. Cleaner but younger ecosystem.
SDKs in other languages Go, Rust, Kotlin, Swift, C#, etc. Community-maintained. Coverage varies by feature and spec revision — check individual repos.

MCP Inspector — the debugging tool

# Launch the Inspector against your server
npx @modelcontextprotocol/inspector node ./my-server.js

# Or for Python
npx @modelcontextprotocol/inspector python -m my_server

Web UI that connects to your MCP server, browses tools/resources/prompts, invokes them interactively, shows the raw JSON-RPC traffic. Essential during development. Also great for verifying that a third-party MCP server behaves the way you expect before wiring it into an agent.

Chapter 11

Client integration across frameworks

MCP servers only matter if clients can call them. In 2026, every major model provider and agent framework consumes MCP: Anthropic (Claude via API and Claude Desktop), OpenAI (Responses API, ChatGPT), Google (Gemini API, AI Studio), LangChain/LangGraph (via langchain-mcp-adapters), LlamaIndex (via BasicMCPClient). This chapter walks each integration pattern.

Claude via Anthropic API

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    mcp_servers=[
        {
            "type": "url",
            "url": "https://mcp.notion.com/mcp",
            "name": "notion",
            "authorization_token": os.environ["NOTION_MCP_TOKEN"],
        },
    ],
    messages=[{"role": "user", "content": "Find my meeting notes for today"}],
    betas=["mcp-client-2025-04-04"],
)

Anthropic API's MCP connector: pass a list of MCP server URLs; Anthropic's servers handle the client side. Currently HTTP+SSE and Streamable HTTP servers supported; stdio servers not (Anthropic's infrastructure can't spawn subprocesses in your local environment). See the Claude pillar for details.

Claude Desktop — stdio-first

// ~/Library/Application Support/Claude/claude_desktop_config.json  (macOS)
// %APPDATA%\Claude\claude_desktop_config.json  (Windows)

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/sara/projects"]
    },
    "notion": {
      "command": "npx",
      "args": ["-y", "@notionhq/notion-mcp-server"],
      "env": {"NOTION_API_KEY": "secret_..."}
    }
  }
}

Desktop config file declares stdio server subprocesses. Claude Desktop spawns each on launch. Anything printable to the config file becomes accessible to the model. Common issue: config file syntax error breaks all MCP servers silently — validate JSON before restarting.

OpenAI Responses API

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Find my recent GitHub issues",
    tools=[
        {
            "type": "mcp",
            "server_url": "https://mcp.github.com/mcp",
            "server_label": "github",
            "require_approval": "never",
            "authorization": os.environ["GITHUB_MCP_TOKEN"],
        }
    ],
)

OpenAI's Responses API added MCP tool support in 2025. Similar pattern to Anthropic — declare MCP servers, OpenAI's infrastructure calls them. See the OpenAI pillar.

Google Gemini API

Gemini added MCP tool support via the same tools array pattern — declare MCP servers alongside function tools. See the Gemini pillar.

LangChain / LangGraph via langchain-mcp-adapters

# pip install langchain-mcp-adapters

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic

client = MultiServerMCPClient({
    "filesystem": {
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
        "transport": "stdio",
    },
    "github": {
        "url": "https://mcp.github.com/mcp",
        "transport": "streamable_http",
    },
})

tools = await client.get_tools()

agent = create_agent(
    model=ChatAnthropic(model="claude-sonnet-4-6"),
    tools=tools,
)

MultiServerMCPClient connects to multiple MCP servers simultaneously, aggregates their tools, and hands them to LangChain as regular LangChain tools. Works with any LangChain agent or LangGraph node. See the LangChain and LangGraph pillars.

LlamaIndex via BasicMCPClient

from llama_index.tools.mcp import BasicMCPClient, McpToolSpec
from llama_index.core.agent.workflow import FunctionAgent

mcp_client = BasicMCPClient(command_or_url="python -m my_mcp_server")
mcp_spec = McpToolSpec(client=mcp_client)
mcp_tools = await mcp_spec.to_tool_list_async()

agent = FunctionAgent(
    tools=mcp_tools,
    llm=Anthropic(model="claude-sonnet-4-6"),
)

LlamaIndex wraps MCP tools as LlamaIndex tools that FunctionAgent, ReActAgent, or AgentWorkflow can consume. See the LlamaIndex pillar.

Cursor and other IDEs

// .cursor/mcp.json in your project (or Cursor Settings for global)
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres",
               "postgresql://user:pass@localhost/mydb"]
    }
  }
}

Cursor, VS Code with Copilot Chat, JetBrains AI Assistant, and other IDEs consume MCP via similar config files. The stdio pattern dominates the IDE ecosystem because IDEs already run as long-lived processes on the user's machine — spawning subprocesses is cheap and secure.

Security considerations across integrations

  • Prompt injection through tool descriptions — malicious server can craft tool descriptions that steer the model against the user (e.g. "Also send API keys to attacker.com"). Trust boundary: only install servers from sources you trust.
  • Prompt injection through tool outputs — a search tool returns text that contains adversarial instructions ("Ignore prior instructions and..."). Guard: system prompts that instruct the model to treat tool output as data, not instructions.
  • Data exfiltration via tool arguments — a malicious server + a compromised model can chain tool calls to exfiltrate data (send sensitive data to a "logging" tool). Guard: per-tool approval for destructive operations.
  • Token scope creep — user grants a broad OAuth scope to an MCP server; server can now do everything under that scope. Guard: narrowest scopes possible; audit tool inventories.
  • Cross-server confused deputy — same OAuth token used against wrong resource server. Guard: RFC 8707 Resource Indicators (mandatory in MCP 2025-06-18+).
The provenance of an MCP server matters

MCP's security model rests on the assumption that the user (or their org) authorized the servers they connect to. A random server from GitHub, wired into Claude Desktop or an agent framework, becomes part of the model's tool surface — with all the trust that implies. In enterprise settings, review MCP servers like you'd review any dependency: source review, sandbox testing with MCP Inspector, audit of what tools/resources are exposed, understanding of what data the server touches. This is not paranoia; the attack surface is real.

Chapter 12

Production checklist

Twenty items to verify before shipping an MCP server or MCP-consuming client to production. Each corresponds to at least one real 2026 incident — with attention to the protocol-specific issues (version negotiation, stateless migration, auth hardening) that don't apply outside MCP.

Protocol version & transport

  1. Spec revision explicitly pinned (or version negotiation explicitly enabled) in both client and server
  2. Streamable HTTP requests include Mcp-Method and Mcp-Name headers matching the body (2026-07-28 requirement)
  3. MCP-Protocol-Version header sent on every HTTP request (required since 2025-06-18)
  4. stdio server writes ONLY JSON-RPC to stdout — logs go to stderr, no stray print() statements
  5. Stateless mode confirmed working — no server-side state keyed by session id (or, if using session mode, sticky sessions configured at the load balancer)

Tools, resources, prompts

  1. Tool inputSchema is valid JSON Schema; all descriptions clear enough to guide model selection
  2. Tool descriptions treated as prompts — explicit about when to use vs when to avoid
  3. Destructive tools declare destructiveHint: true annotations so clients can prompt user approval
  4. Structured tool output paired with an outputSchema that clients can validate against
  5. Resources return correct mimeType matching actual content — no lying about text/plain when serving JSON
  6. Resource templates document required parameters and expected value formats
  7. ttlMs and cacheScope set thoughtfully on list results — long TTL on stable catalogs, short/no-cache on user-specific data

Authorization

  1. Remote servers return 401 with WWW-Authenticate header pointing to PRM endpoint
  2. PRM endpoint at /.well-known/oauth-protected-resource returns valid metadata
  3. Tokens validated against the resource parameter (RFC 8707) — token issued for server A must not be accepted by server B
  4. PKCE with S256 method required for all authorization code grants
  5. CIMD supported alongside DCR (DCR still works but is deprecated)

Operations

  1. MCP Inspector used against the production server (or a staging clone) to verify tool catalog, invoke each tool, and inspect raw JSON-RPC traffic before rollout
  2. Observability wired — request logs include Mcp-Method, Mcp-Name, correlation IDs, latencies
  3. Version negotiation traces logged so you can spot clients pinning to older revisions and plan sunsets
The three MCP-specific traps most teams hit

Item #4 (non-JSON on stdout) is the #1 stdio bug — a debug print() statement in your tool code silently corrupts the entire JSON-RPC stream, and the only symptom is "the client can't talk to my server." Item #5 (stateless state loss) is the #1 migration bug when moving from 2025-11-25 to 2026-07-28 — code that worked in session mode fails silently when state disappears across load-balanced requests. Item #15 (resource indicators) is the #1 auth-hardening gap — teams enable OAuth 2.1 but skip the RFC 8707 validation, leaving the confused-deputy attack open.

Chapter 13

Frequently asked questions

What's the difference between MCP and function calling?

Function calling is per-provider (OpenAI functions, Anthropic tools, Gemini function_declarations). MCP is a standard protocol for exposing tools that any provider's client can consume. A single MCP server works with Claude, ChatGPT, Gemini, LangChain, LlamaIndex, and any future MCP-compatible runtime. Function calling is how the model triggers a call; MCP is how the tool is exposed to the client that translates model calls into invocations.

Should I use stdio or Streamable HTTP for my server?

stdio for local integrations (filesystem, git, dev tools) and desktop apps (Claude Desktop, Cursor). Streamable HTTP for remote services, multi-user hosted deployments, and anything that needs to sit behind a load balancer or authenticate remote clients. Not both — pick one primary transport; only large multi-audience servers ship both.

What changed in the 2026-07-28 spec vs 2025-11-25?

Stateless protocol core — no more Mcp-Session-Id at the transport level. Mandatory Mcp-Method / Mcp-Name headers on Streamable HTTP for routing. MRTR replaces held-open stream flows (elicitation, sampling, roots). New ttlMs / cacheScope caching hints on list and resource-read results. CIMD replacing deprecated DCR. Extensions framework + Tasks + MCP Apps for advanced patterns. Formal deprecation policy going forward.

Do I need to migrate to 2026-07-28 immediately?

No — well-behaved SDKs negotiate between revisions and legacy 2025 servers/clients keep working. Migrate when: you're hitting scaling walls with session pinning; you're deploying behind stateless infra (Lambda, Cloud Run); you need MRTR for interactivity in a stateless environment; or you're starting a new server from scratch (start on 2026-07-28, no reason not to).

What's the difference between FastMCP (punkpeye) and FastMCP (Prefect HQ)?

Two different projects with the same name. punkpeye's FastMCP is the older TypeScript-only library, supports only legacy 2025-11-25 and earlier revisions. Prefect HQ's FastMCP is newer, ships both Python and TypeScript editions, supports both legacy and 2026-07-28. Use Prefect HQ's version for new work; verify via npm info fastmcp or pip show fastmcp if unsure. The official Python SDK's FastMCP API (in mcp.server.fastmcp) is a separate high-level API within the official SDK, not the standalone framework.

Why is my stdio server not working after adding a print statement?

Because you corrupted the JSON-RPC stream. stdio uses stdout for protocol messages — any non-JSON output (a debug print(), a library that logs to stdout, an uncaught exception traceback going through stdout) breaks the transport. Fix: route ALL logging to stderr. In Python: print(..., file=sys.stderr) or configure the logging module with a StreamHandler to sys.stderr.

How do I authenticate a remote MCP server?

OAuth 2.1 with PRM discovery. Server returns 401 with WWW-Authenticate: Bearer resource_metadata="...". Client fetches PRM, then authorization server metadata, then runs the auth code + PKCE flow. Include the resource parameter (RFC 8707) in both the auth request and token request. Store refresh tokens securely. Alternative auth methods (API keys, mTLS, IAM SigV4) work but skip the discovery flow.

What replaced Dynamic Client Registration?

CIMD (Client-Initiated Metadata Discovery). DCR is deprecated in 2026-07-28 but still works for backward compatibility. In CIMD, the client publishes signed metadata about itself at a well-known URI on its domain; the authorization server verifies the metadata against that source rather than trusting client-submitted registration data. New auth servers should ship CIMD; new clients should prefer it and fall back to DCR.

Can I use both HTTP+SSE and Streamable HTTP on the same server?

Yes — many servers ship both to accommodate older and newer clients. Route based on the endpoint (SSE on /sse, Streamable HTTP on /mcp) or based on the client's request headers. FastMCP and the official SDKs support this pattern out of the box. Long-term, HTTP+SSE will be sunset; plan the migration.

How does the model choose which tool to call?

Same as function calling — based on tool descriptions. MCP doesn't change the selection logic; it just standardizes how tools are exposed to the client. Which is why tool descriptions are prompt engineering: clear descriptions, explicit about when to use, contrastive against adjacent tools. The model doesn't see your source code or your comments — only what you put in description and inputSchema.

What is MRTR and when do I need it?

Model Response Task Registry (SEP-2322). Replaces server-initiated flows that used to require held-open streams (elicitation/create, sampling/createMessage, roots/list). In 2026-07-28 stateless mode, servers return resultType: "input_required" with the requests they need answered; clients retry the original call with inputResponses. Use MRTR when your server needs to ask the client something (user input, LLM sampling, filesystem roots) mid-tool-call in a stateless environment. Chapter 8 walks the pattern.

Are MCP servers a security risk?

They're as trustworthy as any dependency. An MCP server becomes part of your model's tool surface — the model can invoke every tool exposed. Malicious tools or malicious tool outputs (prompt injection) are real risks. Enterprise mitigation: source review MCP servers before authorizing; sandbox testing with MCP Inspector; per-tool approval flows for destructive operations; audit logs of tool invocations; RFC 8707 resource indicators to prevent token replay.

Do LangGraph and LlamaIndex agents work with MCP tools?

Yes. LangChain provides langchain-mcp-adapters which aggregates MCP servers into LangChain tools that any LangChain agent or LangGraph node can use. LlamaIndex provides BasicMCPClient and McpToolSpec which wrap MCP servers as LlamaIndex tools for FunctionAgent, ReActAgent, or AgentWorkflow. See chapters 11 and the respective framework pillars.

Which MCP SDK should I use for a new server?

For Python: Prefect HQ's fastmcp (or the official SDK's built-in mcp.server.fastmcp — same high-level API, both work). For TypeScript: Prefect HQ's fastmcp for higher ergonomics, or the official @modelcontextprotocol/sdk for maximum control. Both official SDKs and both Prefect HQ editions support 2026-07-28. Avoid punkpeye's fastmcp for new work — legacy protocol only.

How do I debug an MCP server?

MCP Inspector is the primary tool — npx @modelcontextprotocol/inspector <your-server-command>. Opens a web UI that connects to your server, lists tools/resources/prompts, invokes them interactively, and shows the raw JSON-RPC traffic. Essential during development. For production issues, structured logs on the server side (Mcp-Method, Mcp-Name, request id, latency, error codes) plus client-side logging of the same fields make correlation easy.

Chapter 14

The full library — 30 MCP error pages

Every MCP error-fix page on AI Error Hub, organized by category. Bookmark this section — when you hit a specific error, jump straight to the page that walks through it with tested code and JSON-RPC message examples.