Summary
The broker silently serves no tools/prompts for an upstream that (a) negotiates a 2025 protocol revision other than 2025-11-25 (e.g. 2025-03-26) and/or (b) runs the streamable-HTTP transport in stateless mode (no Mcp-Session-Id). Such an upstream connects successfully but is dropped from every served set with no error, and — because the broker treats any non-2026 upstream as stateful — its session ping is rejected, de-federating it.
Why it happens
rebuildProtocolCaches partitions an upstream's tools into the stateful/stateless served set on an exact Version2025 (2025-11-25) / Version2026 (2026-07-28) match. An upstream advertising 2025-03-26 matches neither → served to no client.
- The broker requests
2025-11-25 at initialize; an upstream on an older MCP SDK down-negotiates to the newest revision it knows (e.g. 2025-03-26), so this is the common case for any not-freshly-updated server.
- Statelessness is inferred from
protocol version >= 2026-07-28, not from the transport. A session-less 2025 server therefore gets a session ping (rejected by strict stateless servers) and a session-keyed GET SSE watcher (nothing to watch).
Reproduction
Stock baruchiro/paperless-mcp (stateless streamable-HTTP, @modelcontextprotocol/sdk 1.11 → negotiates 2025-03-26): registers behind the broker, connects, and lists zero tools downstream. mcp-searxng 2.0.0 exhibits the same class of interop failure (cf. #1419).
Proposed fix
Decouple "served" from "stateless":
- serve any
2025- family upstream on the stateful route (consistently in rebuildProtocolCaches, buildRoutingTable, and FetchUserSpecificTools so tools list, route, and fetch);
- detect statelessness by transport (
session.ID() == ""), skipping the session ping and GET SSE watcher and recycling the connection each health tick (as 2026 upstreams already do) so liveness/recovery is preserved.
A complete, unit-tested implementation (9 files, go vet/gofmt clean, broker + upstream suites green) is ready on a branch and can be opened as a PR once this is triaged:
https://github.com/rwlove/mcp-gateway/tree/feat/serve-stateless-2025-upstreams
Summary
The broker silently serves no tools/prompts for an upstream that (a) negotiates a 2025 protocol revision other than
2025-11-25(e.g.2025-03-26) and/or (b) runs the streamable-HTTP transport in stateless mode (noMcp-Session-Id). Such an upstream connects successfully but is dropped from every served set with no error, and — because the broker treats any non-2026upstream as stateful — its session ping is rejected, de-federating it.Why it happens
rebuildProtocolCachespartitions an upstream's tools into the stateful/stateless served set on an exactVersion2025(2025-11-25) /Version2026(2026-07-28) match. An upstream advertising2025-03-26matches neither → served to no client.2025-11-25at initialize; an upstream on an older MCP SDK down-negotiates to the newest revision it knows (e.g.2025-03-26), so this is the common case for any not-freshly-updated server.protocol version >= 2026-07-28, not from the transport. A session-less2025server therefore gets a session ping (rejected by strict stateless servers) and a session-keyed GET SSE watcher (nothing to watch).Reproduction
Stock
baruchiro/paperless-mcp(stateless streamable-HTTP,@modelcontextprotocol/sdk1.11 → negotiates2025-03-26): registers behind the broker, connects, and lists zero tools downstream.mcp-searxng2.0.0 exhibits the same class of interop failure (cf. #1419).Proposed fix
Decouple "served" from "stateless":
2025-family upstream on the stateful route (consistently inrebuildProtocolCaches,buildRoutingTable, andFetchUserSpecificToolsso tools list, route, and fetch);session.ID() == ""), skipping the session ping and GET SSE watcher and recycling the connection each health tick (as 2026 upstreams already do) so liveness/recovery is preserved.A complete, unit-tested implementation (9 files,
go vet/gofmt clean, broker + upstream suites green) is ready on a branch and can be opened as a PR once this is triaged:https://github.com/rwlove/mcp-gateway/tree/feat/serve-stateless-2025-upstreams