Skip to content

Stop forwarding inbound client headers to backend MCP servers#62

Open
clintonb wants to merge 2 commits into
stainless-api:mainfrom
voriteam:stop-forwarding-inbound-headers
Open

Stop forwarding inbound client headers to backend MCP servers#62
clintonb wants to merge 2 commits into
stainless-api:mainfrom
voriteam:stop-forwarding-inbound-headers

Conversation

@clintonb

Copy link
Copy Markdown

mcp-go stamps every inbound HTTP request's headers onto the parsed request structs (mark3labs/mcp-go#480, since v0.33), and its streamable client sends a non-nil request.Header verbatim as the outgoing request's headers (mark3labs/mcp-go#546, since v0.43.0). A proxy that passes the request struct from its server side to its backend client — which mcp-front's tool, prompt, and resource handlers all do — therefore silently forwards the end client's headers to backends: Accept-Encoding, Cookie, and Authorization (when the backend config doesn't override it).

The visible symptom in production: an explicit forwarded Accept-Encoding disables Go's transparent gzip decompression, so backends that honor it (observed with Google's and Datadog's hosted MCP servers) return gzip bodies that reach mcp-go's JSON decoder, failing with invalid character '\x1f' looking for beginning of value. The failure pattern tracks which end clients send an explicit Accept-Encoding header, which made it look like random backend flakiness. Beyond the gzip symptom, forwarding inbound credentials-adjacent headers to backends is a header leak in its own right.

The fix clears request.Header at every backend-forwarding boundary: the aggregate tool handler and the proxy tool, prompt, resource, and resource-template handlers. Unit tests assert the header (including Accept-Encoding and Cookie) is dropped before the backend client sees the request.

🤖 Generated with Claude Code

mcp-go stamps every inbound HTTP request's headers onto the parsed
request structs (PR #480) and its streamable client sends request.Header
verbatim to the backend (PR #546, since v0.43.0). A proxy passing the
request struct through therefore forwards the end client's headers —
Accept-Encoding, cookies, auth — to backends. An explicit
Accept-Encoding disables Go's transparent gzip decompression, which is
how still-gzipped gke/datadog responses reached the JSON decoder.

Clears request.Header at every backend-forwarding boundary: the
aggregate tool handler and the proxy tool, prompt, resource, and
resource-template handlers. The gzip guard transport remains as
defense-in-depth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces changes to strip inbound HTTP headers from requests before forwarding them to backend clients, preventing potential header leakage and issues with transparent gzip decompression. This logic is applied across tool, prompt, resource, and resource template handlers, and is accompanied by new unit tests. The review feedback suggests optimizing performance by moving several closure allocations and handler definitions out of loops to avoid redundant allocations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread internal/client/client.go Outdated
"description": tool.Description,
})
// Wrap the tool handler to check for user tokens if required
callTool := stripInboundHeaders(c.client)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Efficiency: Redundant Closure Allocation in Loop

The stripInboundHeaders wrapper is called inside the for _, tool := range tools.Tools loop:

callTool := stripInboundHeaders(c.client)

Since c.client is constant across all iterations of the loop, this closure is unnecessarily recreated and allocated for every single tool.

Recommendation:
Move callTool := stripInboundHeaders(c.client) outside of the loop (before the for loop) to avoid redundant allocations.

Comment thread internal/client/client.go
Comment on lines 329 to 335
for _, prompt := range prompts.Prompts {
log.Logf("<%s> Adding prompt %s", c.name, prompt.Name)
mcpServer.AddPrompt(prompt, c.client.GetPrompt)
mcpServer.AddPrompt(prompt, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
request.Header = nil
return c.client.GetPrompt(ctx, request)
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Efficiency: Redundant Closure Allocation in Loop

Defining the anonymous handler function inside the for loop causes a new closure to be allocated for every single prompt during discovery. Since the handler only interacts with c.client and the incoming request (which is passed as a parameter to the handler), it does not capture any loop-specific variables (like prompt).

We can define the handler once outside the loop and reuse it for all prompts, avoiding unnecessary allocations.

		getPrompt := func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
			request.Header = nil
			return c.client.GetPrompt(ctx, request)
		}
		for _, prompt := range prompts.Prompts {
			log.Logf("<%s> Adding prompt %s", c.name, prompt.Name)
			mcpServer.AddPrompt(prompt, getPrompt)
		}

Comment thread internal/client/client.go Outdated
Comment on lines +373 to +374
mcpServer.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
request.Header = nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Efficiency: Redundant Closure Allocation in Loop

Similar to the prompts handler, the anonymous function for reading resources is defined inside the for loop, causing a new closure to be allocated for every resource. Since the handler does not capture any loop-specific variables (like resource), it can be defined once outside the loop to avoid redundant allocations.

For example:

		readResource := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
			request.Header = nil
			readResource, e := c.client.ReadResource(ctx, request)
			if e != nil {
				return nil, e
			}
			return readResource.Contents, nil
		}
		for _, resource := range resources.Resources {
			log.Logf("<%s> Adding resource %s", c.name, resource.Name)
			mcpServer.AddResource(resource, readResource)
		}

Comment thread internal/client/client.go Outdated
Comment on lines +410 to +411
mcpServer.AddResourceTemplate(resourceTemplate, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
request.Header = nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Efficiency: Redundant Closure Allocation in Loop

Similarly, the anonymous function for reading resource templates is defined inside the for loop, causing a new closure to be allocated for every template. Since the handler does not capture any loop-specific variables (like resourceTemplate), it can be defined once outside the loop to avoid redundant allocations.

For example:

		readResource := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
			request.Header = nil
			readResource, e := c.client.ReadResource(ctx, request)
			if e != nil {
				return nil, e
			}
			return readResource.Contents, nil
		}
		for _, resourceTemplate := range resourceTemplates.ResourceTemplates {
			log.Logf("<%s> Adding resource template %s", c.name, resourceTemplate.Name)
			mcpServer.AddResourceTemplate(resourceTemplate, readResource)
		}

The tool, prompt, resource, and resource-template handlers are identical
across every discovered item, so hoist them out of the discovery loops
to avoid allocating an equivalent closure per item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clintonb

Copy link
Copy Markdown
Author

Addressed the closure-allocation feedback in 319ca25: the tool, prompt, resource, and resource-template handlers are loop-invariant, so each is now built once before its discovery loop rather than per item. For the tool handler the whole handler (including the wrapToolHandler wrapping) is invariant, so the entire computation was hoisted, not just the stripInboundHeaders call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant