mcp: add Headers and MaxResponseBytes to StreamableClientTransport#926
mcp: add Headers and MaxResponseBytes to StreamableClientTransport#926
Conversation
The header merge loop indexed req.Header by raw map key, so a non-canonical key like "content-type" would miss the canonical entry written by req.Header.Set and be sent as a duplicate. Canonicalize before lookup, and add tests for Headers and MaxResponseBytes.
dbb02b7 to
f612d1a
Compare
| if err == nil && c.maxResponseBytes > 0 { | ||
| resp.Body = http.MaxBytesReader(nil, resp.Body, c.maxResponseBytes) | ||
| } |
There was a problem hiding this comment.
The MaxResponseBytes limit is applied inside doRequest() for the initial POST response, this means that when handleSSE() reconnects after a stream interruption (line 2019), the new response body has no size protection.
I think we should apply the limit in connectSSE() as well, or it that an intentional exclusion?
|
Hi @pja-ant, I believe both these use cases can be already achieved via the AI-generated snippets below:
type headerTransport struct {
base http.RoundTripper
headers http.Header
}
func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for k, vs := range t.headers {
if _, ok := req.Header[http.CanonicalHeaderKey(k)]; !ok {
req.Header[http.CanonicalHeaderKey(k)] = vs
}
}
return t.base.RoundTrip(req)
}
// Usage:
transport := &mcp.StreamableClientTransport{
Endpoint: "https://example.com/mcp",
HTTPClient: &http.Client{
Transport: &headerTransport{
base: http.DefaultTransport,
headers: http.Header{
"X-Custom": {"custom-value"},
},
},
},
}This achieves the exact same "add headers, but don't override transport-set headers" behavior, because
type maxBytesTransport struct {
base http.RoundTripper
maxBytes int64
}
func (t *maxBytesTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := t.base.RoundTrip(req)
if err != nil {
return nil, err
}
if t.maxBytes > 0 {
resp.Body = http.MaxBytesReader(nil, resp.Body, t.maxBytes)
}
return resp, err
}
// Usage:
transport := &mcp.StreamableClientTransport{
Endpoint: "https://example.com/mcp",
HTTPClient: &http.Client{
Transport: &maxBytesTransport{
base: http.DefaultTransport,
maxBytes: 1 << 20, // 1 MiB
},
},
}This is functionally equivalent. One minor difference: the PR's version only applies |
Summary
Adds two configuration options to
StreamableClientTransport:Headers http.Header— additional HTTP headers to send with every request to the MCP endpoint (useful for API keys, tracing IDs, tenant identifiers). Transport-managed headers (Content-Type,Accept,Authorization,Mcp-Protocol-Version,Mcp-Session-Id) take precedence; user-supplied keys are canonicalized before the precedence check so non-canonical spellings cannot produce duplicate headers.MaxResponseBytes int64— caps POST response body size viahttp.MaxBytesReaderto protect clients from unbounded payloads. Zero means no limit; does not apply to the standalone SSE stream.Test plan
TestStreamableClientHeaders— custom headers are forwarded on POST/GET/DELETE; non-canonicalcontent-typedoes not override the transport-set valueTestStreamableClientMaxResponseBytes— tool call succeeds under a large limit and fails when the response exceeds the limitgo test ./mcp/passesgo vet ./mcp/clean