Skip to content

fix(mcp): forward configured headers to OAuth discovery on the server host#3159

Merged
dgageot merged 2 commits into
mainfrom
fix/3148-mcp-oauth-headers
Jun 17, 2026
Merged

fix(mcp): forward configured headers to OAuth discovery on the server host#3159
dgageot merged 2 commits into
mainfrom
fix/3148-mcp-oauth-headers

Conversation

@Sayt-0

@Sayt-0 Sayt-0 commented Jun 17, 2026

Copy link
Copy Markdown
Member

Fixes #3148

Problem

A remote MCP server's headers were applied only to the main MCP channel, never to the OAuth flow. Grafana Cloud needs X-Grafana-URL on the OAuth discovery request to scope the flow to the right instance; without it the auth screen keeps prompting for the instance.

Root cause

Request Transport used Headers sent?
Main MCP channel oauthTransport.base (header transport) yes
OAuth discovery / token oauthHTTPClient (bare singleton) no

The protected-resource-metadata request (the one Grafana uses to scope the instance) went out with none of the configured headers.

Fix

  • The OAuth HTTP client now forwards the configured headers...
  • ...but only to the MCP server's own host. Requests to a third-party authorization server (URL taken from untrusted server metadata) get no headers, so Authorization / API keys can't leak.
  • Preserved: SSRF protection, allow_private_ips, timeout, and ${headers.NAME} placeholders. The shared client singleton is never mutated.

Tests

Test Verifies
...ScopesHeadersToMCPHost header reaches the MCP host, not a third party
...NoHeadersReusesBaseClient no headers configured -> base client returned unchanged
TestInitialize_CustomHeadersReachOAuthDiscovery real Initialize() path; reverting the fix makes it fail (reproduces #3148)

Validation: go build ./..., golangci-lint run ./pkg/tools/mcp/... (0 issues), go test -race ./pkg/tools/mcp/ all pass.

Caveat

Scoping is on the exact host of the configured URL. If a server ever required the header on a host different from its MCP endpoint, the scope would need widening. Started strict, for safety.

… host

Remote MCP `headers` were applied only to the main channel, never to the OAuth flow's discovery/token requests. Servers like Grafana Cloud need a routing header (X-Grafana-URL) on the protected-resource-metadata request to scope the OAuth flow to the right instance; without it the auth screen re-prompts for the instance.

The OAuth HTTP client now forwards configured headers, but only to requests targeting the MCP server's own host, so credentials are never leaked to a third-party authorization server advertised in (untrusted) server metadata. SSRF safety, allow_private_ips, timeout and ${headers.NAME} placeholders are preserved; the shared client singleton is never mutated.

Fixes #3148
@Sayt-0 Sayt-0 requested a review from a team as a code owner June 17, 2026 16:40

@docker-agent docker-agent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

One low-probability finding in newly added code: the host-scoping check may silently drop configured headers when the MCP server URL includes an explicit standard port (e.g., :443). The fix logic and security model (no credential leakage to third-party auth servers) are sound. All other security properties — SSRF protection, timeout, singleton non-mutation — are preserved correctly.

Comment thread pkg/tools/mcp/oauth_helpers.go Outdated
url.Parse keeps an explicit standard port verbatim, so a config URL like https://host:443/mcp stored host "host:443" while servers usually advertise port-less discovery URLs (host). The host comparison then missed and headers were silently dropped, reproducing #3148 for users who spell out :443/:80. Normalize both sides by stripping the scheme's default port.
@Sayt-0 Sayt-0 requested a review from docker-agent June 17, 2026 16:54

@docker-agent docker-agent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

The host-scoping mechanism (hostScopedHeaderTransport) correctly forwards configured MCP server headers only to the MCP server's own host, preventing credential leakage to third-party authorization servers. The core security property is sound and well-tested.

Minor observations (no action required):

  1. oauthHTTPClientWithHeaders omits Jar from base client (oauth_helpers.go:82) — The newly constructed *http.Client does not copy base.Jar. Today all base clients have a nil Jar, so this is harmless. If a cookie jar is ever attached to the base OAuth client in the future, it would silently disappear in the wrapped client.

  2. req.URL.Host may be empty on redirect requests (oauth_helpers.go:101) — Go's HTTP client may reissue requests with Request.URL.Host == "" (host in Request.Host) on redirects. In that case hostWithoutDefaultPort returns "", the host check fails, and headers are silently dropped even if the redirect target is the MCP server itself. This only triggers on same-host redirects and is not the primary OAuth discovery path.

Both are low-risk edge cases that don't affect the primary use case described in this PR.

@aheritier aheritier added area/mcp MCP protocol, MCP tool servers, integration kind/fix PR fixes a bug (maps to fix: commit prefix) labels Jun 17, 2026
@dgageot dgageot merged commit 264e20f into main Jun 17, 2026
11 checks passed
@dgageot dgageot deleted the fix/3148-mcp-oauth-headers branch June 17, 2026 19:43
pull Bot pushed a commit to TheTechOddBug/cagent that referenced this pull request Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/mcp MCP protocol, MCP tool servers, integration kind/fix PR fixes a bug (maps to fix: commit prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Headers not passed to MCP ?

4 participants