feat(mcp): support protocol revision 2026-07-28 - #4044
Conversation
346edb3 to
06d63f1
Compare
|
👋 This PR has merge conflicts with the base branch. Please rebase or merge the latest base branch and resolve them. I've moved it to draft and added |
aheritier
left a comment
There was a problem hiding this comment.
Reviewed at 06d63f1 with CI green (19/19 checks). I independently rebuilt and re-validated this branch in a clean worktree: go build ./... OK, go mod tidy -diff clean, go test -race ./pkg/mcp/... ./cmd/root/... ./pkg/tools/mcp/... ./pkg/tools/builtin/mcpcatalog/... ./pkg/runtime/... all pass, and go test ./e2e -run 'TestExec_Gemini_SamplingWithTools|TestMCP_' passes (3/3).
The implementation matches issue #3952 point for point, and the new HTTP tests are the real thing — they drive the production newStreamableHTTPHandler over a real httptest server rather than an in-process handler, which was exactly the gap called out in the investigation comment.
Compatibility evidence I generated (beyond the PR's own tests)
Because the main open question on #3952 was "will this break existing clients", I built a real pre-2026-07-28 client (go-sdk v1.6.1) and pointed it at the new stateless handler:
- connect →
2025-11-25negotiated,tools/list→ OK,tools/call→ OK,Close()→ OK. - Repeated with a client registering
ToolListChangedHandler/LoggingMessageHandler(so the SDK opens the standalone GET SSE stream): the405on GET is tolerated, the session survives, and subsequent calls still succeed.
So stateless mode does not break go-sdk-based legacy clients.
I also confirmed enabling Stateless: true is safe for our served surface: createToolHandler (pkg/mcp/server.go:236) runs the agent with WithNonInteractive(true) via rt.Run and never touches req.Session, and there is no Elicit/CreateMessage/notification call anywhere in pkg/mcp. We issue no server→client requests, which is precisely the thing stateless mode forbids.
Finally, the .golangci.yml SA1019 exclusion is necessary and correctly scoped — with the block removed, golangci-lint run --max-issues-per-linter=0 --max-same-issues=0 reports sampling deprecations in exactly the listed paths (pkg/runtime/sampling.go, pkg/runtime/sampling_test.go, pkg/tools/mcp/mcp.go, pkg/tools/mcp/session_client.go, pkg/tools/mcp/session_client_test.go, pkg/tools/sampling.go, pkg/tools/codemode/codemode_test.go). No dead entries.
[should-fix] No coverage for the MRTR elicitation path, which this bump silently rewires
The investigation comment on #3952 asked for "MRTR elicitation/sampling retry coverage". Sampling has it — e2e/sampling_test.go pairs a real gomcp server over StreamableHTTPHandler with our client and passes. Elicitation has none: no *_test.go in the repo pairs an SDK client and server for elicitation (every file referencing ElicitationHandler has zero mcp.NewClient references), so the retry path is entirely unexercised.
This matters because the semantics genuinely changed under 2026-07-28. Server-side inline elicitation is now rejected outright:
"elicitation/create" cannot be sent while serving a request on protocol version 2026-07-28:
return an InputRequests map instead (multi round-trip requests, SEP-2322)
Our client wiring (pkg/tools/mcp/remote.go:169, pkg/tools/mcp/stdio.go:51 → ClientOptions.ElicitationHandler) does still work against an MRTR-compliant server — I verified it: a stateless v1.7 server returning CallToolResult.InputRequests{"q1": &mcp.ElicitParams{...}} + RequestState caused our exact handler shape to be invoked once and the retried call to complete with action=accept. That's good news, but it's guaranteed only by SDK middleware today, with nothing in our suite to catch a regression. A hermetic test using that recipe (server returns InputRequests, client asserts the elicitation handler fires and the retry resolves) would close the last item from the agreed plan.
[optional] The mcp-mode doc is more alarming than the observed behaviour
docs/features/mcp-mode/index.md:64 says older stateful clients depending on a standalone GET stream or session DELETE teardown "must upgrade to (or switch to) a client compatible with stateless streaming HTTP". Per the v1.6.1 experiment above, such a client keeps working — the 405s are tolerated and only out-of-band notifications are lost (and we send none). Softening this to "loses out-of-band GET-stream notifications" would set more accurate expectations. Being conservative here is not harmful, so purely your call.
[optional] examples/golibrary/renderer/main.go:127 still constructs NewStreamableHTTPHandler(..., nil) (stateful) while production is now stateless. Harmless for an example, but it no longer mirrors product behaviour.
Not approving yet — one item needs your call, not mine
The remaining gate is the one you raised on #3952 on 2026-08-18: verifying this doesn't break Gordon, Agentic Platform, or existing users. The PR body is candid that those smoke tests "were not available locally", so that validation is still outstanding. My v1.6.1 interop results above should de-risk the go-sdk client case substantially, but non-Go clients and the two downstream products remain unverified.
Worth also confirming you're happy to land this as a single commit — the plan on #3952 proposed splitting "upgrade, preserve behaviour" (step 1) from "enable stateless serving" (step 2), which would have made a revert cheaper if a downstream product does turn out to be affected.
Two intentional behaviour changes for the release notes: --mcp-keepalive now hard-errors with --http (previously accepted) and with --attach (previously silently ignored). Both are correct and well tested (cmd/root/mcp_test.go:43-85, pkg/mcp/server_test.go:50), and runAttach (cmd/root/mcp.go:115) does indeed discard runConfig, so the error message is accurate.
Nice work — this is careful, well-evidenced migration work.
Summary
github.com/modelcontextprotocol/go-sdkfrom v1.6.1 to v1.7.02026-07-28initializecompatibility and update fallback fixturesIssue expectations
go.modandgo.sum2026-07-28over HTTPStreamableHTTPOptions.Statelessin the production handlerserver/discovernegotiationinitialize,notifications/initialized, andtools/listremain accepted with request-local stateMcp-Session-Idand that GET/DELETE return 405readOnlyHintandidempotentHintvalues remain serialized2025-11-25and fixed discover fallback request IDsCompatibility
Validation
task buildtask testtask lintgo build ./...go vet ./...go test -race -count=1 ./pkg/mcp ./pkg/tools/mcp ./pkg/tools/builtin/mcpcataloggo test -count=1 ./e2e -run 'TestExec_Gemini_SamplingWithTools|TestMCP_'npx --yes markdownlint-cli2@0.22.1 features/cli/index.md features/mcp-mode/index.mdgo mod tidy -diffgit diff --checkCloses #3952