Skip to content

Skip logging/setLevel on a Modern-negotiated session - #6184

Merged
amirejaz merged 2 commits into
mainfrom
classify-legacy-only-methods
Aug 6, 2026
Merged

Skip logging/setLevel on a Modern-negotiated session#6184
amirejaz merged 2 commits into
mainfrom
classify-legacy-only-methods

Conversation

@amirejaz

@amirejaz amirejaz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

vMCP tool calls against a dual-era stdio backend fail while the gateway reports healthy. Reproduced against github-mcp-server v1.6.0 over stdio:

backend unavailable: tool call failed on backend gh-c: connection closed:
  calling "tools/call": client is closing: sending "logging/setLevel":
  MCP-Protocol-Version header "2026-07-28" does not match _meta protocol version "": Bad Request

The chain:

  1. github-mcp-server v1.6.0 negotiates 2026-07-28 over the Legacy initialize handshake, so the session must carry that version on every subsequent request.
  2. logging/setLevel was removed in 2026-07-28 — the per-request io.modelcontextprotocol/logLevel _meta key replaces it, and the Modern path already mints it (modern.go).
  3. go-sdk nonetheless sends setLevel as a Legacy-shaped call, so it arrives with a Modern protocol header and no Modern _meta.
  4. The classifier rejects that shape with -32020, and go-sdk treats the rejection as fatal — the session closes and the tool call dies with it.

Skip the RPC when the negotiated version is Modern. Sessions that negotiate a Legacy version still get it. legacyInit now returns the negotiated version so the call site can decide; the four callers that do not need it discard it.

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)
  • Manual testing (described below)

TestEnableBackendLogging_SkipsRemovedRPCOnModernSession is table-driven over both negotiated versions and asserts against a recording backend: Modern → not sent, Legacy → still sent. Verified to fail without the production change (expected: false, actual: true on the Modern case).

End-to-end A/B, real tools/call through a vMCP gateway to github-mcp-server v1.6.0 over stdio:

tools/call
origin/main failsclient is closing: sending "logging/setLevel"
this branch succeeds — returns the live GitHub payload

Full task test run: only the 10 pre-existing pkg/plugins/pluginsvc failures, which reproduce identically on clean origin/main. pkg/vmcp/server — including TestIntegration_Modern_RealBackend_LoggingContract — is green.

Does this introduce a user-facing change?

Yes. vMCP can call tools on dual-era stdio backends again, github-mcp-server v1.6.0 among them. Before this, such a gateway reported Ready with every health check passing and still failed every tool call.

Special notes for reviewers

This PR changed approach in place (force-pushed) rather than opening a new one, so the review history stays here. It previously classified logging/setLevel as Legacy-only in ClassifyRevision; it now leaves the classifier untouched and fixes the caller.

Why not loosen the classifier. The original approach classified logging/setLevel as Legacy-only. @jhrozek correctly caught that it breaks TestIntegration_Modern_RealBackend_LoggingContract, whose comment records a deliberate contract: go-sdk closed the upstream report wont-fix-by-design, so -32020 is the standing answer for a caller using a removed RPC on a Modern session. That contract is right and this change leaves it intact. What it did not anticipate is the blast radius — its "worse than the failing upstream call" tradeoff assumed only the logging call fails, but the rejection closes the session and takes the tool call with it. Fixing the caller rather than the classifier resolves that without weakening the contract.

Why this hid behind green health checks. enableBackendLogging is called only from legacyCallTool — not from ListCapabilities, resources or prompts. Health checks and tools/list never send setLevel, so they stayed green while every tool call died. Worth knowing when reading vMCP health status: it does not exercise the tool-call path.

Related, and complementary: #6188. That report hits the same -32020 rejection from a different angle — the ChatGPT connector sends tools/call with a Legacy MCP-Protocol-Version: 2025-11-25 header plus a reserved _meta signal key and no _meta protocolVersion. I verified both shapes against the classifier directly:

tools/call     header=2025-11-25  meta={clientInfo}  -> -32020   (#6188)
logging/setLevel header=2026-07-28  meta=nil          -> -32020   (this PR)

The two need different fixes and neither subsumes the other. #6188's proposal keys off an explicitly non-Modern header, so it cannot help this case (the header here is Modern); and this caller-side skip cannot help theirs, since tools/call has a Modern counterpart and can never be treated as Legacy-only. #6188's own analysis of that is correct.

Regression window. The strict classifier shipped in v0.41.0 (#5839/#5884); v0.40.1's proxy answers the same request {"result":{}}. Verified by curling both builds directly.

Generated with Claude Code

@jhrozek jhrozek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

vMCP tool calls against a dual-era stdio backend failed while the gateway
reported healthy. github-mcp-server v1.6.0 negotiates 2026-07-28 over the
LEGACY initialize handshake, so the session must then carry that version
on every request. go-sdk still sends logging/setLevel as a Legacy-shaped
call, producing a Modern protocol header with no Modern _meta -- a shape
the classifier rejects with -32020. go-sdk treats that as fatal, closing
the session and killing the tool call the logging was only meant to
decorate.

The 2026-07-28 revision REMOVED logging/setLevel; the per-request
io.modelcontextprotocol/logLevel _meta key replaces it, and the Modern
path already mints it. Calling an RPC the negotiated revision removed is
the defect, so skip it there. Sessions that negotiate a Legacy version
still get it.

Rejecting the malformed shape is deliberate and pinned by
TestIntegration_Modern_RealBackend_LoggingContract, whose comment records
that go-sdk closed the upstream report wont-fix-by-design. Loosening the
classifier instead would have broken that contract; what the contract's
"worse than the failing upstream call" tradeoff did not anticipate is
that the rejection is fatal to the whole session, not just to the logging
call.

legacyInit now returns the negotiated version so the call site can make
this decision; the four callers that do not need it discard it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@amirejaz
amirejaz force-pushed the classify-legacy-only-methods branch from 0e99a07 to 4145394 Compare August 5, 2026 11:39
@amirejaz amirejaz changed the title Classify logging/setLevel as a Legacy-only method Skip logging/setLevel on a Modern-negotiated session Aug 5, 2026
…methods

# Conflicts:
#	pkg/vmcp/client/revision_realbackend_test.go
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Aug 5, 2026
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.47%. Comparing base (227ee7b) to head (cd8f428).

Files with missing lines Patch % Lines
pkg/vmcp/client/client.go 80.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6184   +/-   ##
=======================================
  Coverage   72.46%   72.47%           
=======================================
  Files         739      739           
  Lines       76719    76723    +4     
=======================================
+ Hits        55594    55602    +8     
+ Misses      17160    17155    -5     
- Partials     3965     3966    +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@amirejaz
amirejaz merged commit f178d9e into main Aug 6, 2026
48 checks passed
@amirejaz
amirejaz deleted the classify-legacy-only-methods branch August 6, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants