Skip to content

vMCP: Modern backends never emit notifications/message — no per-request logLevel _meta opt-in #6130

Description

@JAORMX

Background

Upstream resolution of modelcontextprotocol/go-sdk#1116 (closed 2026-07-28, maintainer comment, no fix PR) confirmed that logging/setLevel is removed from the MCP 2026-07-28 ("Modern") method surface, not merely deprecated. Verified against modelcontextprotocol/modelcontextprotocol@main schema/draft/schema.ts:

  • ClientRequest is a closed union with no SetLevelRequest — the method does not exist on Modern.
  • RequestMetaObject carries "io.modelcontextprotocol/logLevel"?: LoggingLevel, documented as: "If absent, the server MUST NOT send any notifications/message notifications for this request. The client opts in to log messages by explicitly setting a level. Replaces the former logging/setLevel RPC." (itself @deprecated per SEP-2577, twelve-month window).
  • Logging survives on Modern only as the deprecated notifications/message notification plus the deprecated logging server capability.

go-sdk's ClientSession.SetLoggingLevel therefore stays broken-by-design on Modern sessions (it sends a Modern protocol header with no _meta.protocolVersion, and the method it targets no longer exists). Callers are expected to stop using it there and set the per-request logLevel _meta key instead.

Impact on ToolHive

The pinned Modern contract is validated — no change needed to the rejection behavior:

  • pkg/vmcp/server/modern_realbackend_integration_test.go:398-466 — well-formed Modern logging/setLevel → 404/-32601 (matches go-sdk's own server); go-sdk's malformed variant → 400/-32020 HeaderMismatch. Both are the spec-correct answers.
  • pkg/mcp/revision.go:59-133 already models io.modelcontextprotocol/logLevel correctly: in the strip set (per-hop key, terminated at the vMCP boundary) but excluded from modernSignalMetaKeys (presence alone does not claim Modern).
  • The streamable proxy's Legacy logging/setLevel interception + max-verbosity reconciliation (pkg/transport/proxy/streamable/streamable_proxy.go:1187-1204, :1429-1453) is unaffected — it only runs on session-bearing Legacy requests and hand-rolls the upstream call.

The real gap: with logging, a downstream client of vMCP gets no notifications/message from a Modern backend at all:

  1. enableBackendLogging (pkg/vmcp/client/forwarding.go:51-63) calls c.SetLoggingLevel(ctx, mcp.LoggingLevelDebug) — but only from legacyCallTool (pkg/vmcp/client/client.go:1701), so it never runs against a Modern-classified backend (and would fail there anyway, per above).
  2. The Modern path — modernCallTool (pkg/vmcp/client/client.go:1638-1672) via modernCall (pkg/vmcp/client/modern.go:143-178) — mints the per-request _meta but never sets io.modelcontextprotocol/logLevel, so per the draft schema a conformant Modern backend MUST NOT emit notifications/message for the request.

So the mid-call logging forwarding that Legacy sessions get (backend emits at debug → notification forwarder relays downstream) has no Modern equivalent, even though the schema's opt-in semantics are exactly what the forwarder wants: setting logLevel on the request is what permits the backend to send notifications/message for that request.

Proposed fix

On the Modern backend path, inject io.modelcontextprotocol/logLevel: "debug" into the request _meta vMCP mints in modernCall (mirroring the Legacy enableBackendLogging intent: ask for debug so the backend emits, relay everything downstream — per the documented limitation in docs/arch/10-virtual-mcp-architecture.md:807, downstream level filtering is already out of scope).

Scope is small and self-contained:

  • pkg/vmcp/client/modern.go / client.go:1638-1672 — overlay the logLevel key into the minted _meta on tools/call (and any other Modern request where backend log relay is wanted), gated on the same "forwarders bound" condition the Legacy path uses.
  • pkg/vmcp/client/forwarding.go:46-63 — doc comment: note the RPC is Legacy-only and the Modern equivalent is the _meta key.

Follow-up hygiene (same PR or separate)

  • Reframe the two pin comments that attribute the -32020 rejection to an "upstream bug, filed separately" — pkg/vmcp/server/forwarding_realbackend_integration_test.go:476-485 and pkg/vmcp/server/modern_realbackend_integration_test.go:405-410. With upstream's wont-fix-by-design resolution, the malformed wire shape is a permanent fixture of go-sdk v1.7.x; the -32020 arm is the standing contract for any caller still using the removed RPC on a Modern session, not a temporary bug guard.
  • docs/arch/10-virtual-mcp-architecture.md:807 — the logging limitation gains a second dimension: on Modern, downstream clients express their level per-request via logLevel _meta, which vMCP currently strips without honoring.

Priority

Low. SEP-2577 deprecates the entire logging feature (the logLevel key itself is deprecated with a twelve-month window), and the failure mode today is silent absence of a best-effort notification stream, not an error. Worth doing while the deprecated feature is still in the window so Modern backends get parity with Legacy for the remaining deprecation period.

Refs: go-sdk#1116 (resolution), go-sdk#1112 and go-sdk#1113 (siblings, still open), SEP-2575, SEP-2577, MCP schema/draft/schema.ts (RequestMetaObject, ClientRequest union).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    loggingmcpneeds-triageIssue needs initial triage by a maintainervmcpVirtual MCP Server related issues

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions