fastmcp Documentation

The fast, Pythonic way to build MCP servers and clients.

RAW Doc

1. Project Overview & Quickstart (PrefectHQ/fastmcp)

File: README.md


The Model Context Protocol (MCP) connects LLMs to tools and data. FastMCP is a full MCP application framework for servers, clients, and interactive apps. A server starts with ordinary Python:

python
from fastmcp import FastMCP

mcp = FastMCP("Demo ๐Ÿš€")

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

if __name__ == "__main__":
    mcp.run()

Why FastMCP

Building an effective MCP application is harder than it looks. FastMCP handles all of it. Declare a tool with a Python function, and the schema, validation, and documentation are generated automatically. Connect to a server with a URL, and transport negotiation, authentication, and protocol lifecycle are managed for you. You focus on your logic, and the MCP part just works: with FastMCP, best practices are built in.

That's why FastMCP is the standard framework for working with MCP. FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024. Today, the actively maintained standalone project is downloaded a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages.

FastMCP has three pillars:

| Servers

Expose tools, resources, and prompts to LLMs. | Apps

Give your tools interactive UIs rendered directly in the conversation. | Clients

Connect to any MCP server โ€” local or remote, programmatic or CLI. |
| --- | --- | --- |

Servers wrap your Python functions into MCP-compliant tools, resources, and prompts. Clients connect to any server with full protocol support. And Apps give your tools interactive UIs rendered directly in the conversation.

Building in TypeScript? FastMCP for TypeScript is the official counterpart, built and maintained by the same team. Same pillars, same ideas, npm install @prefecthq/fastmcp-ts.

Ready to build? Start with the installation guide or jump straight to the quickstart.

Scale MCP with Horizon

FastMCP handles the MCP application layer. Prefect Horizon is the enterprise MCP gateway for scaling servers and tools across teams, with centralized governance over how they are deployed, discovered, secured, and used.

FastMCP and Horizon are built by the same team at Prefect.

Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents.

Start with FastMCP. Scale with Horizon โ†’

Installation

We recommend adding FastMCP to your project with uv:

bash
uv add fastmcp

For full installation instructions, including verification and upgrading, see the Installation Guide.

Upgrading? We have guides for:

๐Ÿ“š Documentation

FastMCP's complete documentation is available at gofastmcp.com, including detailed guides, API references, and advanced patterns.

Documentation is also available in llms.txt format, which is a simple markdown standard that LLMs can consume easily:

  • llms.txt is essentially a sitemap, listing all the pages in the documentation.
  • llms-full.txt contains the entire documentation. Note this may exceed the context window of your LLM.

Community: Join our Discord server to connect with other FastMCP developers and share what you're building.

Contributing

We welcome contributions! See the Contributing Guide for setup instructions, testing requirements, and PR guidelines.


File: docs/apps/overview.mdx


title: Apps sidebarTitle: Overview icon: grid-2 mode: center

import { VersionBadge } from '/snippets/version-badge.mdx'
import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'

A FastMCP app is a tool that returns an interactive UI instead of text. When the host calls it, the user sees a chart, a table, a form, or a whole dashboard rendered right inside the conversation, with working sort, search, tooltips, and state.

The dashboard above is a Prefab showcase โ€” a taste of what you can deliver from a FastMCP tool. Every card, chart, slider, dialog, and carousel is a Python component. Build a composition like this, add @mcp.tool(app=True), and the host renders it inside the conversation.

Under the hood, FastMCP builds on the MCP Apps extension and uses Prefab to describe UIs in Python.

bash
pip install "fastmcp[apps]"

Pick your path

Four patterns cover almost everything you'd want to build. Most apps start with Interactive Tools; you only reach for the others when you've hit a specific limit.

[Interactive Tools](/apps/prefab) โ€” start here

Add app=True to a tool and return a Prefab component. Charts, tables, dashboards, and client-side interactivity (toggles, tabs, filtering) all work without any server round-trips.

python
@mcp.tool(app=True)
def team_directory() -> DataTable:
    return DataTable(columns=[...], rows=employees, search=True)

[FastMCPApp](/apps/fastmcp-app) โ€” when the UI calls back to the server

Forms that save data, buttons that trigger backend work, search that hits a database. FastMCPApp manages the wiring between UI actions and backend tools, with stable tool identifiers that survive server composition.

[Generative UI](/apps/generative) โ€” when the LLM writes the UI

Register one provider and the model can write Prefab code tailored to the current data and request. The user watches the UI build up as the model generates it.

python
mcp.add_provider(GenerativeUI())

[Custom HTML](/apps/low-level) โ€” when you need full control

Write your own HTML, CSS, and JavaScript. Use a specific framework, drop in a map or 3D viewer, embed video. You're talking to the MCP Apps protocol directly.

What's next

  • Quickstart โ€” build a working app in a minute
  • Examples โ€” complete working servers you can run today
  • Providers โ€” ready-made capabilities (approvals, choice pickers, file upload, forms) you add with one line
  • Development โ€” preview app tools locally with fastmcp dev apps

File: docs/clients/auth/bearer.mdx


title: Bearer Token Authentication sidebarTitle: Bearer Auth icon: key

import { VersionBadge } from "/snippets/version-badge.mdx"

Bearer Token authentication is only relevant for HTTP-based transports.

You can configure your FastMCP client to use bearer authentication by supplying a valid access token. This is most appropriate for service accounts, long-lived API keys, CI/CD, applications where authentication is managed separately, or other non-interactive authentication methods.

A Bearer token is a JSON Web Token (JWT) that is used to authenticate a request. It is most commonly used in the Authorization header of an HTTP request, using the Bearer scheme:

http
Authorization: Bearer <token>

Client Usage

The most straightforward way to use a pre-existing Bearer token is to provide it as a string to the auth parameter of the fastmcp.Client or transport instance. FastMCP will automatically format it correctly for the Authorization header and bearer scheme.

If you're using a string token, do not include the `Bearer` prefix. FastMCP will add it for you.
python {5}
from fastmcp import Client

async with Client(
    "https://your-server.fastmcp.app/mcp", 
    auth="<your-token>",
) as client:
    await client.list_tools()

You can also supply a Bearer token to a transport instance, such as StreamableHttpTransport or SSETransport:

python {6}
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport

transport = StreamableHttpTransport(
    "http://your-server.fastmcp.app/mcp", 
    auth="<your-token>",
)

async with Client(transport) as client:
    await client.list_tools()

`BearerAuth` Helper

If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the BearerAuth class yourself, which implements the httpx2.Auth interface.

python {6}
from fastmcp import Client
from fastmcp.client.auth import BearerAuth

async with Client(
    "https://your-server.fastmcp.app/mcp", 
    auth=BearerAuth(token="<your-token>"),
) as client:
    await client.list_tools()

Custom Headers

If the MCP server expects a custom header or token scheme, you can manually set the client's headers instead of using the auth parameter by setting them on your transport:

python {5}
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport

async with Client(
    transport=StreamableHttpTransport(
        "https://your-server.fastmcp.app/mcp", 
        headers={"X-API-Key": "<your-token>"},
    ),
) as client:
    await client.list_tools()

File: docs/clients/auth/cimd.mdx


title: CIMD Authentication sidebarTitle: CIMD icon: id-badge tag: NEW

import { VersionBadge } from "/snippets/version-badge.mdx"

CIMD authentication is only relevant for HTTP-based transports and requires a server that advertises CIMD support.

With standard OAuth, your client registers dynamically with every server it connects to, receiving a fresh client_id each time. This works, but the server has no way to verify who your client actually is โ€” any client can claim any name during registration.

CIMD (Client ID Metadata Documents) flips this around. You host a small JSON document at an HTTPS URL you control, and that URL becomes your client_id. When your client connects to a server, the server fetches your metadata document and can verify your identity through your domain ownership. Users see a verified domain badge in the consent screen instead of an unverified client name.

Client Usage

Pass your CIMD document URL to the client_metadata_url parameter of OAuth:

python
from fastmcp import Client
from fastmcp.client.auth import OAuth

async with Client(
    "https://mcp-server.example.com/mcp",
    auth=OAuth(
        client_metadata_url="https://myapp.example.com/oauth/client.json",
    ),
) as client:
    await client.list_tools()

When the server supports CIMD, the client uses your metadata URL as its client_id instead of performing Dynamic Client Registration. The server fetches your document, validates it, and proceeds with the standard OAuth authorization flow.

You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` โ€” the transport provides the server URL automatically.

Creating a CIMD Document

A CIMD document is a JSON file that describes your client. The most important field is client_id, which must exactly match the URL where you host the document.

Use the FastMCP CLI to generate one:

bash
fastmcp auth cimd create \
    --name "My Application" \
    --redirect-uri "http://localhost:*/callback" \
    --client-id "https://myapp.example.com/oauth/client.json"

This produces:

json
{
  "client_id": "https://myapp.example.com/oauth/client.json",
  "client_name": "My Application",
  "redirect_uris": ["http://localhost:*/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code"],
  "response_types": ["code"]
}

If you omit --client-id, the CLI generates a placeholder value and reminds you to update it before hosting.

CLI Options

The create command accepts these flags:

Flag Description
--name Human-readable client name (required)
--redirect-uri, -r Allowed redirect URIs โ€” can be specified multiple times (required)
--client-id The URL where you'll host this document (sets client_id directly)
--output, -o Write to a file instead of stdout
--scope Space-separated list of scopes the client may request
--client-uri URL of the client's home page
--logo-uri URL of the client's logo image
--no-pretty Output compact JSON

Redirect URIs

The redirect_uris field supports wildcard port matching for localhost. The pattern http://localhost:*/callback matches any port, which is useful for development clients that bind to random available ports (which is what FastMCP's OAuth helper does by default).

Hosting Requirements

CIMD documents must be hosted at a publicly accessible HTTPS URL with a non-root path:

  • HTTPS required โ€” HTTP URLs are rejected for security
  • Non-root path โ€” The URL must have a path component (e.g., /oauth/client.json, not just /)
  • Public accessibility โ€” The server must be able to fetch the document over the internet
  • Matching client_id โ€” The client_id field in the document must exactly match the hosting URL

Common hosting options include static file hosting services like GitHub Pages, Cloudflare Pages, Vercel, or S3 โ€” anywhere you can serve a JSON file over HTTPS.

Validating Your Document

Before deploying, verify your hosted document passes validation:

bash
fastmcp auth cimd validate https://myapp.example.com/oauth/client.json

The validator fetches the document and checks that:

  • The URL is valid (HTTPS, non-root path)
  • The document is well-formed JSON conforming to the CIMD schema
  • The client_id in the document matches the URL it was fetched from

How It Works

When your client connects to a CIMD-enabled server, the flow works like this:

Your client sends its `client_metadata_url` as the `client_id` in the OAuth authorization request. The server sees that the `client_id` is an HTTPS URL with a path โ€” the signature of a CIMD client โ€” and skips Dynamic Client Registration. The server fetches your JSON document from the URL, validates that `client_id` matches the URL, and extracts your client metadata (name, redirect URIs, scopes). The standard OAuth flow continues: browser opens for user consent, authorization code exchange, token issuance. The consent screen shows your verified domain.

The server caches your CIMD document according to HTTP cache headers, so subsequent requests don't require re-fetching.

Server Configuration

CIMD is a server-side feature that your MCP server must support. FastMCP's OAuth proxy providers (GitHub, Google, Auth0, etc.) support CIMD by default. See the OAuth Proxy CIMD documentation for server-side configuration, including private key JWT authentication and security details.


File: docs/clients/auth/oauth.mdx


title: OAuth Authentication sidebarTitle: OAuth icon: window

import { VersionBadge } from "/snippets/version-badge.mdx"

OAuth authentication is only relevant for HTTP-based transports and requires user interaction via a web browser.

When your FastMCP client needs to access an MCP server protected by OAuth 2.1, and the process requires user interaction (like logging in and granting consent), you should use the Authorization Code Flow. FastMCP provides the fastmcp.client.auth.OAuth helper to simplify this entire process.

This flow is common for user-facing applications where the application acts on behalf of the user.

Client Usage

Default Configuration

The simplest way to use OAuth is to pass the string "oauth" to the auth parameter of the Client or transport instance. FastMCP will automatically configure the client to use OAuth with default settings:

python {4}
from fastmcp import Client

# Uses default OAuth settings
async with Client("https://your-server.fastmcp.app/mcp", auth="oauth") as client:
    await client.list_tools()

`OAuth` Helper

To fully configure the OAuth flow, use the OAuth helper and pass it to the auth parameter of the Client or transport instance. OAuth manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full httpx2.Auth interface.

python {2, 4, 6}
from fastmcp import Client
from fastmcp.client.auth import OAuth

oauth = OAuth(scopes=["user"])

async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
    await client.list_tools()
You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` โ€” the transport provides the server URL automatically.

`OAuth` Parameters

  • scopes (str | list[str], optional): OAuth scopes to request. Can be space-separated string or list of strings
  • client_name (str, optional): Client name for dynamic registration. Defaults to "FastMCP Client"
  • client_id (str, optional): Pre-registered OAuth client ID. When provided, skips Dynamic Client Registration entirely. See Pre-Registered Clients
  • client_secret (str, optional): OAuth client secret for pre-registered clients. Optional โ€” public clients that rely on PKCE can omit this
  • client_metadata_url (str, optional): URL-based client identity (CIMD). See CIMD Authentication for details
  • token_storage (AsyncKeyValue, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See Token Storage for encrypted storage options
  • additional_client_metadata (dict[str, Any], optional): Extra metadata for client registration
  • callback_port (int, optional): Fixed port for OAuth callback server. If not specified, uses a random available port
  • httpx_client_factory (McpHttpClientFactory, optional): Factory for creating httpx2 clients

OAuth Flow

The OAuth flow is triggered when you use a FastMCP Client configured to use OAuth.

The client first checks the configured `token_storage` backend for existing, valid tokens for the target server. If one is found, it will be used to authenticate the client. If no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`. If a `client_id` is provided, the client uses those pre-registered credentials directly and skips this step entirely. Otherwise, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity. As a fallback, the client performs Dynamic Client Registration (RFC 7591) if the server supports it. A temporary local HTTP server is started on an available port (or the port specified via `callback_port`). This server's address (e.g., `http://127.0.0.1:/callback`) acts as the `redirect_uri` for the OAuth flow. The user's default web browser is automatically opened, directing them to the OAuth server's authorization endpoint. The user logs in and grants (or denies) the requested `scopes`. Upon approval, the OAuth server redirects the user's browser to the local callback server with an `authorization_code`. The client captures this code and exchanges it with the OAuth server's token endpoint for an `access_token` (and often a `refresh_token`) using PKCE for security. The obtained tokens are saved to the configured `token_storage` backend for future use, eliminating the need for repeated browser interactions. The access token is automatically included in the `Authorization` header for requests to the MCP server. If the access token expires, the client will automatically use the refresh token to get a new access token.

Token Storage

By default, tokens are stored in memory and lost when your application restarts. For persistent storage, pass an AsyncKeyValue-compatible storage backend to the token_storage parameter.

**Security Consideration**: Use encrypted storage for production. MCP clients can accumulate OAuth credentials for many servers over time, and a compromised token store could expose access to multiple services.
python
from fastmcp import Client
from fastmcp.client.auth import OAuth
from key_value.aio.stores.disk import DiskStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
import os

# Create encrypted disk storage
encrypted_storage = FernetEncryptionWrapper(
    key_value=DiskStore(directory="~/.fastmcp/oauth-tokens"),
    fernet=Fernet(os.environ["OAUTH_STORAGE_ENCRYPTION_KEY"])
)

oauth = OAuth(token_storage=encrypted_storage)

async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
    await client.list_tools()

You can use any AsyncKeyValue-compatible backend from the key-value library including Redis, DynamoDB, and more. Wrap your storage in FernetEncryptionWrapper for encryption.

When selecting a storage backend, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have constraints that affect production suitability.

CIMD Authentication

Client ID Metadata Documents (CIMD) provide an alternative to Dynamic Client Registration. Instead of registering with each server, your client hosts a static JSON document at an HTTPS URL. That URL becomes your client's identity, and servers can verify who you are through your domain ownership.

python
from fastmcp import Client
from fastmcp.client.auth import OAuth

async with Client(
    "https://mcp-server.example.com/mcp",
    auth=OAuth(
        client_metadata_url="https://myapp.example.com/oauth/client.json",
    ),
) as client:
    await client.list_tools()

See the CIMD Authentication page for complete documentation on creating, hosting, and validating CIMD documents.

Pre-Registered Clients

Some OAuth servers don't support Dynamic Client Registration โ€” the MCP spec explicitly makes DCR optional. If your client has been pre-registered with the server (you already have a client_id and optionally a client_secret), you can provide them directly to skip DCR entirely.

python
from fastmcp import Client
from fastmcp.client.auth import OAuth

async with Client(
    "https://mcp-server.example.com/mcp",
    auth=OAuth(
        client_id="my-registered-client-id",
        client_secret="my-client-secret",
    ),
) as client:
    await client.list_tools()

Public clients that rely on PKCE for security can omit client_secret:

python
oauth = OAuth(client_id="my-public-client-id")
When using pre-registered credentials, the client will not attempt Dynamic Client Registration. If the server rejects the credentials, the error is surfaced immediately rather than falling back to DCR.

File: docs/clients/client.mdx


title: The FastMCP Client sidebarTitle: Overview icon: user-robot

import { VersionBadge } from '/snippets/version-badge.mdx'

The fastmcp.Client class provides a programmatic interface for interacting with any MCP server. It handles protocol details and connection management automatically, letting you focus on the operations you want to perform.

The FastMCP Client is designed for deterministic, controlled interactions rather than autonomous behavior, making it ideal for testing MCP servers during development, building deterministic applications that need reliable MCP interactions, and creating the foundation for agentic or LLM-based clients with structured, type-safe operations.

This is a programmatic client that requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems.

Creating a Client

You provide a server source and the client automatically infers the appropriate transport mechanism.

python
import asyncio
from fastmcp import Client, FastMCP

# In-memory server (ideal for testing)
server = FastMCP("TestServer")
client = Client(server)

# HTTP server
client = Client("https://example.com/mcp")

# Local Python script
client = Client("my_mcp_server.py")

async def main():
    async with client:
        # List available operations
        tools = await client.list_tools()
        resources = await client.list_resources()
        prompts = await client.list_prompts()

        # Execute operations
        result = await client.call_tool("example_tool", {"param": "value"})
        print(result)

asyncio.run(main())

All client operations require using the async with context manager for proper connection lifecycle management.

Choosing a Transport

The client automatically selects a transport based on what you pass to it, but different transports have different characteristics that matter for your use case.

In-memory transport connects directly to a FastMCP server instance within the same Python process. Use this for testing and development where you want to eliminate subprocess and network complexity. The server shares your process's environment and memory space.

python
from fastmcp import Client, FastMCP

server = FastMCP("TestServer")
client = Client(server)  # In-memory, no network or subprocess

STDIO transport launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. By default, the subprocess receives the MCP SDK's default environment; pass an explicit transport when you need to add environment variables, set a working directory, or control process reuse.

python
from fastmcp import Client
from fastmcp.client.transports import PythonStdioTransport

# Simple inference from file path
client = Client("my_server.py")

# With explicit environment configuration
transport = PythonStdioTransport(
    "my_server.py",
    env={"API_KEY": "secret"},
)
client = Client(transport)

HTTP transport connects to servers running as web services. Use this for production deployments where the server runs independently and manages its own lifecycle.

python
from fastmcp import Client

client = Client("https://api.example.com/mcp")

See Transports for detailed configuration options including authentication headers, session persistence, and multi-server configurations.

Configuration-Based Clients

Create clients from MCP configuration dictionaries, which can include multiple servers. While there is no official standard for MCP configuration format, FastMCP follows established conventions used by tools like Claude Desktop.

python
config = {
    "mcpServers": {
        "weather": {
            "url": "https://weather-api.example.com/mcp"
        },
        "assistant": {
            "command": "python",
            "args": ["./assistant_server.py"]
        }
    }
}

client = Client(config)

async with client:
    # Tools are prefixed with server names
    weather_data = await client.call_tool("weather_get_forecast", {"city": "London"})
    response = await client.call_tool("assistant_answer_question", {"question": "What's the capital of France?"})

    # Resources use prefixed URIs
    icons = await client.read_resource("weather://weather/icons/sunny")

Connection Lifecycle

The client uses context managers for connection management. When you enter the context, the client establishes a connection and negotiates the protocol era with the server. Metadata returned by either legacy initialization or modern discovery is exposed through the same client properties.

python
from fastmcp import Client, FastMCP

mcp = FastMCP(name="MyServer", instructions="Use the greet tool to say hello!")

@mcp.tool
def greet(name: str) -> str:
    """Greet a user by name."""
    return f"Hello, {name}!"

async with Client(mcp) as client:
    # Protocol negotiation already happened automatically
    assert client.server_info is not None
    assert client.server_capabilities is not None
    print(f"Server: {client.server_info.name}")
    print(f"Instructions: {client.instructions}")
    print(f"Capabilities: {client.server_capabilities.tools}")

For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call initialize() manually. initialize() is a handshake-era operation, so pin the connection with mode="legacy": the modern protocol has no initialize round trip, and calling it on a modern connection raises.

python
from fastmcp import Client

client = Client("my_mcp_server.py", auto_initialize=False, mode="legacy")

async with client:
    # Connection established, but not initialized yet
    print(f"Connected: {client.is_connected()}")
    print(f"Initialized: {client.initialize_result is not None}")  # False

    # Initialize manually with custom timeout
    result = await client.initialize(timeout=10.0)
    print(f"Server: {result.server_info.name}")

    # Now ready for operations
    tools = await client.list_tools()

Protocol negotiation

MCP has two protocol eras: the original legacy era, which begins every connection with an initialize handshake, and the modern era (protocol version 2026-07-28 and later), which a client discovers by probing the server's server/discover endpoint. The mode parameter controls which era the client negotiates when it connects.

By default, mode="auto". The client probes server/discover and adopts the modern protocol when the server responds; for any server that is not positive evidence of modern support, it falls back to the legacy handshake. This makes the default safe against a mixed fleet of legacy and modern servers.

python
from fastmcp import Client

# Negotiate the newest era the server supports (the default)
client = Client("https://example.com/mcp", mode="auto")

Set mode="legacy" to force the initialize handshake. This behaves identically to earlier FastMCP versions and is the opt-out if a server misbehaves under discovery or you need the legacy initialize result object.

python
client = Client("https://example.com/mcp", mode="legacy")

Legacy mode is also what carries the pushed form of a server's requests. The handshake opens a persistent back-channel down which a server can send a sampling, roots, or elicitation request mid-call, and the modern era removed it. Your handlers are unaffected by that: a sampling, roots, or elicitation handler you register answers a modern server's input-required rounds from the same registration. Pin mode="legacy" when you connect to a server that pushes, or when your code calls client.ping() or transport.get_session_id(), which need the session the modern era does not open.

Conversely, background tasks are modern-only: the tasks capability is negotiated over 2026-07-28 connections, so mode="legacy" never triggers one and a task-enabled tool just runs synchronously.

A FastMCP server serves both eras, so a default client negotiates the modern one and the session-dependent calls raise an era-specific error there. Pinning the handshake restores them.

You can also pin a specific modern protocol version to adopt it directly, without a discovery probe:

python
client = Client("https://example.com/mcp", mode="2026-07-28")

Once connected, the negotiated version, server identity, capabilities, and instructions are available as properties. They are populated from either the legacy InitializeResult or modern DiscoverResult, and reset to None when the client disconnects. instructions is also None when the server does not provide any.

When you pin a modern version directly, the client skips discovery and adopts that version with minimal synthesized metadata. In that mode, server_info has an empty name and instructions is None.

python
async with Client("https://example.com/mcp", mode="auto") as client:
    print(client.protocol_version)      # e.g. "2026-07-28"
    print(client.server_info)           # Implementation | None
    print(client.server_capabilities)   # ServerCapabilities | None
    print(client.instructions)          # str | None
`mode="auto"` is the default as of FastMCP 4.0. Earlier versions defaulted to `"legacy"`. If a server behaves unexpectedly under discovery, or you depend on the legacy `initialize` result, pin the old behavior with `Client(..., mode="legacy")`.

The SSE transport is legacy-only โ€” it cannot carry the sessionless modern era โ€” so a client connecting over SSE always negotiates the legacy handshake, even under mode="auto". A multi-server config (MCPConfigTransport with more than one server) is likewise legacy-only, because it mounts each backend behind a legacy-era proxy; a single-server config mirrors its one backend transport's era.

Response caching

The client can cache the results of list_tools, list_resources, and list_prompts so that repeated calls avoid a network round-trip. Caching is opt-in and honors the server's own cache hints, so it only takes effect against modern-era servers that advertise them โ€” a cache is inert on a legacy connection.

Enable the default in-memory cache by passing cache=True. It respects the ttlMs and cacheScope hints the server attaches to each response.

python
from fastmcp import Client

client = Client("https://example.com/mcp", mode="auto", cache=True)

async with client:
    tools = await client.list_tools()   # fetched from the server
    tools = await client.list_tools()   # served from the cache

The default (cache=None) and cache=False both disable caching. For control over the store, TTL, or partitioning, pass a CacheConfig. A custom config requires a target_id, since in-memory FastMCP transports expose no server URL to derive a shared-store identity from.

python
from fastmcp import Client
from mcp.client.caching import CacheConfig

config = CacheConfig(target_id="weather-api", default_ttl_ms=60_000)
client = Client("https://example.com/mcp", mode="auto", cache=config)

The high-level list_tools, list_resources, and list_prompts methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level list_tools_mcp, list_resources_mcp, list_resource_templates_mcp, and list_prompts_mcp variants, which accept a cache_mode argument: "use" (the default) serves and stores, "refresh" stores a fresh result without serving a cached one, and "bypass" skips the cache entirely.

python
async with client:
    fresh = await client.list_tools_mcp(cache_mode="refresh")

Sharing a cache across clients

The default cache lives in each client's process. To share cached responses across a fleet โ€” a set of proxy replicas backed by one Redis, for example โ€” pass a KeyValueResponseCacheStore, FastMCP's adapter over the same AsyncKeyValue key-value abstraction the event store and OAuth proxy use. It accepts any compatible backend (memory, Redis, and more).

A shared store mingles responses from different principals, so it requires an explicit partition that isolates them. Derive the partition from a verified credential โ€” never from request data or the server URL โ€” and construct a new client when the principal changes. Only responses the server marks "public" are ever served across partitions.

python
from fastmcp import Client
from fastmcp.client.caching import KeyValueResponseCacheStore
from mcp.client.caching import CacheConfig
from key_value.aio.stores.redis import RedisStore

backend = RedisStore(url="redis://localhost")
store = KeyValueResponseCacheStore(storage=backend)

config = CacheConfig(store=store, partition="tenant-a", target_id="weather-api")
client = Client("https://example.com/mcp", mode="auto", cache=config)

The adapter serializes each result through a type-tagged envelope validated against an allowlist of cacheable result models, so a value naming an unknown type is treated as a cache miss rather than deserialized blindly. Each store instance owns its own collection namespace; clear() affects only that namespace, never another tenant's entries.

Client extensions

Client extensions (SEP-2133) are the advanced mechanism a client uses to opt into vendor capabilities that live outside the core protocol. An extension is a ClientExtension instance that bundles three things: a capability advertisement the server can read, one or more result claims that let the client parse extra tools/call result shapes, and notification bindings that observe server notifications the core protocol doesn't define. Pass a sequence of them to extensions=.

python
from fastmcp import Client
from myproject.extensions import AppsExtension

client = Client("https://example.com/mcp", extensions=[AppsExtension()])

Each extension's contributions are threaded into the underlying session. FastMCP folds in its own internal extension for background tasks automatically, and your own extensions compose with it rather than replacing it โ€” pass your own tasks extension with the same identifier if you need to override it. When a tool returns a shape an extension claims, client.call_tool() resolves it transparently through the owning claim's resolver and hands you back an ordinary result. Result claims and their advertisements are honored only on modern-era connections, so they are inert on a legacy handshake.

For the rare case where you need to register additional result claims against an extension that is already advertised, pass them through result_claims=, keyed by the extension's identifier. Prefer declaring claims on the extension itself; this parameter merges extra claims with an extension's own.

python
client = Client(
    "https://example.com/mcp",
    extensions=[AppsExtension()],
    result_claims={"example.com/apps": [extra_claim]},
)

Operations

FastMCP clients interact with three types of server components.

Tools are server-side functions that the client can execute with arguments. Call them with call_tool() and receive structured results.

python
async with client:
    tools = await client.list_tools()
    result = await client.call_tool("multiply", {"a": 5, "b": 3})
    print(result.data)  # 15

See Tools for detailed documentation including version selection, error handling, and structured output.

Resources are data sources that the client can read, either static or templated. Access them with read_resource() using URIs.

python
async with client:
    resources = await client.list_resources()
    content = await client.read_resource("file:///config/settings.json")
    print(content[0].text)

See Resources for detailed documentation including templates and binary content.

Prompts are reusable message templates that can accept arguments. Retrieve rendered prompts with get_prompt().

python
async with client:
    prompts = await client.list_prompts()
    messages = await client.get_prompt("analyze_data", {"data": [1, 2, 3]})
    print(messages.messages)

See Prompts for detailed documentation including argument serialization.

Callback Handlers

The client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.

Sampling, elicitation, and roots are the requests a server makes of the client. A server reaches your handler by whichever route its era allows โ€” pushed down the open session on the handshake, returned as an input-required result on the modern protocol โ€” and both routes dispatch to the same handler, so one registration covers both. Logging and progress arrive as notifications on the response stream and work in either era.

python
from fastmcp import Client
from fastmcp.client.logging import LogMessage

async def log_handler(message: LogMessage):
    print(f"Server log: {message.data}")

async def progress_handler(progress: float, total: float | None, message: str | None):
    print(f"Progress: {progress}/{total} - {message}")

async def sampling_handler(messages, params, context):
    # Integrate with your LLM service here
    return "Generated response"

client = Client(
    "my_mcp_server.py",
    log_handler=log_handler,
    progress_handler=progress_handler,
    sampling_handler=sampling_handler,
    timeout=30.0
)

Each handler type has its own documentation:

  • Sampling - Respond to server LLM requests
  • Elicitation - Handle server requests for user input
  • Progress - Monitor long-running operations
  • Logging - Handle server log messages
  • Roots - Provide local context to servers
The FastMCP Client is designed as a foundational tool. Use it directly for deterministic operations, or build higher-level agentic systems on top of its reliable, type-safe interface.

File: docs/clients/elicitation.mdx


title: User Elicitation sidebarTitle: Elicitation icon: message-question

import { VersionBadge } from "/snippets/version-badge.mdx";

Use this when you need to respond to server requests for user input during tool execution.

Elicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.

**These sections show the server-initiated flow, which the handshake-era protocol uses.** On `2026-07-28` the server asks by returning a request instead โ€” see [input-required rounds](#input-required-rounds). One `elicitation_handler` serves both, so the examples below pin `mode="legacy"` only to exercise the pushed form.

Handler Template

python
from fastmcp import Client
from fastmcp.client.elicitation import ElicitResult, ElicitRequestParams, RequestContext

async def elicitation_handler(
    message: str,
    response_type: type | None,
    params: ElicitRequestParams,
    context: RequestContext
) -> ElicitResult | object:
    """
    Handle server requests for user input.

    Args:
        message: The prompt to display to the user
        response_type: Python dataclass type for form responses (None for URL requests or empty schemas)
        params: Original MCP elicitation parameters
        context: Request context with metadata

    Returns:
        - Data directly (implicitly accepts the elicitation)
        - ElicitResult for explicit control over the action
    """
    # Present the message and collect input
    user_input = input(f"{message}: ")

    if not user_input:
        return ElicitResult(action="decline")

    # URL requests and empty-object schemas have no response type to construct,
    # so accepting is the whole response.
    if response_type is None:
        return ElicitResult(action="accept")

    # Create response using the provided dataclass type
    return response_type(value=user_input)

client = Client(
    "my_mcp_server.py",
    mode="legacy",
    elicitation_handler=elicitation_handler,
)

How It Works

When a server needs user input, it sends an elicitation request with a message prompt. Form elicitation requests include a JSON schema describing the expected response structure, and FastMCP automatically converts that schema into a Python dataclass type. URL elicitation requests and empty-object schemas use response_type=None.

The handler receives four parameters:

The prompt message to display to the user A Python dataclass type that FastMCP created from a form request's JSON schema. Use this to construct your response with proper typing. For URL requests or empty-object schemas, this will be `None`. The original MCP elicitation parameters. Form requests carry the raw JSON schema on `params.requested_schema`; URL requests carry `params.url` instead and have no schema. Request context containing metadata about the elicitation request

Response Actions

You can return data directly, which implicitly accepts the elicitation:

python
async def elicitation_handler(message, response_type, params, context):
    user_input = input(f"{message}: ")
    return response_type(value=user_input)  # Implicit accept

Or return an ElicitResult for explicit control over the action:

python
from fastmcp.client.elicitation import ElicitResult

async def elicitation_handler(message, response_type, params, context):
    user_input = input(f"{message}: ")

    if not user_input:
        return ElicitResult(action="decline")  # User declined

    if user_input == "cancel":
        return ElicitResult(action="cancel")   # Cancel entire operation

    return ElicitResult(
        action="accept",
        content=response_type(value=user_input)
    )

Action types:

  • accept: User provided valid input. Include the data in the content field.
  • decline: User chose not to provide the requested information. Omit content.
  • cancel: User cancelled the entire operation. Omit content.

Example

A file management tool might ask which directory to create:

python
from fastmcp import Client
from fastmcp.client.elicitation import ElicitResult

async def elicitation_handler(message, response_type, params, context):
    print(f"Server asks: {message}")

    user_response = input("Your response: ")

    if not user_response:
        return ElicitResult(action="decline")

    # Use the response_type dataclass to create a properly structured response
    return response_type(value=user_response)

client = Client(
    "my_mcp_server.py",
    mode="legacy",
    elicitation_handler=elicitation_handler
)

Input-required rounds

On protocol version 2026-07-28 and later, a server can ask for input before it returns a final result. Nothing is held open: the tool returns a description of what it needs, which completes that round as an ordinary response, and the client answers by issuing a new call_tool, get_prompt, or read_resource request carrying the answer. fastmcp.Client drives that loop for you โ€” it fulfils each round's requests using the callbacks you already configured (your elicitation_handler, sampling_handler, and roots) and repeats until the call reaches a terminal result. No extra wiring is needed beyond the handlers described above.

The input_required_max_rounds parameter caps how many rounds the client will answer before giving up, guarding against a server that never terminates. It defaults to 10.

python
client = Client(
    "https://example.com/mcp",
    mode="auto",
    elicitation_handler=elicitation_handler,
    input_required_max_rounds=5,
)

2. Official Technical Reference & Guides (PrefectHQ/docs)

Prefect Versioned Documentation

This repository contains Prefect's versioned documentation. Development of the docs
happens in the docs directory
of the main Prefect repository, so any edits or additions should be done there.

The docs repository serves two purposes:

  1. Build versioned documentation via a GitHub action.
  2. Make versioned docs available for publishing via Netlify.

How it works

First, the docs build action is triggered, either manually or by an automatic
repository dispatch call from the PrefectHQ/prefect repository.

Next, the build happens in the build workflow action.

To keep things simple, the workflow primarily consists of Bash commands that clone the a
branch or release tag of the Prefect repository,
prepare it for the mike build utility, sort the versions list mike generates, and
then create a pull request to merge the changes into main.

The workflow calls several small Python scripts in the utilities directory to handle
scripts handle preparation steps that would be difficult or verbose in bash.

Manual docs builds

Manually triggered builds accept two parameters:

  • A version
  • An optional display name that's displayed in the version selector box.

Choosing version and display name

If you don't provide a version name, the build action uses the version as the display
name. In most cases, this is what you want. If you're building the docs for 2.10.4,
for example, you probably want them published under the name 2.10.4.

In that scenario, entering 2.10.4 as the version and leaving the display name blank
will build the docs for Prefect 2.10.4 and publish them as 2.10.4 - exactly what
you want.

However, you may occasionally want to publish a docs version under an alias.
For example, it might be better to combine the docs for older release series under a
single name to keep the length of the version selector box manageable.

So, if you know that the docs for 2.6.12 are useful for the entire series of 2.6
releases, you might decide to build the docs for 2.6.12 but publish them as 2.6.x.

Merging and deploying manual builds

When a manual docs build completes, the build action will create a pull request that
must be reviewed by a human and merged into the main branch.

An updated copy of the website is deployed to Netlify every time a merge to main
occurs and the updated docs usually appear at https://docs.prefect.io in under a minute.

Automated docs builds

Automated docs builds run in response specific changes to in the Prefect repository.

The events in the Prefect repo that trigger a versioned docs build in this repository
are:

  • A new release is published
  • A change to the docs directory is merged to main
  • A change to netlify.toml is merged to main

Merging and deploying automated builds

Automated builds create auto-merge pull requests, so no human intervention should be
required unless something goes wrong.

If the docs site isn't updating in response to
changes that should trigger a new build, check the list of recent build workflow
runs
for build failures.

`unreleased` docs version

The versioned docs currently show an unreleased version in the version selector.
This version includes the latest docs changes from the main branch of the prefect
repository.

To hide the unreleased docs, do the following:

  • Change HIDE_UNRELEASED_VERSION to True in sort-docs-versions.py.
  • Delete the unreleased version from versions/versions.json.
  • Push the changes to the main branch of this repository.