Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions docs/arch/10-virtual-mcp-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,98 @@ multi-round tool retrieval (MRTR), so the fix is shaped MRTR-first rather than b
extending the SSE standalone-stream model — see the epic (#5743) and the
mid-call forwarding section below for the Legacy behaviour this contrasts with.

### Limitation: elicitation and sampling are unavailable to Modern clients
Comment thread
JAORMX marked this conversation as resolved.

The client edge mirrors the backend edge. The Modern dispatcher
(`pkg/vmcp/server`'s `dispatchModern`) is single-shot: every result it builds is
`resultType: "complete"`, and it never emits `"input_required"` — MRTR
(SEP-2322) is unimplemented on this edge too. When a backend tool issues a
mid-call server-initiated request during a **Modern** client's `tools/call`,
there is no client session to forward it to, so the call fails with an explicit
`-32603` whose message names the refused request (pinned by
`TestIntegration_Modern_RealBackend_ElicitingToolFailsCleanly`). This is a
deliberate honest-unsupported error, not a gap left by accident:

**The `-32603` is a documented deviation, not the spec's answer — and the
spec's answer is unshippable today.** For a client that did NOT declare the
needed capability in its per-request `clientCapabilities`, SEP-2575 MUSTs a
`-32021` `MissingRequiredClientCapabilityError` at **HTTP 400**, with
explicitly execution-time language ("if processing a request requires a
capability…") — go-sdk's own doc comment on the error type tells handlers to
return it mid-execution, so the mid-call timing is not the problem. The
problem is the transport: go-sdk's streamable client treats any non-transient
4xx (its transient set is only 500/502/503/504/429) as a **connection**
failure — `checkResponse` → `fail()` → a one-shot, permanent session death —
so a conformant 400 would tear down the entire client session to punish one
call. And for a client that DID declare the capability, the 2026-07-28
vocabulary has no conformant code at all: no "operation not supported", MRTR
is not a server-advertised capability, and SEP-2322 has no decline mechanism.
The planned follow-up therefore serves `-32021` (with
`data.requiredCapabilities`, and a message naming both the capability and the
gateway limitation) at **HTTP 200** for the undeclared case — deviating from
the mandated 400 for exactly the reason above — and keeps `-32603` for the
declared case as a documented spec gap. Until that lands, both cases surface
as `-32603`.

**A clean error does not mean nothing happened.** The refusal reaches the
backend mid-call, so a real backend tool may have executed — including side
effects — up to the point it demanded input. A Modern client receiving this
error must not assume the call was side-effect-free. (The integration
fixture's tools elicit as their first action, so the tests cannot exhibit
this; production tools can.)

- The 2026-07-28 revision **removed** server-initiated requests; go-sdk's
`ServerSession.assertServerInitiatedRequestAllowed` refuses
elicitation/sampling/roots purely by negotiated protocol version, so no
capability negotiation can restore the Legacy forwarding model for Modern
clients.
- A server that never returns `input_required` is fully SEP-2575-conformant:
the per-request `clientCapabilities` a client declares are an offer the
server may use, not an obligation.
- SEP-2577 deprecates sampling (and logging and roots) outright as of
2026-07-28, with direct LLM-provider integration as the sanctioned
replacement — so elicitation is the only durable consumer a future MRTR
implementation would serve.

Legacy clients keep the full mid-call forwarding behaviour unchanged; the
forwarding integration tests pin their downstream clients to Legacy explicitly
(`legacyPinningRoundTripper` in `pkg/vmcp/server`'s external test package)
because that surface exists only on a Legacy session.

**Bridging was considered, costed, and rejected.** Serving MRTR to Modern
clients on top of a *Legacy*
backend would require parking the live, mid-flight backend call server-side
(the blocked goroutine and its open session cannot be serialized into the
opaque `requestState` the SEP designed for handler re-invocation) and keying
the resume on an unguessable token — per-round server state with TTL/eviction,
identity binding on a token that becomes a capability to resume someone else's
in-flight call, and replica affinity with no `Mcp-Session-Id` to route on. That
would reintroduce, in different clothes, the per-request server state the
2026-07-28 revision removed. The spec's own sanctioned path for genuinely stateful
`input_required` work is the **Tasks** extension (SEP-2663: `tools/call`
returns `resultType: "task"` with a `taskId`; the client polls `tasks/get` and
answers outstanding `inputRequests` via `inputResponses` on `tasks/update`;
note SEP-2663 supersedes SEP-1686 and removed the blocking `tasks/result`
method for the same reasons argued here) — if Modern-client elicitation over
Legacy backends is ever truly demanded, that is the machinery to reach for,
not parked `tools/call`.

The coherent future MRTR shape for a re-aggregating gateway is
**Modern-client ↔ Modern-backend pass-through** — relay a Modern backend's
`inputRequests`/`requestState` to the client and the client's
`inputResponses` back, genuinely stateless at vMCP. It requires the egress
half first (today a Modern backend's `input_required` surfaces as
`errModernInputRequired`, the seam left in `pkg/vmcp/client`), and by the time
Modern backends exist to relay from, SEP-2577's deprecations make elicitation
its only durable consumer; see #5743.

Progress and log notifications toward Modern clients are a separate concern
from MRTR: they remain spec-legal as request-scoped notifications on the
POST-initiated SSE response stream (SEP-2260 requires messages on that stream
to relate to the originating request; `progressToken` is unchanged), which the
single-shot dispatcher does not produce today — a vMCP streaming-dispatch gap,
not a spec absence.

## Served MCP Capabilities

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.
Expand Down Expand Up @@ -484,6 +576,13 @@ While a backend `tools/call` (or other request) is in flight, the backend may is

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

**Known limitation (Modern clients)**: everything in this section describes a
**Legacy (2025-11-25) client session**. For Modern (2026-07-28) clients there
is no session and no server-initiated request channel, so none of this
forwarding applies — see
[Limitation: elicitation and sampling are unavailable to Modern clients](#limitation-elicitation-and-sampling-are-unavailable-to-modern-clients)
for what a Modern caller gets instead.

**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.

**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.
Expand Down
Loading
Loading