From d08c9d07eefe0971d3e0827ceead2122659db247 Mon Sep 17 00:00:00 2001 From: Jake Kaplan Date: Wed, 5 Aug 2026 20:33:07 -0400 Subject: [PATCH] Tighten proxy metadata docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit đŸ¤– Generated with OpenAI Codex --- docs/servers/middleware.mdx | 23 +++++------------------ docs/servers/providers/proxy.mdx | 23 +++++------------------ 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx index ffdd1ae3c..951c14b06 100644 --- a/docs/servers/middleware.mdx +++ b/docs/servers/middleware.mdx @@ -312,28 +312,15 @@ Rejection works only **before** `call_next()`. Raising `McpError` afterward logs #### on_discover -Called when a modern client negotiates through `server/discover`. Like `on_initialize`, this hook receives the typed request and can safely inspect or transform the typed `DiscoverResult` returned by `call_next()`. +Called when a modern client negotiates through `server/discover`. The returned `DiscoverResult` can be modified before it is sent to the client. ```python -import mcp_types - -from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext - - -class DiscoveryMiddleware(Middleware): - async def on_discover( - self, - context: MiddlewareContext[mcp_types.DiscoverRequest], - call_next: CallNext[ - mcp_types.DiscoverRequest, - mcp_types.DiscoverResult, - ], - ) -> mcp_types.DiscoverResult: - result = await call_next(context) - return result.model_copy(update={"instructions": "Custom instructions"}) +async def on_discover(self, context, call_next): + result = await call_next(context) + return result.model_copy(update={"instructions": "Custom instructions"}) ``` -**Returns:** `DiscoverResult` — the transformed value is serialized to the client. Fields such as `supported_versions`, `capabilities`, `ttl_ms`, and `cache_scope` describe the server's public behavior, so middleware should only change them when it also changes that behavior. +Fields such as `supported_versions`, `capabilities`, and cache policy should only be changed when the server's public behavior also changes. ### Raw Handler diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx index 74c94410e..a2eb7c0bc 100644 --- a/docs/servers/providers/proxy.mdx +++ b/docs/servers/providers/proxy.mdx @@ -388,37 +388,24 @@ Only reuse sessions when you know the backend is stateless (e.g. stateless HTTP) ## Advanced Usage -### Controlled Gateways with Negotiation Metadata +### Forwarding Negotiation Metadata -`ProxyProvider` deliberately owns only remote components. To also forward optional server negotiation metadata, add `ProxyNegotiationMetadataMiddleware` explicitly. This is useful for gateways that need transforms, local components, or other middleware without adopting all of `FastMCPProxy`: +Add `ProxyNegotiationMetadataMiddleware` when a gateway built with `ProxyProvider` should also expose backend instructions and namespaced `_meta`: ```python from fastmcp import FastMCP from fastmcp.server.middleware import ProxyNegotiationMetadataMiddleware from fastmcp.server.providers.proxy import ProxyClient, ProxyProvider - -def create_backend_client() -> ProxyClient: - return ProxyClient("http://backend:8000/mcp", mode="auto") - - -backend = ProxyProvider(create_backend_client) -metadata = ProxyNegotiationMetadataMiddleware( - backend, - identity="proxy", -) - +backend = ProxyProvider(lambda: ProxyClient("http://backend:8000/mcp", mode="auto")) gateway = FastMCP( "Controlled Gateway", - instructions="Use only approved gateway operations.", providers=[backend], - middleware=[metadata], + middleware=[ProxyNegotiationMetadataMiddleware(backend)], ) ``` -The middleware works across both the legacy `initialize` handshake and modern `server/discover`. It forwards upstream instructions, namespaced `_meta`, and optionally the full upstream `serverInfo`. The frontend remains authoritative for protocol versions, capabilities, cache policy, and `resultType`, since those fields must describe the gateway after its filtering and transforms. Unknown top-level fields are not forwarded because the gateway cannot safely interpret their claims. - -`identity="proxy"` (the default) retains the gateway's `serverInfo`; use `identity="upstream"` to expose the backend's complete implementation identity. Explicit frontend instructions and frontend metadata win on collision. Metadata is fetched lazily during negotiation, and an unavailable backend does not fail negotiation solely because optional metadata could not be read—the first proxied operation reports that failure. +By default the gateway keeps its own `serverInfo`; pass `identity="upstream"` to expose the backend identity. Frontend values win on collisions, and protocol versions, capabilities, cache policy, `resultType`, and unknown top-level fields always remain frontend-owned. If the backend is unavailable, negotiation succeeds without its optional metadata. ### FastMCPProxy Class