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
9 changes: 6 additions & 3 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,9 +854,11 @@ The SDK supports this pattern from both sides without requiring callers to
choose:

- **Returning input requests from a handler.** Set `InputRequests` on the
result you return.
result you return. To have the client call again without asking it for
input, return only a `RequestState`; the client echoes it on the retry.
Leave `Content` / `StructuredContent` empty — returning
both at once is a server bug and the SDK returns `-32603 InternalError`.
content together with `InputRequests` or `RequestState` is a server bug and
the SDK returns `-32603 InternalError`.
- **Calling the legacy APIs.** `ServerSession.Elicit`,
`ServerSession.CreateMessage(WithTools)`, and `ServerSession.ListRoots`
remain available and work for both new and legacy clients.
Expand All @@ -868,7 +870,8 @@ clients on a protocol version earlier than `2026-07-28`, the middleware
intercepts any `InputRequiredResult` your handler returns, fulfils each
input request itself by calling the legacy server-initiated APIs
(`Elicit`, `CreateMessage`, `ListRoots`), and re-invokes your handler
with the responses already populated. If the handler asks for more input,
with the responses already populated. A result with a `RequestState` and no
input requests re-invokes it with that state and no responses. If the handler asks for more input,
the middleware repeats this until the handler completes, up to the same
limit of 10 rounds that the client-side middleware applies. This means a
handler written in the MRTR style works against both old and new clients
Expand Down
9 changes: 6 additions & 3 deletions internal/docs/server.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,11 @@ The SDK supports this pattern from both sides without requiring callers to
choose:

- **Returning input requests from a handler.** Set `InputRequests` on the
result you return.
result you return. To have the client call again without asking it for
input, return only a `RequestState`; the client echoes it on the retry.
Leave `Content` / `StructuredContent` empty — returning
both at once is a server bug and the SDK returns `-32603 InternalError`.
content together with `InputRequests` or `RequestState` is a server bug and
the SDK returns `-32603 InternalError`.
- **Calling the legacy APIs.** `ServerSession.Elicit`,
`ServerSession.CreateMessage(WithTools)`, and `ServerSession.ListRoots`
remain available and work for both new and legacy clients.
Expand All @@ -349,7 +351,8 @@ clients on a protocol version earlier than `2026-07-28`, the middleware
intercepts any `InputRequiredResult` your handler returns, fulfils each
input request itself by calling the legacy server-initiated APIs
(`Elicit`, `CreateMessage`, `ListRoots`), and re-invokes your handler
with the responses already populated. If the handler asks for more input,
with the responses already populated. A result with a `RequestState` and no
input requests re-invokes it with that state and no responses. If the handler asks for more input,
the middleware repeats this until the handler completes, up to the same
limit of 10 rounds that the client-side middleware applies. This means a
handler written in the MRTR style works against both old and new clients
Expand Down
37 changes: 26 additions & 11 deletions mcp/mrtr.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,28 @@ type multiRoundTripResponse interface {
inputRequests() map[string]InputRequest
requestState() string
hasContent() bool
NeedsInput() bool
}

// asksForInput reports whether a handler's result is an input-required result.
// SEP-2322 requires at least one of inputRequests and requestState, so a
// result with only a requestState asks the client to continue too.
func asksForInput(res multiRoundTripResponse) bool {
return res.inputRequests() != nil || res.requestState() != ""
}

// validateMultiRoundTripResult rejects a result that carries both content and
// input requests. [annotateResultType] labels the result later, in
// [ServerSession.handle], so this function does not set resultType.
// input requests or a request state. [annotateResultType] labels the result
// later, in [ServerSession.handle], so this function does not set resultType.
func validateMultiRoundTripResult(logger *slog.Logger, res multiRoundTripResponse) error {
if res == nil {
return nil
}
if res.inputRequests() != nil && res.hasContent() {
logger.Warn("handler returned both content and inputRequests")
if asksForInput(res) && res.hasContent() {
logger.Warn("handler returned both content and inputRequests or requestState")
return &jsonrpc.Error{
Code: jsonrpc.CodeInternalError,
Message: "server bug: result has both content and inputRequests",
Message: "server bug: result has both content and inputRequests or requestState",
}
}
return nil
Expand Down Expand Up @@ -79,10 +87,15 @@ func clientMultiRoundTripMiddleware() Middleware {
return res, nil
}
reqMap := mrtrResult.inputRequests()
if reqMap == nil {
// An input-required result may carry only a requestState
// (SEP-2322 requires at least one of the two fields), so the
// result type, not the map alone, decides whether to retry.
if reqMap == nil && !mrtrResult.NeedsInput() {
return res, nil
}
if len(reqMap) == 0 {
// A result with a requestState asks to continue, so it counts as
// a round, not as load shedding.
if len(reqMap) == 0 && mrtrResult.requestState() == "" {
loadSheddingFailures++
Comment thread
guglielmo-san marked this conversation as resolved.
}
if loadSheddingFailures >= maxLoadSheddingMultiRoundTripRetries {
Expand Down Expand Up @@ -112,7 +125,9 @@ func clientMultiRoundTripMiddleware() Middleware {
// handles multi-round-trip for clients on older protocol versions. When a handler returns
// InputRequests and the client does not support multi-round-trip, the middleware fulfills
// the requests by calling the client directly and reinvokes the handler with the responses,
// repeating until the handler completes or maxMultiRoundTripRetries is reached.
// repeating until the handler completes or maxMultiRoundTripRetries is reached. A result
// with a RequestState and no input requests reinvokes the handler with that state and no
// responses.
func serverMultiRoundTripMiddleware() Middleware {
return func(next MethodHandler) MethodHandler {
return func(ctx context.Context, method string, req Request) (Result, error) {
Expand All @@ -137,11 +152,11 @@ func serverMultiRoundTripMiddleware() Middleware {
if !ok {
return res, nil
}
reqMap := mrtrResult.inputRequests()
if reqMap == nil {
if !asksForInput(mrtrResult) {
return res, nil
}
if len(reqMap) == 0 {
reqMap := mrtrResult.inputRequests()
if len(reqMap) == 0 && mrtrResult.requestState() == "" {
return nil, fmt.Errorf("the server is busy, retry later")
}
if retries >= maxMultiRoundTripRetries {
Expand Down
Loading
Loading