diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx index 7f9945854..29d104d4e 100644 --- a/docs/deployment/http.mdx +++ b/docs/deployment/http.mdx @@ -151,6 +151,8 @@ Use `host_origin_protection="auto"` to protect localhost-bound direct servers wh ### Gateway Routing Headers + + A gateway, load balancer, or reverse proxy in front of your MCP server often needs to route a request before it reads the JSON-RPC body — the body may be an SSE stream, or the gateway may simply want to avoid parsing it. On a connection that negotiates the modern `2026-07-28` protocol, Streamable HTTP clients built on the MCP Python SDK (including FastMCP's own client) attach routing information to each request as HTTP headers so an intermediary can dispatch on headers alone: - `Mcp-Method` carries the JSON-RPC method (for example `tools/call`) on every request. diff --git a/docs/getting-started/whats-new.mdx b/docs/getting-started/whats-new.mdx index 84456dc27..1bb4ce45f 100644 --- a/docs/getting-started/whats-new.mdx +++ b/docs/getting-started/whats-new.mdx @@ -23,7 +23,7 @@ The rebuild also pulls the protocol's recent evolution forward in a single step. A FastMCP 4 server answers clients across the protocol transition from one deployment. The MCP SDK negotiates the era per connection — the sessionless `2026-07-28` protocol for clients that have moved forward, the session-based handshake for everyone else — and any replica behind a plain load balancer can serve a modern request. This supersedes FastMCP's earlier "latest protocol only" stance: you adopt the new protocol without forking your deployment or gating clients by version. -The same negotiation runs from the client, and its default flipped. A plain `Client(url)` now probes for the modern protocol and adopts it when the server offers it, falling back to the handshake otherwise — where every earlier FastMCP version pinned the handshake outright. That flip is what brings 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, neither requiring the caller to opt in. Set `mode="legacy"` to pin the handshake when you need the session-based back-channel or the classic `initialize` result. See [Protocol negotiation](/clients/client#protocol-negotiation). +The same negotiation runs from the client, and its default flipped. A plain `Client(url)` now probes for the modern protocol and adopts it when the server offers it, falling back to the handshake otherwise — where every earlier FastMCP version pinned the handshake outright. That flip is what brings 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, neither requiring the caller to opt in. Set `mode="legacy"` to pin the handshake when you need the session-based back-channel or the classic `initialize` result. Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` read the same regardless of which era you negotiated — code that inspects the connection no longer branches on how it got there. See [Protocol negotiation](/clients/client#protocol-negotiation). The modern protocol is sessionless, so it drops the server's ability to call back into the client mid-request (SEP-2577), and FastMCP 4's server API reflects that. `ctx.elicit` moves to a request-shaped pattern that works on modern connections: the tool returns a description of the input it needs, and the client answers with a fresh call. `ctx.sample`, `ctx.sample_step`, and `ctx.list_roots` are gone from the API, because each of them pushed a request down a live connection and a method that only works against old clients is a trap. @@ -102,6 +102,31 @@ def rotate_credentials() -> str: This illustrates the check in isolation — enforcing it for real needs an HTTP-transport server with a token-validating `auth` provider configured (a `JWTVerifier`, a `RemoteAuthProvider`, or a provider built on one, such as `KeycloakAuthProvider`, all expose claims directly), since STDIO has no OAuth concept and skips every check. See [Authorization](/servers/authorization#require_roles) for the full picture. +The client side of enterprise auth arrived too. Not every FastMCP client has a user behind it — a backend service, a scheduled job, one MCP server calling another — and `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). + ## Faster and safer Two more capabilities arrive by default. Response caching (SEP-2549) lets a server stamp freshness hints on its results that a caching [client](/clients/client#response-caching) reuses without a round trip, and a distributed `KeyValueResponseCacheStore` backs that cache with Redis or any key-value store, so a fleet of clients or proxy replicas shares fills. @@ -114,4 +139,8 @@ mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public") Security tightened in the same release: every templated resource screens its parameters for path traversal, absolute paths, and null bytes before the handler runs — [path security](/servers/resources#path-security) on by default, covering mounted and proxied templates too. +The OAuth flow got more precise as well. Dynamic Client Registration now honors a client's declared `application_type` (SEP-837): the permissive loopback and app-scheme callbacks MCP clients rely on stay the default for `"native"`, while a client that registers 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 exactly which scopes would fix it (SEP-2350), 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). + +A gateway or load balancer in front of your server can now route a request without parsing its JSON-RPC body: on a modern connection, FastMCP's client attaches the method, target name, and opted-in argument values as HTTP headers (SEP-2243), so an intermediary dispatches on headers alone. See [Gateway Routing Headers](/deployment/http#gateway-routing-headers). + When you're ready to move a server to v4, [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) walks through every change and what it looks like in practice. diff --git a/docs/integrations/auth0.mdx b/docs/integrations/auth0.mdx index 7047ca850..ce2dcc670 100644 --- a/docs/integrations/auth0.mdx +++ b/docs/integrations/auth0.mdx @@ -16,7 +16,7 @@ FastMCP supports two Auth0 integration paths: ## Auth for MCP (DCR) - + This path uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern. Auth0 acts as the authorization server; FastMCP is the resource server. diff --git a/docs/integrations/descope.mdx b/docs/integrations/descope.mdx index f0357f333..fbe1c2b22 100644 --- a/docs/integrations/descope.mdx +++ b/docs/integrations/descope.mdx @@ -83,6 +83,8 @@ mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider) ### Scope discovery and validation + + When both `scopes_supported` and `required_scopes` are omitted, `DescopeProvider` discovers `scopes_supported` lazily from the OpenID configuration and advertises them to MCP clients. Provider construction remains network-free, and a transient discovery failure is retried on a later metadata request. Set both options when clients should request a broader set of scopes than the server requires on every token: diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx index 101139ca4..0ca8b524c 100644 --- a/docs/servers/auth/oauth-proxy.mdx +++ b/docs/servers/auth/oauth-proxy.mdx @@ -589,6 +589,8 @@ Check your server logs for "Client registered with redirect_uri" messages to ide ### Application Type (Web vs. Native) + + During Dynamic Client Registration, a client may declare an `application_type` (per RFC 7591 and SEP-837) that governs which redirect URIs it is allowed to use. The OAuth proxy honors this field both at registration and when authorizing a redirect. `application_type` defaults to `"native"` because MCP clients typically run locally and register loopback callbacks. Clients that omit the field keep the permissive behavior described above. A client that explicitly registers as `"web"` is held to the stricter browser-app rules. diff --git a/docs/servers/authorization.mdx b/docs/servers/authorization.mdx index 68bac9181..1ba9a3b0e 100644 --- a/docs/servers/authorization.mdx +++ b/docs/servers/authorization.mdx @@ -368,6 +368,8 @@ def read_record(id: str) -> str: ### Signaling Scope Shortfalls + + A denial is more useful when it says what would fix it. When `AuthMiddleware` blocks a call because the token is missing scopes — rather than because some other policy rejected it — it raises `InsufficientScopeError`, which carries the specific scopes the caller needs in its `required_scopes` attribute. An agent that reads the error knows exactly which scopes to re-authorize for, instead of retrying blindly against an opaque refusal. `InsufficientScopeError` subclasses `AuthorizationError`, so existing handlers that catch `AuthorizationError` keep catching it and nothing about your error handling has to change to adopt this. diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx index 5ec71d487..2be18cefd 100644 --- a/docs/servers/providers/proxy.mdx +++ b/docs/servers/providers/proxy.mdx @@ -172,6 +172,8 @@ backend = ProxyClient( ### Tool Results Are Relayed, Not Inspected + + A proxy passes a backend's tool results through untouched, including results that don't match the output schema the backend advertised. Deciding whether a server honored its own contract belongs to the client consuming the result, and that client validates for itself. This matters when a backend's declared schema is subtly wrong — an enum missing a variant it actually returns, say. A proxy that enforced the schema would replace the backend's working response with an error of its own, and the client would never see what the backend actually said. diff --git a/docs/servers/telemetry.mdx b/docs/servers/telemetry.mdx index e902fe5a6..2d3a96c30 100644 --- a/docs/servers/telemetry.mdx +++ b/docs/servers/telemetry.mdx @@ -6,6 +6,8 @@ icon: chart-line tag: NEW --- +import { VersionBadge } from "/snippets/version-badge.mdx" + FastMCP includes native OpenTelemetry instrumentation for observability. Traces are automatically generated for tool, prompt, resource, resource template, and task management operations, providing visibility into server behavior, request handling, and provider delegation chains. ## How It Works @@ -21,6 +23,8 @@ Because FastMCP only depends on the OpenTelemetry API, span creation is a no-op ### Turning Telemetry Off + + To disable FastMCP's instrumentation entirely, set `FASTMCP_ENABLE_TELEMETRY=false` (or `fastmcp.settings.enable_telemetry = False`). When disabled, FastMCP creates no spans even if an SDK is configured. ## Enabling Telemetry