Skip to content

Commit 793eb55

Browse files
authored
Make forwarding tests' Legacy dependency explicit (#6051)
The seven real-backend forwarding tests exercise server-initiated elicitation and sampling, which MCP 2026-07-28 removes outright: go-sdk's assertServerInitiatedRequestAllowed refuses them on negotiated protocol version alone, never consulting client capabilities. They passed only because newRealTestServer leaves Modern dispatch disabled, so the downstream client landed on Legacy incidentally. Pin the downstream client to Legacy explicitly by answering its server/discover probe with a successful Legacy-only DiscoverResult -- what a Legacy-only server actually sends -- and assert the pin fired, so deleting it fails loudly instead of silently relying on a server-side default. Two of the seven passed vacuously without the pin: a sessionless connect error satisfied their lenient assertions regardless of the capability advertisement they exist to exercise. Verified by falsifying their premise -- with the capability granted they pass unpinned and fail pinned. Add Modern-path tests recording today's actual behaviour for progress and logging, so all five dispositions are executable rather than prose, and document why each is what it is: progress is pinned because dispatchModern cannot stream, not because Modern lacks the channel. Refs #5959, #6033, #5743, #6058, #6062.
1 parent 33023da commit 793eb55

3 files changed

Lines changed: 488 additions & 18 deletions

File tree

docs/arch/10-virtual-mcp-architecture.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,98 @@ multi-round tool retrieval (MRTR), so the fix is shaped MRTR-first rather than b
289289
extending the SSE standalone-stream model — see the epic (#5743) and the
290290
mid-call forwarding section below for the Legacy behaviour this contrasts with.
291291

292+
### Limitation: elicitation and sampling are unavailable to Modern clients
293+
294+
The client edge mirrors the backend edge. The Modern dispatcher
295+
(`pkg/vmcp/server`'s `dispatchModern`) is single-shot: every result it builds is
296+
`resultType: "complete"`, and it never emits `"input_required"` — MRTR
297+
(SEP-2322) is unimplemented on this edge too. When a backend tool issues a
298+
mid-call server-initiated request during a **Modern** client's `tools/call`,
299+
there is no client session to forward it to, so the call fails with an explicit
300+
`-32603` whose message names the refused request (pinned by
301+
`TestIntegration_Modern_RealBackend_ElicitingToolFailsCleanly`). This is a
302+
deliberate honest-unsupported error, not a gap left by accident:
303+
304+
**The `-32603` is a documented deviation, not the spec's answer — and the
305+
spec's answer is unshippable today.** For a client that did NOT declare the
306+
needed capability in its per-request `clientCapabilities`, SEP-2575 MUSTs a
307+
`-32021` `MissingRequiredClientCapabilityError` at **HTTP 400**, with
308+
explicitly execution-time language ("if processing a request requires a
309+
capability…") — go-sdk's own doc comment on the error type tells handlers to
310+
return it mid-execution, so the mid-call timing is not the problem. The
311+
problem is the transport: go-sdk's streamable client treats any non-transient
312+
4xx (its transient set is only 500/502/503/504/429) as a **connection**
313+
failure — `checkResponse``fail()` → a one-shot, permanent session death —
314+
so a conformant 400 would tear down the entire client session to punish one
315+
call. And for a client that DID declare the capability, the 2026-07-28
316+
vocabulary has no conformant code at all: no "operation not supported", MRTR
317+
is not a server-advertised capability, and SEP-2322 has no decline mechanism.
318+
The planned follow-up therefore serves `-32021` (with
319+
`data.requiredCapabilities`, and a message naming both the capability and the
320+
gateway limitation) at **HTTP 200** for the undeclared case — deviating from
321+
the mandated 400 for exactly the reason above — and keeps `-32603` for the
322+
declared case as a documented spec gap. Until that lands, both cases surface
323+
as `-32603`.
324+
325+
**A clean error does not mean nothing happened.** The refusal reaches the
326+
backend mid-call, so a real backend tool may have executed — including side
327+
effects — up to the point it demanded input. A Modern client receiving this
328+
error must not assume the call was side-effect-free. (The integration
329+
fixture's tools elicit as their first action, so the tests cannot exhibit
330+
this; production tools can.)
331+
332+
- The 2026-07-28 revision **removed** server-initiated requests; go-sdk's
333+
`ServerSession.assertServerInitiatedRequestAllowed` refuses
334+
elicitation/sampling/roots purely by negotiated protocol version, so no
335+
capability negotiation can restore the Legacy forwarding model for Modern
336+
clients.
337+
- A server that never returns `input_required` is fully SEP-2575-conformant:
338+
the per-request `clientCapabilities` a client declares are an offer the
339+
server may use, not an obligation.
340+
- SEP-2577 deprecates sampling (and logging and roots) outright as of
341+
2026-07-28, with direct LLM-provider integration as the sanctioned
342+
replacement — so elicitation is the only durable consumer a future MRTR
343+
implementation would serve.
344+
345+
Legacy clients keep the full mid-call forwarding behaviour unchanged; the
346+
forwarding integration tests pin their downstream clients to Legacy explicitly
347+
(`legacyPinningRoundTripper` in `pkg/vmcp/server`'s external test package)
348+
because that surface exists only on a Legacy session.
349+
350+
**Bridging was considered, costed, and rejected.** Serving MRTR to Modern
351+
clients on top of a *Legacy*
352+
backend would require parking the live, mid-flight backend call server-side
353+
(the blocked goroutine and its open session cannot be serialized into the
354+
opaque `requestState` the SEP designed for handler re-invocation) and keying
355+
the resume on an unguessable token — per-round server state with TTL/eviction,
356+
identity binding on a token that becomes a capability to resume someone else's
357+
in-flight call, and replica affinity with no `Mcp-Session-Id` to route on. That
358+
would reintroduce, in different clothes, the per-request server state the
359+
2026-07-28 revision removed. The spec's own sanctioned path for genuinely stateful
360+
`input_required` work is the **Tasks** extension (SEP-2663: `tools/call`
361+
returns `resultType: "task"` with a `taskId`; the client polls `tasks/get` and
362+
answers outstanding `inputRequests` via `inputResponses` on `tasks/update`;
363+
note SEP-2663 supersedes SEP-1686 and removed the blocking `tasks/result`
364+
method for the same reasons argued here) — if Modern-client elicitation over
365+
Legacy backends is ever truly demanded, that is the machinery to reach for,
366+
not parked `tools/call`.
367+
368+
The coherent future MRTR shape for a re-aggregating gateway is
369+
**Modern-client ↔ Modern-backend pass-through** — relay a Modern backend's
370+
`inputRequests`/`requestState` to the client and the client's
371+
`inputResponses` back, genuinely stateless at vMCP. It requires the egress
372+
half first (today a Modern backend's `input_required` surfaces as
373+
`errModernInputRequired`, the seam left in `pkg/vmcp/client`), and by the time
374+
Modern backends exist to relay from, SEP-2577's deprecations make elicitation
375+
its only durable consumer; see #5743.
376+
377+
Progress and log notifications toward Modern clients are a separate concern
378+
from MRTR: they remain spec-legal as request-scoped notifications on the
379+
POST-initiated SSE response stream (SEP-2260 requires messages on that stream
380+
to relate to the originating request; `progressToken` is unchanged), which the
381+
single-shot dispatcher does not produce today — a vMCP streaming-dispatch gap,
382+
not a spec absence.
383+
292384
## Served MCP Capabilities
293385

294386
Beyond tools, vMCP aggregates and serves the full complement of MCP capabilities. Every served capability flows through the domain **core** (`pkg/vmcp/core`), so the same admission decision that filters `tools/list` also gates reads, gets, and completions.
@@ -484,6 +576,13 @@ While a backend `tools/call` (or other request) is in flight, the backend may is
484576

485577
**Implementation**: `pkg/vmcp/forwarding.go`, `pkg/vmcp/client/forwarding.go`, `pkg/vmcp/server/serve_handlers.go`
486578

579+
**Known limitation (Modern clients)**: everything in this section describes a
580+
**Legacy (2025-11-25) client session**. For Modern (2026-07-28) clients there
581+
is no session and no server-initiated request channel, so none of this
582+
forwarding applies — see
583+
[Limitation: elicitation and sampling are unavailable to Modern clients](#limitation-elicitation-and-sampling-are-unavailable-to-modern-clients)
584+
for what a Modern caller gets instead.
585+
487586
**Known limitation (logging level)**: forwarded backend logging is not yet filtered to the downstream client's requested `logging/setLevel`. vMCP requests debug-level logging from the backend so it emits `notifications/message`, and every such notification is forwarded — the downstream client's own level preference is not applied to the relayed stream.
488587

489588
**Known limitation (resource-template authorization)**: a resource template is advertised on the template-string entity (e.g. `file:///logs/{date}.txt`), but a concrete read is admission-checked on the **expanded** URI (e.g. `file:///logs/2025-01-01.txt`). Operators should therefore author resource authorization policies against concrete URI patterns, not the template string.

0 commit comments

Comments
 (0)