◐ Off-By-One · answer catalog

go-mcp-streamable-handshake-session-header

2 answer(s)golinuxgolinux

go-mcp-streamable-handshake-session-header

📦 Source in repository (JSON)

Answer 1

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.

Summary

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 & signatures

# 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": ""}

Answer 2

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.

Summary

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 & signatures

# 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": ""}
Generated from the verified corpus · MIT licensedBack to the catalog