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:
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:
uv add fastmcpFor full installation instructions, including verification and upgrading, see the Installation Guide.
Upgrading? We have guides for:
- Upgrading from FastMCP 3
- Upgrading from FastMCP 2
- Upgrading from MCP SDK v1 or v2
- Upgrading from the low-level SDK v1 or v2
๐ 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.txtis essentially a sitemap, listing all the pages in the documentation.llms-full.txtcontains 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.
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.
@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.
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"
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:
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.
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:
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.
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:
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"
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:
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.
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:
fastmcp auth cimd create \
--name "My Application" \
--redirect-uri "http://localhost:*/callback" \
--client-id "https://myapp.example.com/oauth/client.json"This produces:
{
"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โ Theclient_idfield 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:
fastmcp auth cimd validate https://myapp.example.com/oauth/client.jsonThe 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_idin 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:
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"
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:
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.
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()`OAuth` Parameters
scopes(str | list[str], optional): OAuth scopes to request. Can be space-separated string or list of stringsclient_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 Clientsclient_secret(str, optional): OAuth client secret for pre-registered clients. Optional โ public clients that rely on PKCE can omit thisclient_metadata_url(str, optional): URL-based client identity (CIMD). See CIMD Authentication for detailstoken_storage(AsyncKeyValue, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See Token Storage for encrypted storage optionsadditional_client_metadata(dict[str, Any], optional): Extra metadata for client registrationcallback_port(int, optional): Fixed port for OAuth callback server. If not specified, uses a random available porthttpx_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.
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.
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.
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.
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.
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:
oauth = OAuth(client_id="my-public-client-id")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.
Creating a Client
You provide a server source and the client automatically infers the appropriate transport mechanism.
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.
from fastmcp import Client, FastMCP
server = FastMCP("TestServer")
client = Client(server) # In-memory, no network or subprocessSTDIO 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.
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.
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.
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.
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.
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.
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.
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:
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.
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 | NoneThe 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.
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 cacheThe 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.
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.
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.
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=.
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.
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.
async with client:
tools = await client.list_tools()
result = await client.call_tool("multiply", {"a": 5, "b": 3})
print(result.data) # 15See 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.
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().
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.
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
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.
Handler Template
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:
Response Actions
You can return data directly, which implicitly accepts the elicitation:
async def elicitation_handler(message, response_type, params, context):
user_input = input(f"{message}: ")
return response_type(value=user_input) # Implicit acceptOr return an ElicitResult for explicit control over the action:
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 thecontentfield.decline: User chose not to provide the requested information. Omitcontent.cancel: User cancelled the entire operation. Omitcontent.
Example
A file management tool might ask which directory to create:
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.
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:
- Build versioned documentation via a GitHub action.
- 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
docsdirectory is merged to main - A change to
netlify.tomlis 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_VERSIONtoTrueinsort-docs-versions.py. - Delete the
unreleasedversion fromversions/versions.json. - Push the changes to the
mainbranch of this repository.