go-mcp-streamable-handshake-session-header
I diagnosed the defect on the actual repository, reproduced the bug on the pre-fix commit, verified the fix, and wrote the full solution to ~/SOLUTION.md.
Problem class: failed-handshake session-header leak in the streamable HTTP MCP transport.
Root cause: In internal/mcp/streamable.go, HandleStreamable generated a bootstrap session for any header-less request and set up deferred cleanup to delete it if the request never authenticated. But the response tail set Mcp-Session-Id unconditionally, so a failed initialize returned a session id that was already deleted — the client's next call got an opaque 401 -32002 unknown session instead of the handshake error.
The claimed premise ("auth isn't persisted across the session") was stale/false: validateAuth stores authenticated/authScope/sessionKey on the mcpSession, and later tools/list/tools/call trust that via the DOGFOOD-101 gate. The real defect was only the header echo.
Fix: track bootstrapped and gate the echo:
if !(rpcErr != nil && bootstrapped && !sess.authenticated) {
w.Header().Set(streamableSessionHeader, sessionID)
}
Verification performed:
- Reproduced the leak on be28a9e^ (worktree + new regression test): failed initialize handed out session ids.
- On the fixed commit: both regression tests pass, go build ./... and go vet ./... clean, go test ./internal/mcp -count=1 ok.
- Full go test ./... -short -parallel 4: all packages pass except demo/TestSmokeKeyless, which I confirmed is environmental/unrelated — the live provider now rejects the ambiguous model id deepseek-chat with HTTP 400 (plus a pprof port conflict).
The write-up includes the exact code diff, test file contents, spec change, and a patch/command form for applying it.
# Evidence - Problem class: go-mcp-streamable-handshake-session-header - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-27T20:17:14.732Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Consensus streamable-HTTP MCP transport had an adjacent failed-handshake defect: a headerless initialize that failed authentication still returned a Mcp-Session-Id header, then immediately deleted that bootstrap session. Clients followed the advertised identifier and received an opaque 401 unknown-session response. The current auth persistence premise was stale: initialize stores authenticated/authScope/sessionKey on the in-memory mcpSession, and later tools/list/tools/call use that state without re-validating the credential. Fix: emit Mcp-Session-Id only when the bootstrap session remains addressable. Regression tests drive the real streamable HTTP handler through initialize, tools/list, tools/call with follow-ups carrying only the session header, and failed initialize cases assert no session header. Verified with go test ./internal/mcp -count=1, go build ./..., go vet ./..., go test ./... -count=1 -short -parallel 4, live scratch binary probes, official MCP client, GitReins verdict PASS, and CI success.", "environment": "Linux, Go repository, streamable HTTP MCP transport", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-mcp-streamable-handshake-session-header", "provider": "openrouter", "solved_at": "2026-09-27T20:17:14.744Z", "version": ""}I diagnosed the defect on the actual repository, reproduced the bug on the pre-fix commit, verified the fix, and wrote the full solution to ~/SOLUTION.md.
Problem class: failed-handshake session-header leak in the streamable HTTP MCP transport.
Root cause: In internal/mcp/streamable.go, HandleStreamable generated a bootstrap session for any header-less request and set up deferred cleanup to delete it if the request never authenticated. But the response tail set Mcp-Session-Id unconditionally, so a failed initialize returned a session id that was already deleted — the client's next call got an opaque 401 -32002 unknown session instead of the handshake error.
The claimed premise ("auth isn't persisted across the session") was stale/false: validateAuth stores authenticated/authScope/sessionKey on the mcpSession, and later tools/list/tools/call trust that via the DOGFOOD-101 gate. The real defect was only the header echo.
Fix: track bootstrapped and gate the echo:
if !(rpcErr != nil && bootstrapped && !sess.authenticated) {
w.Header().Set(streamableSessionHeader, sessionID)
}
Verification performed:
- Reproduced the leak on be28a9e^ (worktree + new regression test): failed initialize handed out session ids.
- On the fixed commit: both regression tests pass, go build ./... and go vet ./... clean, go test ./internal/mcp -count=1 ok.
- Full go test ./... -short -parallel 4: all packages pass except demo/TestSmokeKeyless, which I confirmed is environmental/unrelated — the live provider now rejects the ambiguous model id deepseek-chat with HTTP 400 (plus a pprof port conflict).
The write-up includes the exact code diff, test file contents, spec change, and a patch/command form for applying it.
# Evidence - Problem class: go-mcp-streamable-handshake-session-header - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-27T20:17:14.732Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Consensus streamable-HTTP MCP transport had an adjacent failed-handshake defect: a headerless initialize that failed authentication still returned a Mcp-Session-Id header, then immediately deleted that bootstrap session. Clients followed the advertised identifier and received an opaque 401 unknown-session response. The current auth persistence premise was stale: initialize stores authenticated/authScope/sessionKey on the in-memory mcpSession, and later tools/list/tools/call use that state without re-validating the credential. Fix: emit Mcp-Session-Id only when the bootstrap session remains addressable. Regression tests drive the real streamable HTTP handler through initialize, tools/list, tools/call with follow-ups carrying only the session header, and failed initialize cases assert no session header. Verified with go test ./internal/mcp -count=1, go build ./..., go vet ./..., go test ./... -count=1 -short -parallel 4, live scratch binary probes, official MCP client, GitReins verdict PASS, and CI success.", "environment": "Linux, Go repository, streamable HTTP MCP transport", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-mcp-streamable-handshake-session-header", "provider": "openrouter", "solved_at": "2026-09-27T20:17:14.744Z", "version": ""}