agentic-community / agentic-community/mcp-gateway-registry
MCP proxy hop does not apply the server `auth_scheme` credential or `custom_headers` (FAQ says it does); no operator-side way to set User-Agent on egress
- Vorherrschende Sprache
- Python
- Sterne
- 911
- Forks
- 234
- Ø Merge
- 1 T. 11 Std.
- Gemergte PRs (30 T.)
- 62
Beschreibung
## Summary
On the MCP proxy hop (`auth_server` `mcp_proxy`), a registered server's **Backend Authentication** credential (`auth_scheme` + `auth_credential`) and its **custom headers** (`custom_headers`, #1028) are **not** applied to the upstream request. They are only used by the registry for health checks and tool discovery (`registry/core/mcp_client.py`, `registry/health/service.py`).
Three consequences for operators fronting a third-party MCP server that expects a shared, operator-owned credential or a fixed header:
1. The FAQ `docs/faq/registering-auth-protected-servers.md` still states "**Request proxying** -- When clients connect through the gateway, the credential is injected into proxied requests". That is not what the code does, so operators configure `auth_scheme=bearer`, see the server turn healthy, and then every proxied call fails at the upstream.
2. `custom_headers` were proposed in #1028 with "Also injected outbound when the gateway proxies to the backend MCP server" and with "Required `User-Agent` … values some backends enforce" as a motivating case. PR #1041 implemented registration, encryption, health/discovery and the Connect JSON, but touched no `auth_server` file, so the outbound part never landed.
3. The 1.30.0 generic HTTP proxy (#1628) introduced a **shared operator key** credential model with operator-injected headers (`_vend_generic_upstream_headers` / `_merge_generic_upstream_headers`). The MCP hop has no equivalent, so the same upstream behaves differently depending on which hop fronts it.
I understand from #1265 that `auth_scheme` was redefined as "the gateway acting as itself" (health checks) and that per-user egress is the preferred model (`egress_auth_mode` pat / oauth_user / obo_exchange). This issue is not asking to weaken that: it asks for (a) the documented behaviour to match the code, and (b) an explicit, opt-in way to apply operator-owned headers on the MCP hop, as the generic hop already allows.
## Environment
- MCP Gateway Registry 1.29.x, Helm (EKS-style deployment), `AUTH_PROVIDER=entra`, `EGRESS_AUTH_ENABLED` unset (default off).
- Upstream: a vendor MCP server that requires two fixed headers (an API key and a protocol-signal header), hosted behind Cloudflare with Browser Integrity Check enabled.
## Observed
- Registry health check / tool fetch to the upstream: sends `Authorization: Bearer ` (from `auth_scheme`), gets the expected app-level answer.
- Proxied client call to the same upstream (`POST /mcp-proxy//mcp`): `_forward_headers()` copies the client's headers minus hop-by-hop, cookie and auth; nothing from the server record is added. The upstream receives the **client's `User-Agent`** verbatim. In our case the client is a hosted coding agent whose default User-Agent trips Cloudflare error 1010, so every proxied call returns 403 while the registry's own health check (httpx default UA, same egress IP) passes. There is no per-server setting that lets the operator override or normalize `User-Agent` on the MCP hop.
- With `egress_auth_mode` unset and `EGRESS_AUTH_ENABLED=false`, no credential is injected at all on the MCP hop, so the upstream additionally answers 401 once the UA problem is worked around.
Relevant code (main at time of writing):
- `auth_server/server.py`: `_forward_headers()` (strips ingress auth, forwards everything else), `mcp_proxy()` (injects only vaulted egress tokens under `settings.egress_auth_enabled`).
- `registry/core/mcp_client.py`: merges `headers`, `custom_headers_encrypted` and `auth_scheme` credential for health/discovery only.
- `registry/api/egress_auth_routes.py`: `/_egress_internal/egress-token` never returns the server's static `auth_credential`.
## Proposal
1. **Docs (immediate):** fix the FAQ bullet to say the credential is used for health checks and tool discovery, and that proxied requests get upstream credentials only through `egress_auth_mode` (link `docs/egress-credential-vault.md`). I can send this PR right away.
2. **`custom_headers` on the MCP hop:** apply the server's decrypted `custom_headers` on egress in `mcp_proxy`, operator value winning over any caller-supplied header of the same name (same semantics as `_merge_generic_upstream_headers` on the generic hop, reusing `_pop_header_ci`). This alone covers the `User-Agent` / protocol-signal-header cases without touching credentials.
3. **Opt-in shared operator credential on the MCP hop:** an explicit per-server mode (for example `egress_auth_mode: operator_credential`) that injects the stored `auth_scheme` credential on egress, gated behind `EGRESS_AUTH_ENABLED` like the other modes, so the default stays "strip and inject nothing". This mirrors the generic hop's "shared operator key" model and keeps #1265's default intact.
## Acceptance criteria
- [ ] FAQ no longer claims proxied requests receive the `auth_scheme` credential.
- [ ] A server with `custom_headers` set has those headers on the upstream request made by `mcp_proxy`, operator value winning on name collision.
- [ ] A server without the opt-in mode still gets no credential on egress (unchanged default).
- [ ] With the opt-in mode, the stored credential is injected with the header derived from `auth_scheme` / `auth_header_name`, and never logged.
- [ ] Unit tests for `_forward_headers` + merge behaviour.
Related: #1237 (closed, same symptom), #1265, #1028 / #1041, #966, #1628.
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.