Skip to content

mcp: include resultType on new-protocol responses - #1060

Merged
guglielmo-san merged 5 commits into
modelcontextprotocol:mainfrom
ychampion:fix-2026-result-type
Jul 13, 2026
Merged

guglielmo-san merged 5 commits into
modelcontextprotocol:mainfrom
ychampion:fix-2026-result-type

Conversation

@ychampion

Copy link
Copy Markdown
Contributor

Summary

  • Add resultType: "complete" to non-MRTR server result responses for 2026-07-28 requests.
  • Cover server/discover, list methods, and completion responses, and update expected conformance/example output.

Why

Fixes #1032. The 2026-07-28 schema requires resultType on result objects, but only the MRTR-capable results were setting it.

Validation

  • go test ./mcp -run 'Test(ServerSessionHandle|CompleteResult|ClientConnectDiscover|InMemory_E2E_Discover)' -count=1 — passed\n- go test ./mcp -count=1 — passed\n- go test ./... — passed\n- go vet ./... — passed

Wrap non-MRTR server result objects for 2026-07-28 requests so the wire response includes resultType without changing direct marshaling of public result structs.

Fixes modelcontextprotocol#1032

Constraint: Preserve existing public result struct shape and direct JSON output for callers.

Tested: go test ./mcp -run 'Test(ServerSessionHandle|CompleteResult|ClientConnectDiscover|InMemory_E2E_Discover)' -count=1; go test ./mcp -count=1; go test ./...
Comment thread mcp/server.go Outdated
Comment thread mcp/protocol.go Outdated
@ychampion
ychampion force-pushed the fix-2026-result-type branch from ee4b0cf to 95e8252 Compare July 9, 2026 10:36
Comment thread mcp/server.go
ychampion and others added 2 commits July 10, 2026 13:04
Constraint: MRTR responses must retain input_required instead of receiving the generic complete discriminator.

Tested: go test ./mcp -run TestServerSessionHandle_SetsResultTypeOnNewProtocol -count=1; go test ./...; go vet ./...
@guglielmo-san
guglielmo-san enabled auto-merge (squash) July 13, 2026 11:50
@guglielmo-san
guglielmo-san merged commit dd79ca3 into modelcontextprotocol:main Jul 13, 2026
9 checks passed
guglielmo-san added a commit that referenced this pull request Jul 13, 2026
This PR adds the resultType in the troubleshoot doc after PR #1060
roncodingenthusiast added a commit to roncodingenthusiast/go-sdk that referenced this pull request Sep 1, 2026
Protocol revision 2026-07-28 requires resultType on every result.
CallToolResult, GetPromptResult and ReadResourceResult hold it in an
unexported field, since they can be complete or input_required and so
cannot embed completeResultWithType as the seven types in modelcontextprotocol#1060 do.

Only handleMultiRoundTripResult writes that field, and it runs inside
Server.callTool, getPrompt and readResource. A receiving middleware that
returns a result instead of calling next -- an auth, rate limit or
entitlement gate -- never reaches it, so the response ships without the
field and strict clients reject it as malformed.

Move the choice of complete versus input_required into
setMultiRoundTripResultType, and call it from both places that need it.
setCompleteResultType, which now labels results it did not before, is
renamed annotateResultType and picks a branch by type switch. It reads
inputRequests() rather than assuming complete, which matters because
setResultType is unexported: populating InputRequests is a middleware's
only way to ask for input, and labeling it complete would make a
conforming client drop the elicitation. Recomputing is safe because a
result that did reach the dispatcher was labeled from the same test.

ServerSession.handle labeled only when the request opted in through
_meta, while handleMultiRoundTripResult tests the session's negotiated
version, which left the same bug open in the mirror direction. Both
sites now call clientSupportsMultiRoundTrip so they cannot drift apart.
annotateServerInfo keeps its existing _meta-only gate.

Embedding completeResultWithType instead would label every result
complete and clobber input_required, breaking multi-round-trip flows.

Also correct ten comments naming an exported ResultType field and
ResultTypeComplete/ResultTypeInputRequired constants, none of which
exist. Two were unresolvable [CallToolResult.ResultType] doc links and
now point at [CallToolResult.NeedsInput].

The exported API is unchanged and the conformance goldens do not move.

Fixes modelcontextprotocol#1225

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
roncodingenthusiast added a commit to roncodingenthusiast/go-sdk that referenced this pull request Sep 1, 2026
Protocol revision 2026-07-28 requires resultType on every result.
CallToolResult, GetPromptResult and ReadResourceResult hold it in an
unexported field, since they can be complete or input_required and so
cannot embed completeResultWithType as the seven types in modelcontextprotocol#1060 do.

Only handleMultiRoundTripResult writes that field, and it runs inside
Server.callTool, getPrompt and readResource. A receiving middleware that
returns a result instead of calling next -- an auth, rate limit or
entitlement gate -- never reaches it, so the response ships without the
field and strict clients reject it as malformed.

Move the choice of complete versus input_required into
setMultiRoundTripResultType, and call it from both places that need it.
setCompleteResultType, which now labels results it did not before, is
renamed annotateResultType and picks a branch by type switch. It reads
inputRequests() rather than assuming complete, which matters because
setResultType is unexported: populating InputRequests is a middleware's
only way to ask for input, and labeling it complete would make a
conforming client drop the elicitation. Recomputing is safe because a
result that did reach the dispatcher was labeled from the same test.

ServerSession.handle labeled only when the request opted in through
_meta, while handleMultiRoundTripResult tests the session's negotiated
version, which left the same bug open in the mirror direction. handle
now tests usesNewProtocol or clientSupportsMultiRoundTrip, a superset of
the dispatcher's test, so it can never label less than the dispatcher
does. It does label more: a session that negotiated 2026-07-28 and sends
no _meta now gets resultType where it did not before.
annotateServerInfo keeps its existing _meta-only gate.

Embedding completeResultWithType instead would label every result
complete and clobber input_required, breaking multi-round-trip flows.

Also correct ten comments naming an exported ResultType field and
ResultTypeComplete/ResultTypeInputRequired constants, none of which
exist. Two were unresolvable [CallToolResult.ResultType] doc links and
now point at [CallToolResult.NeedsInput].

The exported API is unchanged and the conformance goldens do not move.

Fixes modelcontextprotocol#1225

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
roncodingenthusiast added a commit to roncodingenthusiast/go-sdk that referenced this pull request Sep 1, 2026
Protocol revision 2026-07-28 requires resultType on every result.
CallToolResult, GetPromptResult and ReadResourceResult hold it in an
unexported field, since they can be complete or input_required and so
cannot embed completeResultWithType as the seven types in modelcontextprotocol#1060 do.

Only handleMultiRoundTripResult writes that field, and it runs inside
Server.callTool, getPrompt and readResource. A receiving middleware that
returns a result instead of calling next -- an auth, rate limit or
entitlement gate -- never reaches it, so the response ships without the
field and strict clients reject it as malformed.

Move the choice of complete versus input_required into
setMultiRoundTripResultType, and call it from both places that need it.
setCompleteResultType, which now labels results it did not before, is
renamed annotateResultType and picks a branch by type switch. It reads
inputRequests() rather than assuming complete, which matters because
setResultType is unexported: populating InputRequests is a middleware's
only way to ask for input, and labeling it complete would make a
conforming client drop the elicitation. Recomputing is safe because a
result that did reach the dispatcher was labeled from the same test.

ServerSession.handle labeled only when the request opted in through
_meta, while handleMultiRoundTripResult tests the session's negotiated
version, which left the same bug open in the mirror direction. handle
now tests usesNewProtocol or clientSupportsMultiRoundTrip, a superset of
the dispatcher's test, so it can never label less than the dispatcher
does. It does label more: a session that negotiated 2026-07-28 and sends
no _meta now gets resultType where it did not before.
annotateServerInfo keeps its existing _meta-only gate.

Embedding completeResultWithType instead would label every result
complete and clobber input_required, breaking multi-round-trip flows.

Also correct ten comments naming an exported ResultType field and
ResultTypeComplete/ResultTypeInputRequired constants, none of which
exist. Two were unresolvable [CallToolResult.ResultType] doc links and
now point at [CallToolResult.NeedsInput].

The exported API is unchanged and the conformance goldens do not move.

Fixes modelcontextprotocol#1225

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
roncodingenthusiast added a commit to roncodingenthusiast/go-sdk that referenced this pull request Sep 2, 2026
Protocol revision 2026-07-28 requires resultType on every result.
CallToolResult, GetPromptResult and ReadResourceResult hold it in an
unexported field, since they can be complete or input_required and so
cannot embed completeResultWithType as the seven types in modelcontextprotocol#1060 do.

Only handleMultiRoundTripResult writes that field, and it runs inside
Server.callTool, getPrompt and readResource. A receiving middleware that
returns a result instead of calling next -- an auth, rate limit or
entitlement gate -- never reaches it, so the response ships without the
field and strict clients reject it as malformed.

Move the choice of complete versus input_required into
setMultiRoundTripResultType, and call it from both places that need it.
setCompleteResultType, which now labels results it did not before, is
renamed annotateResultType and picks a branch by type switch. It reads
inputRequests() rather than assuming complete, which matters because
setResultType is unexported: populating InputRequests is a middleware's
only way to ask for input, and labeling it complete would make a
conforming client drop the elicitation. Recomputing is safe because a
result that did reach the dispatcher was labeled from the same test.

Embedding completeResultWithType instead would label every result
complete and clobber input_required, breaking multi-round-trip flows.

ServerSession.handle still gates on the request's _meta while
handleMultiRoundTripResult gates on the session's negotiated version, so
a session that negotiated 2026-07-28 and sends no _meta gets no
resultType from a short-circuited request. Only a client that mixes
protocol versions can reach that, so
TestServerSessionHandle_ResultTypeGate records it rather than changing
behavior to cover it.

Also correct ten comments naming an exported ResultType field and
ResultTypeComplete/ResultTypeInputRequired constants, none of which
exist. Two were unresolvable [CallToolResult.ResultType] doc links and
now point at [CallToolResult.NeedsInput].

The exported API is unchanged and the conformance goldens do not move.

Fixes modelcontextprotocol#1225

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
roncodingenthusiast added a commit to roncodingenthusiast/go-sdk that referenced this pull request Sep 2, 2026
Protocol revision 2026-07-28 requires resultType on every result.
CallToolResult, GetPromptResult and ReadResourceResult hold it in an
unexported field, since they can be complete or input_required and so
cannot embed completeResultWithType as the seven types in modelcontextprotocol#1060 do.

Only handleMultiRoundTripResult writes that field, and it runs inside
Server.callTool, getPrompt and readResource. A receiving middleware that
returns a result instead of calling next -- an auth, rate limit or
entitlement gate -- never reaches it, so the response ships without the
field and strict clients reject it as malformed.

Move the choice of complete versus input_required into
setMultiRoundTripResultType, and call it from both places that need it.
setCompleteResultType, which now labels results it did not before, is
renamed annotateResultType and picks a branch by type switch. It reads
inputRequests() rather than assuming complete, which matters because
setResultType is unexported: populating InputRequests is a middleware's
only way to ask for input, and labeling it complete would make a
conforming client drop the elicitation. Recomputing is safe because a
result that did reach the dispatcher was labeled from the same test.

Embedding completeResultWithType instead would label every result
complete and clobber input_required, breaking multi-round-trip flows.

A middleware can also return a typed nil, which satisfies Result, so
ServerSession.handle now skips annotation for it. That case panicked
before this change too, one frame further on in annotateServerInfo.

ServerSession.handle still gates on the request's _meta while
handleMultiRoundTripResult gates on the session's negotiated version, so
a session that negotiated 2026-07-28 and sends no _meta gets no
resultType from a short-circuited request. Only a client that mixes
protocol versions can reach that, so
TestServerSessionHandle_ResultTypeGate records it rather than changing
behavior to cover it.

Also correct ten comments naming an exported ResultType field and
ResultTypeComplete/ResultTypeInputRequired constants, none of which
exist. Two were unresolvable [CallToolResult.ResultType] doc links and
now point at [CallToolResult.NeedsInput].

The exported API is unchanged and the conformance goldens do not move.

Fixes modelcontextprotocol#1225

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
roncodingenthusiast added a commit to roncodingenthusiast/go-sdk that referenced this pull request Sep 2, 2026
Protocol revision 2026-07-28 requires resultType on every result.
CallToolResult, GetPromptResult and ReadResourceResult hold it in an
unexported field, since they can be complete or input_required and so
cannot embed completeResultWithType as the seven types in modelcontextprotocol#1060 do.

Only handleMultiRoundTripResult writes that field, and it runs inside
Server.callTool, getPrompt and readResource. A receiving middleware that
returns a result instead of calling next -- an auth, rate limit or
entitlement gate -- never reaches it, so the response ships without the
field and strict clients reject it as malformed.

Move the choice of complete versus input_required into
setMultiRoundTripResultType, and call it from both places that need it.
setCompleteResultType, which now labels results it did not before, is
renamed annotateResultType and picks a branch by type switch. It reads
inputRequests() rather than assuming complete, which matters because
setResultType is unexported: populating InputRequests is a middleware's
only way to ask for input, and labeling it complete would make a
conforming client drop the elicitation. Recomputing is safe because a
result that did reach the dispatcher was labeled from the same test.

Embedding completeResultWithType instead would label every result
complete and clobber input_required, breaking multi-round-trip flows.

A middleware can also return a typed nil, which satisfies Result, and
annotating one panics. That panic predates this change; it happened one
frame further on, in annotateServerInfo. Result gains isNil, matching
what Params already does, and ServerSession.handle skips annotation for
a nil result. Result is sealed by isResult, so adding an unexported
method to it is not a breaking change.

ServerSession.handle still gates on the request's _meta while
handleMultiRoundTripResult gates on the session's negotiated version, so
a session that negotiated 2026-07-28 and sends no _meta gets no
resultType from a short-circuited request. Only a client that mixes
protocol versions can reach that, so
TestServerSessionHandle_ResultTypeGate records it rather than changing
behavior to cover it.

Also correct ten comments naming an exported ResultType field and
ResultTypeComplete/ResultTypeInputRequired constants, none of which
exist. Two were unresolvable [CallToolResult.ResultType] doc links and
now point at [CallToolResult.NeedsInput].

The exported API is unchanged and the conformance goldens do not move.

Fixes modelcontextprotocol#1225

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to ankitm123/go-sdk that referenced this pull request Sep 14, 2026
…textprotocol#1226)

Fixes modelcontextprotocol#1225. See the [issue
comment](modelcontextprotocol#1225 (comment)).

## 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 modelcontextprotocol#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 modelcontextprotocol#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2026-07-28 responses omit required resultType for most result types

2 participants