Repository navigation
mcp: retry input-required results that carry only requestState - #1365
SergeevDmitry wants to merge 4 commits into
Conversation
SEP-2322 requires an InputRequiredResult to carry at least one of
inputRequests and requestState, so a result with only requestState is
valid, and the TypeScript SDK server sends one from
inputRequired({ requestState }). clientMultiRoundTripMiddleware stopped
as soon as InputRequests was nil and returned such a result to the
caller as the final one.
Also check the result type, so these results are retried with the
requestState echoed, like an empty inputRequests map. A result with
neither field is now retried until the load-shedding limit instead of
being returned.
|
@SergeevDmitry what do you think about fixing this on server side too? |
|
Agreed, the server should be able to send a requestState-only result too. I'll add it to this PR.
One question about the legacy shim: for a client before 2026-07-28, should a requestState-only round call the handler again with the state echoed, counted against |
|
Yes I would call the handler again |
The server treated a result as input-required only when InputRequests was set. A handler that returned only a RequestState sent a complete result, and a resources/read handler doing so failed with "read handler returned nil information". Use one check, inputRequests or requestState, for the result type, the content validation, the empty content and messages fill, the resources/read early return and the legacy shim. For clients before 2026-07-28 the shim now calls the handler again with the state echoed. On the client, a requestState-only result counts as a round rather than load shedding, the same as in the shim.
|
Pushed the server side.
|
| @@ -342,6 +342,226 @@ type wrappedCallToolParams struct{ *CallToolParams } | |||
| // retry loop reports an explicit error when the params carry a type it | |||
| // cannot build retry params for, instead of silently resending the request | |||
| // unchanged until the retry cap. | |||
There was a problem hiding this comment.
this comment now is placed in the wrong place
| if reqMap != nil && len(reqMap) == 0 { | ||
| return nil, fmt.Errorf("the server is busy, retry later") | ||
| } |
There was a problem hiding this comment.
| if reqMap != nil && len(reqMap) == 0 { | |
| return nil, fmt.Errorf("the server is busy, retry later") | |
| } | |
| if len(reqMap) == 0 && mrtrResult.requestState() == "" { | |
| return nil, fmt.Errorf("the server is busy, retry later") | |
| } |
| // round, not as load shedding. | ||
| continuing := reqMap == nil && mrtrResult.requestState() != "" | ||
| if len(reqMap) == 0 && !continuing { | ||
| loadSheddingFailures++ |
There was a problem hiding this comment.
| loadSheddingFailures++ | |
| if len(reqMap) == 0 && mrtrResult.requestState() == "" { | |
| loadSheddingFailures++ | |
| } |
Apply the review suggestions: the client counts a round as load shedding, and the legacy server path returns "busy", only when the result has neither input requests nor a requestState. An empty inputRequests map with a requestState now continues like a requestState-only result. Also move the TestMultiRoundTrip_UnsupportedRetryParamsType comment back above its function.
|
Thanks. I applied both suggestions and moved the comment back.
|
A result with only
requestStateis a validInputRequiredResultunder SEP-2322 (servers "MUST include at least one ofinputRequestsorrequestState"), and the TypeScript SDK server sends one frominputRequired({ requestState }). The Go SDK handled it on neither side:clientMultiRoundTripMiddlewarereturned early whenInputRequestswas nil, so such a result reached the caller as the final one (NeedsInput()true, no content), although the client docs say the middleware retries whileNeedsInput()is true.annotateResultTypeand the rest of the server's result handling checked onlyInputRequests, so a handler returning only aRequestStatesent a complete result, and aresources/readhandler doing so failed with "read handler returned nil information".This uses one check,
inputRequests() != nil || requestState() != "", for the server's result type, the content validation, the emptycontent/messagesfill, theresources/readearly return and the legacy shim. For clients before 2026-07-28 the shim calls the handler again with the state echoed. The client retries these results with the state echoed and counts them as rounds (maxMultiRoundTripRetries), not as load shedding, the same as the shim. An emptyinputRequestsmap is still load shedding, and a result with neither field still fails after the load-shedding limit. Content together with arequestStateis now rejected like content withinputRequests. The server docs are updated anddocs/server.mdis regenerated.New tests, all failing on
main:TestMultiRoundTrip_AutoRetry_RequestStateOnly,TestMultiRoundTrip_AutoRetry_InputRequiredWithoutFields,TestMultiRoundTrip_RequestStateOnly_MaxRetries.TestMultiRoundTrip_ServerRequestStateOnly(tools/call, prompts/get, resources/read),TestMultiRoundTrip_ServerMiddleware_RequestStateOnly,TestMultiRoundTrip_ContentWithRequestState, and three requestState-only cases inTestServerSessionHandle_SetsResultTypeWhenMiddlewareShortCircuits.go test -race ./...,go vet ./...and both conformance scripts pass locally (server 40/40, client 239/239).Fixes #1364
AI assistance: I used Claude Code to help write this fix and the tests.