mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-17 05:37:43 +00:00
docs(readme): handshake fallback, legacy-SSE exclusion, protocol allowlist (SF5)
Rewrote the federated POST /mcp bullets to document the review round 2 fix batch: per-backend timeouts (20s tools/list, 120s tools/call), the bare-first-with-fallback session handshake and exactly what triggers it (JSON-RPC error or HTTP 4xx, never a network failure or 5xx), the legacy HTTP+SSE-transport exclusion (URL path ending '/sse') and why no handshake can fix it, the loud 'no MCP server reachable' error when every attempted server fails, non-2xx/ empty-envelope rejection, the protocol-version allowlist, and the new 405 for a disallowed method on /mcp. Extended the admin-API GET /admin/api/targets bullet, the total-scope accounting list, and the rejected-before-quota bullet to include the federated endpoint. Architecture diagram gains a Federated branch node (render-checked with mmdc).
This commit is contained in:
@@ -915,35 +915,78 @@ or in CI.
|
|||||||
decodes the request itself, answers `initialize`/`ping` locally, and for
|
decodes the request itself, answers `initialize`/`ping` locally, and for
|
||||||
`tools/list`/`tools/call` issues its own outbound JSON-RPC call(s) to
|
`tools/list`/`tools/call` issues its own outbound JSON-RPC call(s) to
|
||||||
the relevant server(s) as part of handling that one request — no
|
the relevant server(s) as part of handling that one request — no
|
||||||
session state is cached across separate calls (per-request upstream
|
session state is cached across separate CLIENT requests (per-request
|
||||||
sessions, matching the per-server proxy's own complete absence of
|
upstream sessions, matching the per-server proxy's own complete absence
|
||||||
gateway-side session tracking).
|
of gateway-side session tracking); a session a handshake retry opens
|
||||||
- `tools/list` fans out to every allowed server concurrently and merges
|
(below) lives entirely inside the one client request that triggered it.
|
||||||
the results, prefixing each tool's name `"<serverName>_<toolName>"` —
|
Any other HTTP method on exactly `/mcp` is a `405` with an
|
||||||
the same underscore-separator convention the earlier agentgateway used
|
`Allow: POST` header, checked before authentication.
|
||||||
and that existing MCP clients (pugbot's `mcpclient`, agentkit) already
|
- `tools/list` fans out to every allowed server concurrently — each
|
||||||
persist in their own tool-id records (e.g.
|
under its own 20s timeout — and merges the results, prefixing each
|
||||||
`brave-search_brave_web_search`). A server that errors or is
|
tool's name `"<serverName>_<toolName>"` — the same underscore-
|
||||||
unreachable is skipped, not surfaced as a whole-call failure — the
|
separator convention the earlier agentgateway used and that existing
|
||||||
aggregate degrades to every other server's tools.
|
MCP clients (pugbot's `mcpclient`, agentkit) already persist in their
|
||||||
|
own tool-id records (e.g. `brave-search_brave_web_search`). A server
|
||||||
|
that errors, times out, or answers with a response that is not valid
|
||||||
|
JSON-RPC is skipped, not surfaced as a whole-call failure — the
|
||||||
|
aggregate degrades to every other server's tools. If EVERY attempted
|
||||||
|
server fails, the response is a JSON-RPC `internal error`
|
||||||
|
(`"no MCP server reachable"`) instead of a silently-empty tools list,
|
||||||
|
which would otherwise be indistinguishable from "this caller's group
|
||||||
|
has no MCP access at all".
|
||||||
- `tools/call` resolves the target server by the LONGEST matching
|
- `tools/call` resolves the target server by the LONGEST matching
|
||||||
`"<serverName>_"` prefix against the caller's allowed servers (so two
|
`"<serverName>_"` prefix against the caller's allowed servers (so two
|
||||||
configured servers where one name prefixes the other, e.g. `foo` and
|
configured servers where one name prefixes the other, e.g. `foo` and
|
||||||
`foo_bar`, resolve unambiguously), strips the prefix, forwards the
|
`foo_bar`, resolve unambiguously), strips the prefix, forwards the
|
||||||
call, and relays the result under the caller's own JSON-RPC `id`. An
|
call under a 120s timeout, and relays the result under the caller's
|
||||||
unresolvable prefix — including one that names a real but
|
own JSON-RPC `id`. An unresolvable prefix — including one that names a
|
||||||
group-restricted server — is a JSON-RPC `invalid params` error
|
real but group-restricted or transport-excluded server (below) — is a
|
||||||
(`-32602`), not an HTTP `404`: the request reached a real route and
|
JSON-RPC `invalid params` error (`-32602`), not an HTTP `404`: the
|
||||||
method, it just named a tool nothing could route.
|
request reached a real route and method, it just named a tool nothing
|
||||||
|
could route.
|
||||||
|
- **Session handshake fallback**: each outbound backend call tries bare
|
||||||
|
(no session) first. A backend that answers with a JSON-RPC error or an
|
||||||
|
HTTP `4xx` — signalling "you need a session" — gets ONE retry: `POST
|
||||||
|
initialize` (this gateway's own protocol version and minimal
|
||||||
|
`clientInfo`), capture the `Mcp-Session-Id` response header if the
|
||||||
|
backend sets one, re-issue the real call carrying that header, then
|
||||||
|
best-effort `DELETE` the session afterward (failure logged, never
|
||||||
|
surfaced). A network failure, timeout, or `5xx` never triggers this
|
||||||
|
retry — only a response that specifically signals "missing session" is
|
||||||
|
worth a second attempt. This ruling was verified against all 11
|
||||||
|
production MCP servers behind this gateway: 8 answer bare outright, 2
|
||||||
|
(`readitall`, `fetch`) need exactly this handshake, and one — see
|
||||||
|
below — needs neither because it cannot be reached this way at all.
|
||||||
|
- **Legacy HTTP+SSE transport is excluded, not handshaken around**: an
|
||||||
|
MCP server configured with a URL whose path ends in `/sse` speaks the
|
||||||
|
superseded HTTP+SSE transport, not Streamable HTTP — its client must
|
||||||
|
drive a persistent SSE session no simple POST-per-call model (with or
|
||||||
|
without a session handshake) can replicate. Such a server never
|
||||||
|
appears in `tools/list`'s aggregate and can never be resolved by
|
||||||
|
`tools/call` — it is invisible to federation for every caller,
|
||||||
|
independent of group access — but stays fully reachable via the
|
||||||
|
per-server proxy (`/mcp/{name}/...` above), where the real client
|
||||||
|
drives the transport end to end.
|
||||||
- `notifications/*` gets a `202 Accepted` with no body (no response is
|
- `notifications/*` gets a `202 Accepted` with no body (no response is
|
||||||
ever due for a JSON-RPC notification); any other method is a JSON-RPC
|
ever due for a JSON-RPC notification); any other method is a JSON-RPC
|
||||||
`method not found` (`-32601`). Malformed JSON is a JSON-RPC `parse
|
`method not found` (`-32601`). Malformed JSON is a JSON-RPC `parse
|
||||||
error` (`-32700`) with `id: null`, per spec.
|
error` (`-32700`) with `id: null`, per spec. A non-2xx backend
|
||||||
|
response, or one whose body is valid JSON but carries neither
|
||||||
|
`result` nor `error`, is always treated as a failure — never silently
|
||||||
|
relayed as an empty success.
|
||||||
|
- `initialize`'s `protocolVersion` echoes the caller's own requested
|
||||||
|
value only when it names one of a small, explicit set of recognized
|
||||||
|
revisions (`2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`);
|
||||||
|
an unrecognized or omitted value gets this gateway's own default
|
||||||
|
(`2025-11-25`) instead — never an arbitrary caller string echoed back
|
||||||
|
as a capability claim this plugin cannot back up.
|
||||||
- `checkAndCount` runs once per request (the caller's own user/group/
|
- `checkAndCount` runs once per request (the caller's own user/group/
|
||||||
total scopes); each backend server actually contacted is additionally
|
total scopes); each backend server actually ATTEMPTED — not
|
||||||
attributed its own per-target counters — see `GET /admin/api/targets`
|
necessarily reached or succeeded — is additionally attributed its own
|
||||||
above and the request-rate note below. Applies to MCP servers only,
|
per-target counters, batched into one write per request, see `GET
|
||||||
not agents: there is no equivalent aggregated `/a2a` endpoint.
|
/admin/api/targets` above and the request-rate note below. Applies to
|
||||||
|
MCP servers only, not agents: there is no equivalent aggregated
|
||||||
|
`/a2a` endpoint.
|
||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user