Commit graph go-sdk/mcp/mrtr.go
Author SHA1 Message Date
Ronald
e40f35d137
mcp: label resultType on results that bypass the dispatcher (#1226)
Fixes #1225. See the [issue
comment](https://github.com/modelcontextprotocol/go-sdk/issues/1225#issuecomment-5500818546).

## The bug

`2026-07-28` requires `resultType` on every result. `CallToolResult`,
`GetPromptResult`, and `ReadResourceResult` keep `resultType` in an
unexported field. Each of these results can be `complete` or
`input_required`, so it cannot embed `completeResultWithType` the way
the seven types in #1060 do.

Only `handleMultiRoundTripResult` sets that field. It runs inside
`Server.callTool`, `getPrompt`, and `readResource`. Some middleware
returns a result directly instead of calling `next` — for example, an
auth, rate-limit, or entitlement gate. That middleware never reaches
`handleMultiRoundTripResult`. The response then ships without the field,
and strict clients reject it.

## The fix

The choice between `complete` and `input_required` now happens inside
`setMultiRoundTripResultType`. Both call sites that need this choice
call that function. `setCompleteResultType` now labels results it did
not label before, so it is renamed `annotateResultType`. The new
function picks a branch with a type switch.

Reading matters here. `setResultType` is unexported, so setting
`InputRequests` is the only way for middleware to ask for input. If the
code always labels results `complete`, the response ships as
`{"resultType":"complete","inputRequests":{...}}`. A client that trusts
the discriminator then drops the elicitation request.

Recomputing the label every time is safe. A result that did reach the
dispatcher already carries the same label from that test. This also
covers one edge case: a handler that returns `InputRequests` again after
the shim's single reinvocation. Before this fix, that case shipped with
no `resultType`.

Embedding `completeResultWithType` instead is not an option: it labels
everything `complete` and overwrites `input_required`, breaking
multi-round-trip flows.

**Skip nil results.** Middleware can return a typed nil value, which
satisfies the `Result` interface. `handle` now checks for that case
before it calls the labeling function. Before this change, that same
case also panicked, one frame later in `annotateServerInfo` and then
`GetMeta`.

## Checks

- `TestServerSessionHandle_SetsResultTypeWhenMiddlewareShortCircuits`
fails on all three methods before the first change. Its fourth case
fails if labeling overwrites a populated `InputRequests`.
- `TestServerSessionHandle_ResultTypeGate` records which requests carry
`resultType`, including one known gap. See "Separate from this PR"
below.
- `TestServerSessionHandle_NilResultFromMiddleware` panics without the
nil check.
- The complete-path cases added to
`TestServerSessionHandle_SetsResultTypeOnNewProtocol` also pass before
this change. They close a gap in #1060, which added only three cases,
and only for `input_required`.
- The exported API is unchanged. `go doc -all ./mcp`, with comments
stripped, is identical before and after this change.
- With the reproduction from the issue, over the streamable transport:
the dispatched `tools/call` and the short-circuited `tools/call` both
now return `resultType: "complete"`.

I could not run `./scripts/conformance.sh`. CONTRIBUTING.md names that
script, but the repo only has `client-conformance.sh` and
`server-conformance.sh`. Both scripts need GNU `timeout`, and macOS does
not include it.

## Also here

Ten comments named an exported `ResultType` field and
`ResultTypeComplete` / `ResultTypeInputRequired` constants. None of
those exist. Two of the comments were `[CallToolResult.ResultType]` doc
links, and those links cannot resolve. They now point at
`[CallToolResult.NeedsInput]` instead, because an unexported field is
also not a valid link target.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
2026-09-14 09:52:10 +02:00
Haoxin Yang
69c17de411
mcp: don't mutate caller params on multi-round-trip retries (#1145)
Fixes #1144.

`clientMultiRoundTripMiddleware` calls
`setMultiRoundTripRetryParams(req, ...)`, which writes `InputResponses`
and `RequestState` through the caller's params pointer and never clears
them. A `*CallToolParams` (or `GetPromptParams` / `ReadResourceParams`)
struct reused across calls then carries the first call's answers into
the next call: a server gating on `len(req.Params.InputResponses) == 0`
— the idiom in this repo's own tests and examples — treats the fresh
call as already answered, so the client's `ElicitationHandler` never
fires and an elicitation-gated (e.g. destructive) tool runs without
confirmation.

This change makes the retry loop carry the fulfilled responses and
echoed state on a shallow copy of the params
(`multiRoundTripRetryRequest`) instead of mutating the original request.
Caller-owned params are now left untouched by `CallTool`, `GetPrompt`,
and `ReadResource`. The server-side shim
(`serverMultiRoundTripMiddleware`) is unchanged: the params it mutates
are SDK-owned, decoded from the wire.

Tests: `TestMultiRoundTrip_AutoRetryDoesNotMutateCallerParams`
(CallTool: params unchanged after the call, and reusing the same struct
fulfills the input requests again instead of silently replaying the
first call's answers — it failed with `elicitations after reuse = 1,
want 2` before the fix), plus GetPrompt/ReadResource analogues asserting
params are left clean. All three fail on main and pass with this change;
`go test ./...` and `go vet ./...` are clean.

Note on observable behavior: callers can no longer read the fulfilled
responses off their own params struct after a call returns. That side
effect was undocumented, and relying on it is exactly what makes the
replay hazard above possible.

---------

Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
2026-08-31 13:53:34 +02:00
Guglielmo Colombo
17029489ff
mcp: bump protocol version from 2026-06-30 to 2026-07-28 (#1015)
The new official date for the protocol version has been moved to
'2026-07-28'. Update all the references in the code of the old version.
2026-06-22 09:53:04 +02:00
Yaroslav
189a85ad92
feat: multi-round-trip request implementation (SEP-2322) (#950)
### Context

Design (`design/mrtr.md`) and implementation proposal for Multi
Round-Trip Requests (MRTR) per
[SEP-2322](de6d76fba3/seps/2322-MRTR.md).

### Changes

- Introduce `InputRequest` and `InputResponse` sealed interfaces with
corresponding `InputRequestMap` and `InputResponseMap` types for custom
map JSON codec implementation.
- Extend `CallToolParams`, `GetPromptParams`, and `ReadResourceParams`
with `InputResponses`, `RequestState` and an unexported `resultType`
("complete" or "input_required") for retry round-trips. The type is set
by the SDK based on input requests presence.
- Add client-side MRTR middleware enabled by default that automatically
fulfills input requests and retries.
- Added MRTROptions on ClientOptions to allow disabling the middleware
and configuring the number of retries.
- Add server-side MRTR middleware for backward compatibility:
transparently bridges input requests to direct
Elicit/CreateMessage/ListRoots calls for older clients
2026-05-29 09:29:34 +02:00