mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Improve the v4 docs (#4707)
This commit is contained in:
parent
7339936980
commit
0792ac812c
25 changed files with 218 additions and 163 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue