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
This commit is contained in:
Yaroslav 2026-05-29 09:29:34 +02:00 • committed by GitHub
commit 189a85ad92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -187,7 +187,7 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
opts.Logger = ensureLogger(nil)
}
return &Server{
s := &Server{
impl: impl,
opts: opts,
prompts: newFeatureSet(func(p *serverPrompt) string { return p.prompt.Name }),
@ -199,6 +199,8 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
resourceSubscriptions: make(map[string]map[*ServerSession]bool),
pendingNotifications: make(map[string]*time.Timer),
}
s.AddReceivingMiddleware(serverMultiRoundTripMiddleware())
return s
}
// AddPrompt adds a [Prompt] to the server, or replaces one with the same name.
@ -370,8 +372,12 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa
}
// Marshal the output and put the RawMessage in the StructuredContent field.
// Skip when the handler returned input requests (multi round-trip): content and
// inputRequests are mutually exclusive on the wire.
var outval any = out
if elemZero != nil {
if res.InputRequests != nil {
outval = nil
} else if elemZero != nil {
// Avoid typed nil, which will serialize as JSON null.
// Instead, use the zero value of the unpointered type.
var z Out
@ -742,7 +748,13 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm
Message: fmt.Sprintf("unknown prompt %q", req.Params.Name),
}
}
return prompt.handler(ctx, req)
res, err := prompt.handler(ctx, req)
if err == nil && res != nil {
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
return nil, err
}
}
return res, err
}
func (s *Server) listTools(_ context.Context, req *ListToolsRequest) (*ListToolsResult, error) {
@ -775,10 +787,15 @@ func (s *Server) callTool(ctx context.Context, req *CallToolRequest) (*CallToolR
}
}
res, err := st.handler(ctx, req)
if err == nil && res != nil && res.Content == nil {
res2 := *res
res2.Content = []Content{} // avoid "null"
res = &res2
if err == nil && res != nil {
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
return nil, err
}
if res.Content == nil && res.resultType != resultTypeInputRequired {
res2 := *res
res2.Content = []Content{} // avoid "null"
res = &res2
}
}
return res, err
}
@ -826,6 +843,12 @@ func (s *Server) readResource(ctx context.Context, req *ReadResourceRequest) (*R
if err != nil {
return nil, err
}
if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil {
return nil, err
}
if res.resultType == resultTypeInputRequired {
return res, nil
}
if res == nil || res.Contents == nil {
return nil, fmt.Errorf("reading resource %s: read handler returned nil information", uri)
}