Skip to content

Fix HTTP 400 on tools/list for HTTP backends with custom auth headers (Atlassian MCP)#2608

Merged
lpcox merged 2 commits intomainfrom
copilot/fix-http-400-list-tools
Mar 26, 2026
Merged

Fix HTTP 400 on tools/list for HTTP backends with custom auth headers (Atlassian MCP)#2608
lpcox merged 2 commits intomainfrom
copilot/fix-http-400-list-tools

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

When an HTTP MCP backend was configured with custom headers (e.g. Authorization: Basic …), NewHTTPConnection skipped the SDK-managed Streamable HTTP and SSE transports entirely and jumped straight to a plain JSON-RPC-over-POST fallback. That fallback violates the MCP 2025-03-26 spec in several ways (missing initialize handshake ordering, params: null vs params: {}), causing Atlassian's server to return HTTP 400 on tools/list.

Core fix

internal/mcp/http_transport.go

  • Added headerInjectingRoundTripper: an http.RoundTripper wrapper that injects a fixed header map into every outgoing request.
  • Added buildHTTPClientWithHeaders(base, headers): returns a shallow-cloned *http.Client whose transport is the injecting round-tripper; returns base unchanged when headers is empty.
type headerInjectingRoundTripper struct {
    base    http.RoundTripper
    headers map[string]string
}

func (rt *headerInjectingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    reqCopy := req.Clone(req.Context())
    for k, v := range rt.headers {
        reqCopy.Header.Set(k, v)
    }
    return rt.base.RoundTrip(reqCopy)
}

internal/mcp/connection.go

  • Removed the if len(headers) > 0 { → tryPlainJSONTransport } bypass. All backends now always attempt Streamable HTTP → SSE → plain JSON-RPC, passing a header-injecting client to the SDK transports.
  • reconnectSDKTransport likewise rebuilds the header-injecting client on reconnect so auth headers survive session expiry.

Test updates

Several tests assumed "custom headers → plain JSON-RPC". Changes:

  • Tests that specifically exercise the plain JSON-RPC code path now call tryPlainJSONTransport directly via a newPlainJSONConn helper, insulating them from transport selection changes.
  • Test HTTP servers that called t.FailNow() / require.NoError on empty-body requests (GET/DELETE probe requests from the Streamable transport) now return 405 Method Not Allowed gracefully so they fail-fast and fall back to plain JSON-RPC as intended.
  • Integration tests updated to accept that early SDK transport probes may not carry Mcp-Session-Id, while still asserting the Authorization header is present on every request and that the final plain JSON-RPC initialization succeeds with the correct session ID pattern.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build171139688/b334/launcher.test /tmp/go-build171139688/b334/launcher.test -test.testlogfile=/tmp/go-build171139688/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s -W aw-mcpg/internal/tmp/go-build303593668/b241/symabis aw-mcpg/internal-c=4 x_amd64/link . --gdwarf2 --64 x_amd64/link -I 372667/b001/_pkg_.a -I (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build171139688/b319/config.test /tmp/go-build171139688/b319/config.test -test.testlogfile=/tmp/go-build171139688/b319/testlog.txt -test.paniconexit0 -test.timeout=10m0s -uns�� Fj0d/1ybRA9tQ-C_xvKXEFj0d /tmp/go-build3955865240/b041/vet.cfg x_amd64/compile -D GOAMD64_v1 -o x_amd64/compile -I ache/go/1.25.8/x64/src/net/addrselect.go ache/go/1.25.8/x64/src/net/cgo_unix.go ache/Python/3.12.13/x64/bin/bash --gdwarf-5 --64 -o /opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linuconntrack (dns block)
    • Triggering command: /tmp/go-build3065041637/b319/config.test /tmp/go-build3065041637/b319/config.test -test.testlogfile=/tmp/go-build3065041637/b319/testlog.txt -test.paniconexit0 -test.timeout=10m0s -o fa3e4140c55d28ab -trimpath docker-compose by/abae9c8c0ced3bash github.com/githu/usr/bin/runc -lang=go1.25 docker-compose �� d68cedb73cad4caf/run/containerd/io.containerd.runtime.v2.task/moby/aecbb305fa8c8fe12aec9eb4a2fb2/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -goversion /usr/bin/docker -c=4 -nolocalimports 802/log.json docker (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build171139688/b334/launcher.test /tmp/go-build171139688/b334/launcher.test -test.testlogfile=/tmp/go-build171139688/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s -W aw-mcpg/internal/tmp/go-build303593668/b241/symabis aw-mcpg/internal-c=4 x_amd64/link . --gdwarf2 --64 x_amd64/link -I 372667/b001/_pkg_.a -I (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build171139688/b334/launcher.test /tmp/go-build171139688/b334/launcher.test -test.testlogfile=/tmp/go-build171139688/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s -W aw-mcpg/internal/tmp/go-build303593668/b241/symabis aw-mcpg/internal-c=4 x_amd64/link . --gdwarf2 --64 x_amd64/link -I 372667/b001/_pkg_.a -I (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build3955865240/b001/mcp.test /tmp/go-build3955865240/b001/mcp.test -test.testlogfile=/tmp/go-build3955865240/b001/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.run=TestNewHTTPConnection 1.4.1/internal/x. ypto.go x_amd64/compile credential.usernas (dns block)
    • Triggering command: /tmp/go-build399372667/b001/mcp.test /tmp/go-build399372667/b001/mcp.test -test.testlogfile=/tmp/go-build399372667/b001/testlog.txt -test.paniconexit0 -test.run=TestNewHTTPConnection|TestTryPlain -test.v=true -test.timeout=1m0s S2YQJddBm 64/pkg/tool/linu-o gpg.program (dns block)
    • Triggering command: /tmp/go-build3428895484/b001/mcp.test /tmp/go-build3428895484/b001/mcp.test -test.testlogfile=/tmp/go-build3428895484/b001/testlog.txt -test.paniconexit0 -test.timeout=2m0s -goversion go1.25.8 -c=4 -nolocalimports -importcfg /tmp/go-build399372667/b001/importcfg -pack /tmp/go-build399372667/b001/_testmain.go ortc�� g_.a 64/src/crypto/internal/impl/impl--gdwarf2 ache/go/1.25.8/x64/pkg/tool/linu--64 commit.gpgsign abis (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

@lpcox lpcox marked this pull request as ready for review March 26, 2026 20:29
Copilot AI review requested due to automatic review settings March 26, 2026 20:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review any files in this pull request.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…ia RoundTripper in SDK transports

Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/aa49466a-a33a-4fcc-b3f9-6b8d24d91a06

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix HTTP 400 error on tools/list for Atlassian MCP backend Fix HTTP 400 on tools/list for HTTP backends with custom auth headers (Atlassian MCP) Mar 26, 2026
Copilot AI requested a review from lpcox March 26, 2026 21:08
@lpcox lpcox merged commit 9718346 into main Mar 26, 2026
34 checks passed
@lpcox lpcox deleted the copilot/fix-http-400-list-tools branch March 26, 2026 21:15
lpcox added a commit that referenced this pull request Mar 26, 2026
PR #2608 removed the 'headers → skip SDK transports' shortcut so SDK
transports now always attempt first. The mock servers were hardcoding
JSON-RPC response id=1, which caused mismatched request IDs when the
SDK assigned sequential IDs — tools/list responses never matched their
Await calls, hanging the tests.

Fixes:
- Echo back the request ID from the JSON-RPC body in mock responses
- Handle notifications/initialized with 202 Accepted
- Handle empty/probe requests (GET/DELETE) with 405
- Update session ID propagation assertions to account for SDK transport
  behavior (SDK manages sessions internally, no Mcp-Session-Id header)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lpcox added a commit that referenced this pull request Mar 26, 2026
PR #2608 removed the 'headers → skip SDK transports' shortcut, so SDK
transports now always attempt first. This caused 3 tests in
`unified_http_backend_test.go` to hang because:

1. Mock servers hardcoded `"id": 1` in all JSON-RPC responses, but the
SDK assigns sequential IDs — `tools/list` responses never matched their
`Await` calls
2. Mocks didn't handle `notifications/initialized` (SDK sends this after
initialize)
3. Session ID propagation assertions assumed plain JSON-RPC behavior

**Fixes:**
- Echo back the request ID from the JSON-RPC body
- Handle `notifications/initialized` with 202 Accepted  
- Handle empty/probe requests with 405
- Update session ID assertions to account for SDK streamable transport

`make agent-finished` passes ✅
lpcox added a commit that referenced this pull request Mar 27, 2026
Fixes hanging `TestConnection_SendRequest` in `internal/mcp` caused by
SDK streamable transport changes from #2608.

**Root cause:** After #2608 removed the headers shortcut,
`NewHTTPConnection` always tries SDK streamable transport first. Two
mock server issues caused hangs:

1. `newPlainJSONTestServer` didn't handle `notifications/initialized`
(SDK sends this after `initialize`)
2. `TestConnection_SendRequest` hardcoded `"id": 1` — SDK assigns
sequential IDs, so the response never matched the `Await` call

**Fix:**
- Added `notifications/initialized` handler (202 Accepted) to
`newPlainJSONTestServer`
- Echo back `req["id"]` instead of hardcoding `1` in the test handler
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.

[gateway] HTTP 400 on tools/list for Atlassian MCP HTTP backend (Streamable HTTP transport)

3 participants