Forward proxy server metadata across protocol eras (#4776)

* Forward proxy negotiation metadata

🤖 Generated with OpenAI Codex

* Limit forwarded proxy metadata

🤖 Generated with OpenAI Codex

* Tighten negotiation metadata forwarding

🤖 Generated with OpenAI Codex

* Tighten proxy metadata docs

🤖 Generated with OpenAI Codex

* Keep proxy metadata middleware with provider

🤖 Generated with OpenAI Codex

* Simplify proxy negotiation middleware API

🤖 Generated with OpenAI Codex

* Name proxy metadata middleware directly

🤖 Generated with OpenAI Codex

* Preserve discovery middleware contracts

🤖 Generated with OpenAI Codex

* Clarify proxy metadata ownership

🤖 Generated with OpenAI Codex

* Align proxy metadata wording

🤖 Generated with OpenAI Codex

* Call forwarded values server metadata

🤖 Generated with OpenAI Codex

* Harden proxy metadata reads

🤖 Generated with OpenAI Codex

* Expose configured discovery result

🤖 Generated with OpenAI Codex

* Preserve proxy discovery compatibility

🤖 Generated with OpenAI Codex

* Preserve deprecated initialization middleware

🤖 Generated with OpenAI Codex

* Harden proxy metadata boundaries

🤖 Generated with OpenAI Codex

* Restore deprecated middleware location

🤖 Generated with OpenAI Codex

* Simplify proxy metadata client lifecycle

🤖 Generated with OpenAI Codex

* Clarify proxy metadata lifecycle

🤖 Generated with OpenAI Codex

* Preserve proxy factory errors

🤖 Generated with OpenAI Codex

* Detach forwarded proxy metadata

🤖 Generated with OpenAI Codex

* Simplify proxy metadata implementation

🤖 Generated with OpenAI Codex

* Distinguish proxy metadata failures

🤖 Generated with OpenAI Codex

* Narrow proxy metadata validation fallback

🤖 Generated with OpenAI Codex

* Retrigger CI

🤖 Generated with OpenAI Codex
This commit is contained in:
Jake Kaplan 2026-08-06 19:09:05 -04:00 committed by GitHub
commit 9feb1f378b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1113 additions and 134 deletions

View file

@ -310,6 +310,22 @@ async def on_initialize(self, context: MiddlewareContext, call_next):
Rejection works only **before** `call_next()`. Raising `McpError` afterward logs the error without sending it — the client still receives a successful initialize response.
</Warning>
#### on_discover
Called when a modern client negotiates through `server/discover`. Core discovery responses are returned as `DiscoverResult`; extension-owned result types are returned as dictionaries and should be passed through unless the middleware handles that extension.
```python
from mcp_types import DiscoverResult
async def on_discover(self, context, call_next):
result = await call_next(context)
if not isinstance(result, DiscoverResult):
return result
return result.model_copy(update={"instructions": "Custom instructions"})
```
Fields such as `supported_versions`, `capabilities`, and cache policy should only be changed when the server's public behavior also changes.
### Raw Handler
For complete control over all messages, override `__call__` instead of individual hooks:

View file

@ -60,11 +60,9 @@ To mount a proxy inside another FastMCP server, see [Mounting External Servers](
## Connection Semantics
FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. The upstream connection begins when an MCP client sends an `initialize` request to the proxy.
FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. During client negotiation, the proxy makes a best-effort request for optional server metadata using the backend client's existing lifecycle and negotiation mode; an unavailable backend does not prevent the client from connecting to the proxy.
During initialization, the proxy initializes the upstream server before responding locally. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or upstream authentication cannot complete, the proxy initialization fails. This keeps the local proxy's connection status aligned with the upstream server it represents.
After initialization, the proxy forwards MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress through the upstream client.
Subsequent MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress connect to the backend as needed. Component provider failures follow `provider_error_strategy`: the default `"warn"` logs and skips a failed provider, while `"raise"` reports the failure to the client.
## Transport Bridging
@ -388,6 +386,28 @@ Only reuse sessions when you know the backend is stateless (e.g. stateless HTTP)
## Advanced Usage
### Forwarding Server Metadata
Add `ProxyMetadataMiddleware` when a gateway built with `ProxyProvider` should also expose backend instructions and namespaced `_meta`:
```python
from fastmcp import FastMCP
from fastmcp.server.providers.proxy import (
ProxyClient,
ProxyMetadataMiddleware,
ProxyProvider,
)
backend = ProxyProvider(lambda: ProxyClient("http://backend:8000/mcp", mode="auto"))
gateway = FastMCP(
"Controlled Gateway",
providers=[backend],
middleware=[ProxyMetadataMiddleware(backend)],
)
```
By default the gateway keeps its own `serverInfo`; pass `identity="upstream"` to use the backend identity when available. Frontend instructions and `_meta` values win on collisions. The middleware never copies upstream protocol versions, connection metadata, capabilities, cache policy, `resultType`, or unknown top-level fields. If the backend is unavailable, the client can still connect without its optional metadata.
### FastMCPProxy Class
For explicit session control, use `FastMCPProxy` directly: