Improve the v4 docs (#4707)

This commit is contained in:
Jeremiah Lowin 2026-07-29 09:51:13 -04:00 committed by GitHub
commit 0792ac812c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 218 additions and 163 deletions

View file

@ -59,16 +59,24 @@ This works with **stdio**, **SSE**, and **stateful HTTP** transports, where sess
In **stateless HTTP** mode, each request creates a new session object with a new ID. Files stored during one request (e.g. the UI upload) will be invisible to the next request (e.g. the LLM calling `list_files`). You **must** override `_get_scope_key` to use a stable identifier like a user ID from your auth token.
</Warning>
For stateless deployments, override `_get_scope_key` to return a stable identifier. For example, to scope files by authenticated user:
For stateless deployments, override `_get_scope_key` to return a stable identifier. To scope files by authenticated user, read the caller from `get_access_token()`.
Reject the request when there is no subject to key on. `get_access_token()` returns `None` on an unauthenticated request, and `subject` is optional even on a valid token, since not every verifier populates it. Returning a fallback in either case would put every such caller in one shared bucket, so they would see each other's uploads.
```python
from fastmcp.apps.file_upload import FileUpload
from fastmcp.server.dependencies import get_access_token
class UserScopedUpload(FileUpload):
def _get_scope_key(self, ctx):
return ctx.access_token["sub"]
token = get_access_token()
if token is None or not token.subject:
raise ValueError("File scoping requires an authenticated user with a subject")
return token.subject
```
If your provider carries the user identity in a different claim, read it from `token.claims` and validate it the same way.
For process-wide shared storage (all users see all files):
```python
@ -85,10 +93,17 @@ The default implementation stores files in memory for the lifetime of the server
import base64
from fastmcp.apps.file_upload import FileUpload
from fastmcp.server.dependencies import get_access_token
class S3Upload(FileUpload):
def _get_scope_key(self, ctx):
token = get_access_token()
if token is None or not token.subject:
raise ValueError("File scoping requires an authenticated user with a subject")
return token.subject
def on_store(self, files, ctx):
user_id = ctx.access_token["sub"]
user_id = self._get_scope_key(ctx)
for f in files:
s3.put_object(
Bucket="uploads",
@ -98,7 +113,7 @@ class S3Upload(FileUpload):
return self.on_list(ctx)
def on_list(self, ctx):
user_id = ctx.access_token["sub"]
user_id = self._get_scope_key(ctx)
objects = s3.list_objects(Bucket="uploads", Prefix=f"{user_id}/")
return [
{
@ -112,7 +127,7 @@ class S3Upload(FileUpload):
]
def on_read(self, name, ctx):
user_id = ctx.access_token["sub"]
user_id = self._get_scope_key(ctx)
obj = s3.get_object(Bucket="uploads", Key=f"{user_id}/{name}")
content = obj["Body"].read()
return {

View file

@ -9,7 +9,7 @@ tag: NEW
**[v4.0.0b1: Fourgone Conclusion](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1)**
FastMCP 4 rebuilds the framework on the MCP Python SDK v2, and this beta is the first release to run on the SDK's stable 2.0. The SDK v2 rewrote the protocol layer end to end — protocol types moved into a standalone `mcp_types` package, every model field renamed from camelCase to snake_case in Python, and the server's request-handling model replaced — and FastMCP absorbs nearly all of it, so most FastMCP 3 servers run untouched. On that foundation v4 serves the sessionless `2026-07-28` protocol and the older handshake from one server, adds stateless session state and background tasks, makes protocol extensions a first-class surface, and removes server-initiated sampling and roots from the server API.
FastMCP 4 makes stateful MCP applications work on the sessionless `2026-07-28` protocol while one deployment continues serving handshake-era clients. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions. Protocol extensions and enterprise identity become first-class surfaces, and most FastMCP 3 servers upgrade unchanged even though MCP Python SDK v2 rewrote the engine underneath them. Server-initiated sampling and roots are removed from the server API; the [upgrade guide](/getting-started/upgrading/from-fastmcp-3) covers their replacements.
### New Features 🎉
* Migrate to MCP Python SDK v2 by [@jlowin](https://github.com/jlowin) in [#4437](https://github.com/PrefectHQ/fastmcp/pull/4437)

View file

@ -89,10 +89,10 @@ To skip authentication entirely — useful for local development servers — pas
fastmcp call http://localhost:8000/mcp my_tool --auth none
```
You can also pass a bearer token directly:
You can also pass a bearer token directly. Give the token value on its own; FastMCP adds the `Bearer` prefix when it builds the `Authorization` header.
```bash
fastmcp list http://localhost:8000/mcp --auth "Bearer sk-..."
fastmcp list http://localhost:8000/mcp --auth "sk-..."
```
## Transport Override

View file

@ -144,12 +144,12 @@ async with Client(mcp) as client:
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:
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)
client = Client("my_mcp_server.py", auto_initialize=False, mode="legacy")
async with client:
# Connection established, but not initialized yet
@ -219,7 +219,7 @@ The SSE transport is legacy-only — it cannot carry the sessionless modern era
<VersionBadge version="4.0.0" />
The client can cache the results of `list_tools`, `list_resources`, `list_prompts`, and `read_resource` 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.
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.
@ -243,7 +243,7 @@ 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`, `list_prompts`, and `read_resource` methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level `*_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.
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:

View file

@ -28,7 +28,16 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()
LOGGING_LEVEL_MAP = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"NOTICE": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
"ALERT": logging.CRITICAL,
"EMERGENCY": logging.CRITICAL,
}
async def log_handler(message: LogMessage):
"""Forward MCP server logs to Python's logging system."""

View file

@ -58,18 +58,25 @@ async with client:
Binary resources include images, PDFs, and other non-text data:
Binary resources arrive as `BlobResourceContents`, whose `blob` field is a base64 **string**, so decode it before writing bytes to disk:
```python
import base64
from mcp_types import BlobResourceContents
async with client:
content = await client.read_resource("resource://images/logo.png")
for item in content:
if hasattr(item, 'blob'):
print(f"Binary content: {len(item.blob)} bytes")
if isinstance(item, BlobResourceContents):
data = base64.b64decode(item.blob)
print(f"Binary content: {len(data)} bytes")
print(f"MIME type: {item.mime_type}")
# Save to file
with open("downloaded_logo.png", "wb") as f:
f.write(item.blob)
f.write(data)
```
## Multi-Server Clients

View file

@ -39,30 +39,33 @@ The `fastmcp.json` configuration answers three fundamental questions about your
This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:
`source` is the *where*, `environment` the *what*, and `deployment` the *how*:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
// WHERE: Location of your server code
"type": "filesystem", // Optional, defaults to "filesystem"
"type": "filesystem",
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
// WHAT: Environment setup and dependencies
"type": "uv", // Optional, defaults to "uv"
"type": "uv",
"python": ">=3.10",
"dependencies": ["pandas", "numpy"]
},
"deployment": {
// HOW: Runtime configuration
"transport": "stdio",
"log_level": "INFO"
}
}
```
Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed. Both `type` fields shown above are optional too, defaulting to `"filesystem"` and `"uv"` respectively.
<Warning>
`fastmcp.json` is parsed as strict JSON, so it accepts no comments or trailing commas.
</Warning>
### JSON Schema Support

View file

@ -16,7 +16,7 @@
"dark": "#475569",
"light": "#1e3a5f"
},
"content": "FastMCP 4 is in beta — check out [what's new](/getting-started/whats-new)!"
"content": "FastMCP 4 is in beta — build stateful applications on sessionless MCP. [See what's new](/getting-started/whats-new)."
},
"colors": {
"dark": "#f72585",

View file

@ -115,8 +115,8 @@ FastMCP follows semantic versioning with pragmatic adaptations for the rapidly e
For production use, always pin to exact versions:
```
fastmcp==4.0.0 # Good
fastmcp>=4.0.0 # Bad - may install breaking changes
fastmcp==4.0.0b1 # Good - an exact version
fastmcp>=4.0.0 # Bad - may install breaking changes
```
See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy.

View file

@ -142,9 +142,11 @@ def greet(name: str) -> PrefabApp:
You can preview app tools locally with `fastmcp dev apps my_server.py` — no MCP host required. See the [Apps overview](/apps/overview) for the full guide, including state management, forms, charts, and server-connected interactivity.
## Deploy to Prefect Horizon
## Deploy Your Server
[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
FastMCP HTTP servers run anywhere you can host a Python application. The [HTTP deployment guide](/deployment/http) covers the transport settings and security boundaries for self-managed infrastructure.
For a managed deployment, [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides hosting, authentication, access control, and observability for MCP servers.
<Info>
Horizon is **free for personal projects** and offers enterprise governance for teams.

View file

@ -1,65 +1,112 @@
---
title: "What's New in FastMCP 4"
sidebarTitle: "What's New"
description: A sessionless MCP protocol, the state layer that replaces sessions, and enterprise identity.
description: FastMCP 4 makes stateful MCP applications work on the sessionless protocol while one server serves every protocol era.
icon: sparkles
---
FastMCP 4 runs on version 2 of the MCP Python SDK, which rewrote the protocol layer to support MCP's new sessionless protocol, `2026-07-28`. That protocol drives most of this release. It changes how servers deploy, how clients connect, where state lives between calls, and how a running tool asks the user a question.
FastMCP 4 makes stateful MCP applications work on MCP's sessionless protocol. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions or a continuously connected client.
Most FastMCP 3 servers run on 4 unchanged. Two things need attention: `ctx.sample()` and `ctx.list_roots()` are gone, and code that builds MCP protocol models by hand now uses snake_case field names where the SDK used camelCase. [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers every break in detail.
The protocol changed completely underneath those APIs. Your application usually does not: one FastMCP server negotiates both protocol eras per connection, and most FastMCP 3 servers upgrade unchanged.
That is the theme of version 4: stateless transport without stateless application code. The release also makes protocol extensions a first-class surface, adds enterprise identity for agents acting on behalf of users, and strengthens production defaults across caching, routing, and security.
<Note>
FastMCP 4 is in **beta**. Pin an exact version and expect sharp edges. See [Install the v4 prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
</Note>
## Every protocol era
## Protocol compatibility
A FastMCP 4 server answers clients on both sides of the protocol transition from a single deployment. The SDK negotiates per connection: the sessionless protocol for clients that have moved forward, the session-based handshake for everyone else. You adopt the new protocol without forking your deployment or gating clients by version, which supersedes FastMCP's earlier "latest protocol only" stance.
A protocol migration usually forces a choice between breaking clients that have not moved yet and holding the server back with them. FastMCP 4 serves both eras from one deployment, negotiating the best mutual version for each connection. Modern clients get the sessionless protocol while handshake-era clients continue working unchanged.
Statelessness pays off in how you run the server. A sessionless request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a deployment requirement.
Statelessness changes how that deployment scales. Each modern request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a requirement.
The client default flipped to match. `Client(url)` probes for the modern protocol and adopts it when the server offers it, where every earlier FastMCP version pinned the handshake outright.
The client default follows the same rule. `Client(url)` probes for the modern protocol and falls back to the handshake when necessary. Pin `mode="legacy"` only when your application specifically needs the session back-channel.
```python
from fastmcp import Client
# Probes for the modern protocol, falls back to the handshake
# Negotiate the best mutual protocol
client = Client("https://example.com/mcp")
# Pins the handshake, when you need the session back-channel
# Require the handshake-era protocol
legacy = Client("https://example.com/mcp", mode="legacy")
```
That default is what puts the modern capabilities within reach of ordinary client code: a task-enabled tool hands back a handle to poll, and multi-round-trip elicitation resolves across successive requests, with the caller opting in to neither. Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` read the same whichever era you negotiated, so code that inspects a connection never branches on how it was established. See [Protocol negotiation](/clients/client#protocol-negotiation).
Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` expose the same interface whichever era was negotiated. Application code that inspects a server does not need a protocol-version branch. See [Protocol negotiation](/clients/client#protocol-negotiation).
Intermediaries benefit too. On a modern connection, FastMCP's client attaches the method, the target name, and any opted-in argument values as HTTP headers, so a gateway or load balancer can route a request without parsing its JSON-RPC body. See [Gateway Routing Headers](/deployment/http#gateway-routing-headers).
On modern connections, FastMCP also attaches the method, target name, and opted-in argument values as HTTP headers. Gateways and load balancers can route requests without parsing JSON-RPC bodies. See [Gateway routing headers](/deployment/http#gateway-routing-headers).
## Server-to-client requests
## Stateful applications
A sessionless connection gives the server no channel to push a request down to a connected client mid-execution. Three `Context` methods depended on that channel, and this is the one part of FastMCP 4 likely to break an existing server.
The modern protocol removes transport-level sessions, but applications still need conversations, user state, and long-running work. FastMCP moves those concerns into explicit application primitives that survive fresh connections. Shared stores and request-state keys extend them across replicas and worker restarts.
`ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` are removed. Touching one raises `AttributeError` on every era, so the break surfaces when you upgrade rather than in production against whichever client happens to negotiate the modern protocol.
### Interactive tools
For generation, call an LLM directly from your tool: your server holds the API key, creates a provider client, and awaits a completion inline. That works against every client, including the many that never implemented sampling at all, and a tool that chains several generations pays no round trip for any of them. See [Sampling](/servers/sampling).
Many useful tools need more than one exchange. A booking tool asks for a destination, then a date, then confirmation. A destructive operation asks the user to approve it before continuing.
When borrowing the *caller's* model is the actual point, or when a tool genuinely needs the client's roots, the tool asks by returning a description of what it needs. The round completes normally, the client answers, and it re-issues the call with the answer attached. `ctx.elicit()` is untouched and still works on handshake connections; on modern connections that same return-and-resume shape covers elicitation as well. See [the guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol).
On the modern protocol, the tool returns a description of the input it needs. That result completes the request normally. The client fulfils the request and calls the tool again with the answer attached; the tool runs from the top, reads `ctx.input_responses`, and either asks another question or returns its final result.
Logging and progress are unaffected. Both are notifications, and notifications ride the response stream on every era.
Each request completes while the user responds. Single-process servers use an automatic process-local key to protect the state carried between rounds; load-balanced deployments configure one shared key so any replica can validate and resume the next round:
## Session state
```python
import os
If every request arrives on a fresh connection, a tool that wants to remember something between calls has nowhere to keep it. Weighing protocol-level sessions against statelessness, the MCP working group [chose statelessness](https://github.com/modelcontextprotocol/transports-wg/blob/main/docs/sessions-vs-sessionless-decision.md) and moved session semantics up to the application: the server hands out an identifier, and the client passes it back.
from fastmcp import Context, FastMCP
from mcp.server.request_state import RequestStateSecurity
from mcp.types import ElicitRequest, ElicitRequestFormParams, InputRequiredResult
FastMCP implements that pattern and adds the isolation a bare handle lacks. State is stored server-side and keyed to the authenticated user, so a handle is inert in anyone else's hands.
mcp = FastMCP(
"Booking",
request_state_security=RequestStateSecurity(
keys=[os.environ["REQUEST_STATE_KEY"].encode()]
),
)
Most tools want a single bucket per user. Declare a `UserSession` parameter and FastMCP injects it the way it injects `Context`: it never appears in the tool's input schema, and the caller passes nothing, because the user's identity selects the right bucket.
@mcp.tool
async def book_flight(ctx: Context) -> str | InputRequiredResult:
answers = ctx.input_responses
if answers is None:
params = ElicitRequestFormParams(
message="Where would you like to fly?",
requested_schema={
"type": "object",
"properties": {"destination": {"type": "string"}},
"required": ["destination"],
},
)
return InputRequiredResult(
result_type="input_required",
input_requests={
"destination": ElicitRequest(
method="elicitation/create",
params=params,
)
},
)
response = answers["destination"]
if response.action != "accept" or response.content is None:
return "Booking cancelled."
destination = response.content["destination"]
return f"Booked a flight to {destination}."
```
Every replica must receive the same `REQUEST_STATE_KEY`, containing at least 32 bytes of secret key material. A FastMCP client drives the loop through its existing elicitation handler, so client code receives the terminal result without managing the intermediate rounds. See [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol).
### Session state
Application state follows the same explicit model. FastMCP stores state server-side and binds it to the authenticated user, so a session handle is inert in another user's hands.
Most tools want one state bucket per user. Declare a `UserSession` parameter and FastMCP injects it like `Context`: it never appears in the tool schema, and the caller passes nothing because their authenticated identity selects the bucket.
```python
from fastmcp import FastMCP
from fastmcp.server.sessions import UserSession
mcp = FastMCP("assistant")
mcp = FastMCP("Assistant")
@mcp.tool
@ -70,18 +117,19 @@ async def remember(fact: str, session: UserSession) -> str:
return f"Remembered {len(facts)} facts."
```
Because the bucket is chosen from the caller's identity, `UserSession` requires [authentication](/servers/auth/authentication). An unauthenticated request has no user to key on, so the tool raises rather than guessing at a bucket.
`UserSession` requires [authentication](/servers/auth/authentication), since an unauthenticated request has no user to key on. When one user needs several independent buckets, such as separate carts or conversations, `SessionId` exposes the handle as an explicit string argument.
When one user needs several independent buckets, such as separate carts or parallel conversations, `SessionId` makes the handle an explicit string argument that the agent obtains from `create_session` and supplies on each call. See [Session State](/servers/sessions).
The default in-memory state store is process-local. To preserve state across restarts or share it among replicas, pass a shared persistent `session_state_store`. See [Session state](/servers/sessions).
## Background tasks
### Background work
Long-running work runs as a background task: the server accepts the call and returns a handle immediately, and the client polls for the result while the work proceeds. Tasks left the core MCP spec during the SDK rewrite and returned as the `io.modelcontextprotocol/tasks` extension, which FastMCP implements end to end in the optional `fastmcp-tasks` package.
Long-running tools create a different kind of state problem: holding a request open for several minutes invites timeouts and leaves the user unable to tell whether work is progressing. Background tasks accept the call and return a handle immediately, then let the client poll while work proceeds asynchronously.
`@mcp.tool(task=True)` remains the authoring surface and [Docket](https://github.com/chrisguidry/docket) still provides the durable execution engine, so the wire protocol modernizing underneath costs you no code change. What's new is the registration: tasks arrive as an extension you add to the server.
FastMCP implements the `io.modelcontextprotocol/tasks` extension in the optional `fastmcp-tasks` package. The authoring API remains `@mcp.tool(task=True)`, backed by [Docket](https://github.com/chrisguidry/docket):
```python
import asyncio
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
@ -91,24 +139,28 @@ mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""A long-running operation."""
"""Run a long computation."""
await asyncio.sleep(duration)
return f"Completed in {duration} seconds"
```
A FastMCP client handles the handle-and-poll cycle transparently, so `client.call_tool(...)` looks the same whether or not the call ran in the background. See [Background Tasks](/servers/tasks).
`fastmcp.Client` handles the task handle and polling cycle, so `client.call_tool(...)` returns the same way whether the tool ran inline or in the background. See [Background tasks](/servers/tasks).
## Server extensions
`TasksExtension()` uses an in-memory, single-process backend by default. Configure a Redis or Valkey backend for durable work that survives restarts and runs across separate workers.
Background tasks are the first capability built on a more general one. An MCP extension is a protocol feature named by a reverse-DNS string and negotiated as a capability, and FastMCP 4 makes extensions a first-class surface rather than something only the framework can add.
## Extensible protocol
`FastMCP.add_extension()` lets an extension advertise a capability, add request methods, intercept `tools/call`, and run a lifespan hook, all with full access to the component registry, `Context`, and auth. The same extensions flow through the client with `Client(extensions=...)`. A cross-cutting protocol feature becomes a supported plugin instead of surgery on core, and `TasksExtension` is the worked example of everything the interface allows. See [Server Extensions](/servers/extensions).
Background tasks are built on a general extension surface. An MCP extension advertises a capability under a reverse-DNS identifier and can add behavior negotiated between a server and client.
## Argument completion
### Server extensions
When a client offers autocomplete for a prompt argument or a resource-template parameter, it asks the server which values fit, narrowing the list as the user types. FastMCP 4 lets a server answer. A single `@mcp.completion` handler receives the reference being completed, the argument and its partial value, and the arguments the user has already supplied, and returns the candidates the client surfaces as suggestions.
`FastMCP.add_extension()` lets an extension advertise capabilities, add request methods, intercept `tools/call`, and own lifespan behavior with access to the component registry, `Context`, and authentication. Client extensions use the matching `Client(extensions=...)` interface.
Because the handler sees the earlier arguments, completions can depend on them: a `repo` parameter can suggest only the repositories under the `owner` already chosen.
Cross-cutting protocol behavior can therefore live in a supported plugin instead of requiring changes to FastMCP core. `TasksExtension` is a complete example of the interface. See [Server extensions](/servers/extensions).
### Argument completion
FastMCP 4 also lets servers answer MCP argument-completion requests. A completion handler sees the prompt or resource-template argument, its partial value, and values already supplied, so suggestions can depend on earlier choices.
```python
from fastmcp import FastMCP
@ -126,74 +178,40 @@ def write_poem(theme: str) -> str:
def complete(ref, argument, context):
if isinstance(ref, PromptReference) and argument.name == "theme":
options = ["nature", "love", "adventure"]
return [o for o in options if o.startswith(argument.value)]
return [option for option in options if option.startswith(argument.value)]
return None
```
Registering a handler advertises the completions capability during negotiation, so a client only sends requests to a server that answers them, identically on both protocol eras. See [Argument Completion](/servers/completions).
Registering the handler advertises the completion capability during negotiation, so clients only send requests to servers that support them. See [Argument completion](/servers/completions).
## Enterprise identity
FastMCP 4 ships a complete server-side implementation of identity assertion, the enterprise "on-behalf-of" flow: a corporate identity provider issues a signed assertion, the user's agent presents it, and the server mints a short-lived token, with no browser login and no per-user consent screen. Behind one parameter on the existing auth providers, FastMCP performs the signature verification, binding checks, replay rejection, and scoped token issuance.
Interactive OAuth authorization assumes a person can complete a browser flow. Internal agents often act for employees without a person waiting at a keyboard, while the server still needs the employee's identity for authorization and audit.
Identity assertion carries that identity through the agent. A corporate identity provider signs an assertion, the agent presents it, and the server exchanges it for a short-lived token without an interactive login or consent screen. FastMCP performs signature verification, binding checks, replay rejection, and scoped token issuance through the authentication providers you already use.
```python
from fastmcp import FastMCP
from fastmcp.server.auth import IdentityAssertion, OAuthProxy
auth = OAuthProxy(
# existing upstream configuration unchanged
identity_assertion=IdentityAssertion(trusted_issuers=["https://login.acme-corp.com"]),
# Existing upstream configuration
identity_assertion=IdentityAssertion(
trusted_issuers=["https://login.acme-corp.com"]
),
)
mcp = FastMCP("Internal API", auth=auth)
```
The asserted subject flows into the normal auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
The asserted subject enters the normal authentication context, so tools read it through `get_access_token()` like any other identity. See [Identity assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
Authorizing a caller by role is a related, provider-agnostic need. Scopes are standardized, so `require_scopes` behaves the same everywhere, but roles and groups are not part of OIDC and every provider files them under a different claim. `require_roles` handles the comparison and takes an `extract` callable naming where to look, so Keycloak's `realm_access.roles`, Cognito's `cognito:groups`, and Auth0's namespaced claims all work without FastMCP guessing.
Authorization gained a provider-neutral role check as well. `require_roles` accepts an extraction function for providers that store roles and groups under different claims, while [scope step-up challenges](/servers/authorization#signaling-scope-shortfalls) tell a client exactly which scopes to request.
```python
from fastmcp import FastMCP
from fastmcp.server.auth import require_roles
For clients with no user behind them, such as backend services and scheduled jobs, `ClientCredentialsOAuthProvider` implements the OAuth 2.0 client-credentials grant with no browser or redirect. See [Machine-to-machine authentication](/clients/auth/client-credentials).
mcp = FastMCP("Internal API")
## Production defaults
@mcp.tool(auth=require_roles("admin", extract=lambda c: c["realm_access"]["roles"]))
def rotate_credentials() -> str:
"""Only callable by a caller holding the 'admin' role."""
return "Rotated"
```
That example shows the check in isolation. Enforcing it for real needs an HTTP-transport server with a token-validating `auth` provider configured, since STDIO has no OAuth concept and skips every check. A `JWTVerifier`, a `RemoteAuthProvider`, or any provider built on one such as `KeycloakAuthProvider` all expose claims directly. See [Authorization](/servers/authorization#require_roles).
The client side arrived too. Plenty of FastMCP clients have no user behind them, such as a backend service, a scheduled job, or one MCP server calling another. `ClientCredentialsOAuthProvider` authenticates one of those to a protected server with the OAuth 2.0 client-credentials grant: no browser, no redirect, no consent screen.
```python
import asyncio
from fastmcp import Client
from fastmcp.client.auth import ClientCredentialsOAuthProvider
auth = ClientCredentialsOAuthProvider(
client_id="my-client-id",
client_secret="my-client-secret",
scopes=["read", "write"],
)
async def main():
async with Client("https://example.com/mcp", auth=auth) as client:
await client.list_tools()
asyncio.run(main())
```
See [Machine-to-Machine Authentication](/clients/auth/client-credentials).
## Response caching
A server can stamp freshness hints on its results, and a caching client reuses a result within that window instead of making the round trip. Set the defaults on the server and every response carries them.
A server can now attach freshness hints to its results, and a caching client can reuse those results without another round trip. Set a default time-to-live and scope on the server:
```python
from fastmcp import FastMCP
@ -201,10 +219,18 @@ from fastmcp import FastMCP
mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public")
```
Backing the client's cache with the distributed `KeyValueResponseCacheStore` puts it in Redis or any key-value store, so a fleet of clients or proxy replicas shares fills rather than each paying for its own. See [Response caching](/clients/client#response-caching).
`KeyValueResponseCacheStore` can place the client cache in Redis or another key-value store so a fleet of clients or proxies shares fills. See [Response caching](/clients/client#response-caching).
## Security defaults
Resource templates now reject path traversal, absolute paths, and null bytes in their parameters before the handler runs. The protection is enabled by default and applies to mounted and proxied templates. See [Path security](/servers/resources#path-security).
Templated resources now screen their parameters for path traversal, absolute paths, and null bytes before the handler runs. This is on by default and covers mounted and proxied templates too, so a template that interpolates a parameter into a filesystem path no longer has to validate it by hand. See [path security](/servers/resources#path-security).
OAuth defaults also distinguish native clients from web applications during Dynamic Client Registration, and missing scopes now produce an `InsufficientScopeError` that names the scopes required to continue. See [Application type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [scope shortfalls](/servers/authorization#signaling-scope-shortfalls).
The OAuth flow got more precise in two places. Dynamic Client Registration honors a client's declared `application_type`: the permissive loopback and app-scheme callbacks that MCP clients rely on stay the default for `"native"`, while a client registering as `"web"` is held to stricter browser-app redirect rules. And when `AuthMiddleware` denies a call specifically for a missing scope, it raises `InsufficientScopeError` naming which scopes would fix it, so a caller re-authorizes precisely instead of retrying blind. See [Application Type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [Signaling Scope Shortfalls](/servers/authorization#signaling-scope-shortfalls).
## Upgrade note
The sessionless protocol has no live connection for a server to call back into during execution. FastMCP 4 therefore removes `ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` from every protocol era so incompatible code fails immediately during an upgrade.
For generation, call an LLM directly from the server when your application owns the model. When borrowing the caller's model is the point, return an `InputRequiredResult` carrying a sampling request and read the answer on the next round. Roots use the same return-and-resume pattern. See [Sampling](/servers/sampling) and [the guard pattern](/servers/elicitation#sampling-and-roots).
`ctx.elicit()` remains available on handshake-era connections; modern connections use the multi-round pattern described above. Code that constructs MCP protocol models directly must also use snake_case Python field names with SDK v2.
[Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers these changes and every other compatibility break.

View file

@ -69,9 +69,11 @@ You'll also need to authenticate with Anthropic. You can do this by setting the
export ANTHROPIC_API_KEY="your-api-key"
```
Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.**
Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment.
```python {5, 13-22}
The connector is in beta, so the call goes through `client.beta.messages` with the `mcp-client-2025-11-20` flag. Each entry in `mcp_servers` also needs a matching `mcp_toolset` entry in `tools` that references it by name; declaring the server without the toolset is rejected as a validation error.
```python {5, 14-23}
import anthropic
from rich import print
@ -81,8 +83,9 @@ url = 'https://your-server-url.com'
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-4-20250514",
model="claude-sonnet-5",
max_tokens=1000,
betas=["mcp-client-2025-11-20"],
messages=[{"role": "user", "content": "Roll a few dice!"}],
mcp_servers=[
{
@ -91,9 +94,7 @@ response = client.beta.messages.create(
"name": "dice-server",
}
],
extra_headers={
"anthropic-beta": "mcp-client-2025-04-04"
}
tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}],
)
print(response.content)
@ -193,7 +194,7 @@ Error code: 400 - {
To authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration:
```python {8, 21}
```python {8, 22}
import anthropic
from rich import print
@ -206,8 +207,9 @@ access_token = 'your-access-token'
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-4-20250514",
model="claude-sonnet-5",
max_tokens=1000,
betas=["mcp-client-2025-11-20"],
messages=[{"role": "user", "content": "Roll a few dice!"}],
mcp_servers=[
{
@ -217,9 +219,7 @@ response = client.beta.messages.create(
"authorization_token": access_token
}
],
extra_headers={
"anthropic-beta": "mcp-client-2025-04-04"
}
tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}],
)
print(response.content)

View file

@ -69,7 +69,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider
# The GitHubProvider handles GitHub's token format and validation
auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID
client_secret="github_pat_...", # Your GitHub OAuth App Client Secret
client_secret="your-github-client-secret", # Your GitHub OAuth App Client Secret
base_url="http://localhost:8000", # Must match your OAuth App configuration
# redirect_path="/auth/callback" # Default value, customize if needed
)
@ -151,7 +151,7 @@ from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN",
client_secret="github_pat_...",
client_secret="your-github-client-secret",
base_url="https://your-production-domain.com",
# Production token management

View file

@ -70,7 +70,7 @@ An object containing environment variables to set when launching the server. All
This format is widely adopted across the MCP ecosystem:
- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json`
- **Claude Desktop**: Uses `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows
- **Cursor**: Uses `~/.cursor/mcp.json`
- **VS Code**: Uses workspace `.vscode/mcp.json`
- **Other clients**: Many MCP-compatible applications follow this standard
@ -457,7 +457,7 @@ The generated configuration works with any MCP-compatible application:
<Note>
**Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.
</Note>
Copy the `mcpServers` object into `~/.claude/claude_desktop_config.json`
Copy the `mcpServers` object into Claude Desktop's config file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows)
### Cursor
<Note>

View file

@ -300,10 +300,12 @@ For advanced configuration options and custom middleware extensions, see [Advanc
See the [example server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/example.py) for a full implementation with JWT-based authentication. For additional examples and usage patterns, see [Example Server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/):
```python
import os
import datetime
import jwt
from fastmcp import FastMCP, Context
from permit_fastmcp.middleware.middleware import PermitMcpMiddleware
import jwt
import datetime
# Configure JWT identity extraction
os.environ["PERMIT_MCP_IDENTITY_MODE"] = "jwt"

View file

@ -283,10 +283,11 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
<ParamField body="jwt_signing_key" type="str | bytes | None">
<VersionBadge version="2.13.0" />
Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.
Secret used to sign FastMCP JWT tokens issued to clients. How the key is derived depends on what you pass:
**Default behavior (`None`):**
Derives a 32-byte key using PBKDF2 from the upstream client secret.
- **`bytes`** are used as-is, with no stretching, so supply at least 32 bytes of high-entropy key material. With the default file-backed client storage, the bytes must also decode as UTF-8; use `secrets.token_urlsafe(32).encode()` instead of raw `secrets.token_bytes()`, or configure `client_storage` explicitly.
- **A string** is stretched into a 32-byte key with PBKDF2 (1,000,000 iterations), since a supplied string may be low-entropy. Strings shorter than 12 characters also log a warning.
- **`None`** (the default) derives a 32-byte key from the upstream client secret using HKDF.
**For production:**
Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the key derived from the upstream client secret. This allows you to manage keys securely in cloud environments, allows keys to work across multiple instances, and allows you to rotate keys without losing client registrations.

View file

@ -155,7 +155,7 @@ Set this if your provider requires a specific authentication method and the defa
<ParamField body="jwt_signing_key" type="str | bytes | None">
<VersionBadge version="2.13.0" />
Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.
Secret used to sign FastMCP JWT tokens issued to clients. **`bytes`** are used as-is, with no stretching, so supply at least 32 bytes of high-entropy key material. With the default file-backed client storage, the bytes must also decode as UTF-8; use `secrets.token_urlsafe(32).encode()` instead of raw `secrets.token_bytes()`, or configure `client_storage` explicitly. **A string** is stretched into a 32-byte key with PBKDF2 (1,000,000 iterations), since a supplied string may be low-entropy.
**Default behavior (`None`):**
The key is deterministically derived from `client_secret` using HKDF, on every platform. Because the derivation is deterministic, the same key is produced across restarts as long as `client_secret` doesn't change, so tokens remain valid without any extra configuration. This convenience makes it **only** suitable for development and local testing.

View file

@ -116,8 +116,6 @@ auth = RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
base_url="https://api.yourcompany.com", # Your server base URL
# Optional: restrict allowed client redirect URIs
allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"]
)
mcp = FastMCP(name="Company API", auth=auth)
@ -216,13 +214,7 @@ WorkOS's support for Dynamic Client Registration makes it particularly well-suit
## Client Redirect URI Security
<Note>
`RemoteAuthProvider` also supports the `allowed_client_redirect_uris` parameter for controlling which redirect URIs are accepted from MCP clients during DCR:
- `None` (default): Broad DCR-compatible redirect support, while rejecting unsafe browser schemes such as `javascript:`, `data:`, `file:`, and `vbscript:`
- Custom list: Specify allowed patterns with wildcard support
- Empty list `[]`: No redirect URIs allowed
This provides defense-in-depth even though DCR providers typically validate redirect URIs themselves.
Redirect URIs are validated by the DCR provider itself, since it owns the registration flow. To constrain them from the FastMCP side, use [`OAuthProxy`](/servers/auth/oauth-proxy), whose `allowed_client_redirect_uris` parameter accepts a list of allowed patterns with wildcard support.
</Note>
## Implementation Considerations

View file

@ -378,7 +378,7 @@ mcp.add_middleware(LoggingMiddleware(
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_payloads` | `bool` | `False` | Log request/response content |
| `max_payload_length` | `int` | `500` | Truncate payloads beyond this length |
| `max_payload_length` | `int` | `1000` | Truncate payloads beyond this length |
| `logger` | `Logger` | module logger | Custom logger instance |
### Timing
@ -533,7 +533,7 @@ mcp.add_middleware(ErrorHandlingMiddleware(
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_traceback` | `bool` | `False` | Include stack traces in logs |
| `transform_errors` | `bool` | `False` | Convert exceptions to MCP errors |
| `transform_errors` | `bool` | `True` | Convert exceptions to MCP errors |
| `error_callback` | `Callable` | `None` | Custom callback on errors |
For automatic retries:

View file

@ -448,14 +448,14 @@ A prompt can ask the client for information before it renders. On an MCP 2026-07
<VersionBadge version="2.1.0" />
You can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization.
You can configure how the FastMCP server handles attempts to register the same prompt twice. Identity is the component's type, name, and version together, so a prompt may share a name with a tool, and two versions of one prompt coexist. The `on_duplicate` setting covers every component type, so it applies to prompts alongside tools and resources.
```python
from fastmcp import FastMCP
mcp = FastMCP(
name="PromptServer",
on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
on_duplicate="error" # Raise an error on an exact duplicate
)
@mcp.prompt

View file

@ -78,7 +78,7 @@ mcp = FastMCP(name="DataServer")
)
def get_application_status() -> str:
"""Internal function description (ignored if description is provided above)."""
return json.dumps({"status": "ok", "uptime": 12345, "version": mcp.settings.version})
return json.dumps({"status": "ok", "uptime": 12345, "version": "2.1"})
```
<Card icon="code" title="@resource Decorator Arguments">
@ -793,14 +793,14 @@ A resource or resource template can ask the client for information before it pro
<VersionBadge version="2.1.0" />
You can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization.
You can configure how the FastMCP server handles attempts to register the same resource or template twice. Identity is the component's type, URI, and version together, so two versions of one resource coexist and only an exact repeat collides. The `on_duplicate` setting covers every component type, so it applies to resources and templates alongside tools and prompts.
```python
from fastmcp import FastMCP
mcp = FastMCP(
name="ResourceServer",
on_duplicate_resources="error" # Raise error on duplicates
on_duplicate="error" # Raise an error on an exact duplicate
)
@mcp.resource("data://config")

View file

@ -1081,22 +1081,22 @@ For full documentation on the Context object and all its capabilities, see the [
<VersionBadge version="2.1.0" />
You can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance.
You can control how the FastMCP server behaves if you register the same component twice. Identity is the component's type, name, and version together, so a tool and a prompt may share a name, and two versions of one tool coexist. Only an exact repeat of all three counts as a duplicate. The `on_duplicate` argument sets that policy once for every component type.
```python
from fastmcp import FastMCP
mcp = FastMCP(
name="StrictServer",
# Configure behavior for duplicate tool names
on_duplicate_tools="error"
# Configure behavior for exact component duplicates
on_duplicate="error"
)
@mcp.tool
def my_tool(): return "Version 1"
# This will now raise a ValueError because 'my_tool' already exists
# and on_duplicate_tools is set to "error".
# and on_duplicate is set to "error".
# @mcp.tool
# def my_tool(): return "Version 2"
```

View file

@ -21,7 +21,7 @@ The answer lies in **standardization**. The AI ecosystem is fragmented. Every mo
1. **Interoperability:** Build one MCP server, and it can be used by any MCP-compliant client (Claude, Gemini, OpenAI, custom agents, etc.) without custom integration code. This is the protocol's most important promise.
2. **Discoverability:** Clients can dynamically ask a server what it's capable of at runtime. They receive a structured, machine-readable "menu" of tools and resources.
3. **Security & Safety:** MCP provides a clear, sandboxed boundary. An LLM can't execute arbitrary code on your server; it can only *request* to run the specific, typed, and validated functions you explicitly expose.
3. **Explicit boundaries:** MCP gives hosts and servers a typed inventory of the capabilities they expose. That creates a clear place to apply authorization, user confirmation, input validation, and sandboxing; the protocol defines the interface, while your application supplies those security policies.
4. **Composability:** You can build small, specialized MCP servers and combine them to create powerful, complex applications.
## Core MCP Components
@ -111,10 +111,6 @@ def summarize_text(text_to_summarize: str) -> str:
## Advanced Capabilities
Beyond the core components, MCP also supports more advanced interaction patterns, such as a server requesting that the *client's* LLM generate a completion (known as **sampling**), or a server sending asynchronous **notifications** to a client. These features enable more complex, bidirectional workflows and are fully supported by FastMCP.
Beyond tools, resources, and prompts, MCP supports richer interaction patterns such as notifications, progress updates, user elicitation, and argument completion. Extensions add capabilities such as durable background tasks.
## Next Steps
Now that you understand the core concepts of the Model Context Protocol, you're ready to start building. The best place to begin is our step-by-step tutorial.
[**Tutorial: How to Create an MCP Server in Python →**](/tutorials/create-mcp-server)
FastMCP exposes these patterns through typed Python APIs. For example, [elicitation](/servers/elicitation) lets tools request missing information or confirmation, while [background tasks](/servers/tasks) let long-running work continue after the original request returns.

View file

@ -11,11 +11,13 @@ title="FastMCP v4.0.0b1: Fourgone Conclusion"
href="https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1"
cta="Read the release notes"
>
FastMCP 4 rebuilds the framework on the MCP Python SDK v2, and this beta is the first release to run on the SDK's stable 2.0. The engine underneath changed completely, but FastMCP absorbs nearly all of it — most FastMCP 3 servers run untouched.
FastMCP 4 makes stateful MCP applications work on the sessionless protocol while one deployment continues serving handshake-era clients. The engine underneath changed completely, but FastMCP absorbs nearly all of it — most FastMCP 3 servers upgrade untouched.
🌐 **Every protocol era** — one server answers both the sessionless `2026-07-28` protocol and the older session-based handshake, negotiated per connection.
💾 **State without a session** — `UserSession` and `SessionId` give tools durable state on a protocol that deliberately has none, keyed per user when the request is authenticated.
💬 **Interactive tools** — tools ask follow-up questions across complete request-response rounds, with shared request-state keys for load balancing and worker restarts.
💾 **State without a session** — `UserSession` and `SessionId` give tools explicit server-side state on a protocol that deliberately has none, keyed per user when the request is authenticated.
⏳ **Background tasks** — the `io.modelcontextprotocol/tasks` extension in the new `fastmcp-tasks` package, on the same Docket engine FastMCP 3 used.