Classify logging/setLevel as a Legacy-only method - #6184
Conversation
A dual-era backend can negotiate 2026-07-28 over a Legacy initialize handshake. The client must then stamp MCP-Protocol-Version: 2026-07-28 on every subsequent request, per the lifecycle spec. But logging/setLevel is Legacy-only -- Modern carries the level per-request in the reserved io.modelcontextprotocol/logLevel _meta key instead -- so go-sdk sends it with no Modern _meta to pair with the header. The classifier treated that as an inconsistent Modern request and rejected it with -32020. go-sdk clients treat the rejection as fatal: the session closes and the in-flight call fails. Observed with vMCP driving github-mcp-server v1.6.0 over stdio, where every tool call died on the logging/setLevel that precedes it -- health checks and tools/list stayed green because only the tool-call path enables backend logging. Generalise the existing initialize short-circuit into a legacyOnlyMethods set and add logging/setLevel to it. Both are methods with no Modern counterpart, so a Modern protocol header can never make them Modern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jhrozek
left a comment
There was a problem hiding this comment.
The failing Tests / Test Go Code check isn't a flake, it's a real, deterministic conflict with an existing test.
TestIntegration_Modern_RealBackend_LoggingContract in pkg/vmcp/server/modern_realbackend_integration_test.go (around lines 398-434) pins the exact contract this PR replaces. Its doc comment and both subtests assert that a well-formed Modern logging/setLevel should be rejected as method-not-found (404 + -32601 from dispatchModern), and a malformed one should get 400 + -32020 (HeaderMismatch).
Now that ClassifyRevision treats logging/setLevel as unconditionally Legacy, neither request reaches those code paths anymore. The well-formed case gets routed as Legacy and 400s with a plain-text "protocol version 2026-07-28 is only supported on stateless HTTP servers" body instead of the expected JSON-RPC error, and the malformed case fails at json.Unmarshal for the same reason (the response isn't JSON).
This test (and its doc comment, which explicitly documents the now-superseded contract) needs to be updated to reflect the new Legacy classification before this can merge. Re-running CI won't help since the failure is deterministic, not timing-related.
Summary
vMCP tool calls against a dual-era stdio backend fail, while the gateway reports healthy. Reproduced against
github-mcp-serverv1.6.0 over stdio:The chain:
mcpcompat/clientdoes not forward the caller's requestedProtocolVersion, so go-sdk negotiates its own latest --2026-07-28-- even over a Legacyinitialize. A dual-era backend accepts it and echoes it back.MCP-Protocol-Version: 2026-07-28on every subsequent request.logging/setLevelis Legacy-only: Modern carries the level per-request in the reservedio.modelcontextprotocol/logLevel_metakey instead (seeMetaKeyLogLevel), and SEP-2577 deprecates even that. go-sdk therefore sends it with no Modern_metato pair with the header.ClassifyRevisionsaw a Modern header with no Modern_meta, called it an inconsistent Modern request, and rejected it with-32020.Generalise the existing
initializeshort-circuit into alegacyOnlyMethodsset and addlogging/setLevel. Both are methods with no Modern counterpart, so a Modern protocol header can never make them Modern, and neither should reach the header/_metaconsistency check.Type of change
Test plan
task test)task lint-fix)Two
TestClassifyRevisioncases:logging/setLevelwith a Modern header and no_meta, and with spoofed Modern_meta— both must classify Legacy, mirroring the existinginitializepair.End-to-end A/B against
github-mcp-serverv1.6.0 over stdio, driving a realtools/callthrough a vMCP gateway:tools/callorigin/mainclient is closing: sending "logging/setLevel"pkg/mcp/...andpkg/transport/...pass with-race.task lint-fixis clean on the changed files; the remainingcmd/thv/app/upgrade.gogosec G115 finding is pre-existing and untouched.Does this introduce a user-facing change?
Yes. vMCP can call tools on dual-era stdio backends again —
github-mcp-serverv1.6.0 among them. Before this, such a gateway reportedReadywith all health checks passing and still failed every tool call.Special notes for reviewers
Why this hid behind green health checks.
enableBackendLoggingis called only fromlegacyCallTool— not fromListCapabilities, resources or prompts. Health checks andtools/listnever sendlogging/setLevel, so they stayed green while every tool call died. Worth knowing when reading the vMCP health status: it does not exercise the tool-call path.Regression window. The strict classifier entered the streamable proxy in #5839/#5884 and shipped in v0.41.0; v0.40.1's proxy answers the same request with
{"result":{}}. Verified by curling both builds directly.The deeper cause is upstream.
mcpcompat/client.Initializesilently drops the caller'sProtocolVersion(zero references to the field), so no caller can control what it negotiates — that is also what produced #6154. Fixing it intoolhive-corewould remove the surprise, but this change is worth making regardless: a dual-era backend can legitimately negotiate a Modern version over a Legacy handshake, and a Legacy-only method must stay classifiable after it does.Generated with Claude Code