mcp: block server initiated requests (SEP-2322) (#1057)

This PR explicitly blocks server initiated requests in server.go
This commit is contained in:
Guglielmo Colombo 2026-07-08 15:17:41 +02:00 • committed by GitHub
commit deecf8a432
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 288 additions and 83 deletions

View file

@ -45,26 +45,27 @@ pattern.
func Example_roots() {
ctx := context.Background()
// Create a client with a single root.
// Create a client with two roots.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
c.AddRoots(&mcp.Root{URI: "file://a"})
c.AddRoots(&mcp.Root{URI: "file://a"}, &mcp.Root{URI: "file://b"})
// Now create a server with a handler to receive notifications about roots.
rootsChanged := make(chan struct{})
handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) {
rootList, err := req.Session.ListRoots(ctx, nil)
if err != nil {
log.Fatal(err)
// Create a server with a tool that requests roots via the multi round-trip
// pattern (SEP-2322): server-to-client requests are no longer sent as
// standalone JSON-RPC calls on protocol version >= 2026-07-28.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "roots"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"roots": &mcp.ListRootsParams{}},
}, nil, nil
}
rootList := req.Params.InputResponses["roots"].(*mcp.ListRootsResult)
var roots []string
for _, root := range rootList.Roots {
roots = append(roots, root.URI)
}
fmt.Println(roots)
close(rootsChanged)
}
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
RootsListChangedHandler: handleRootsChanged,
return &mcp.CallToolResult{}, nil, nil
})
// Connect the server and client...
@ -81,9 +82,11 @@ func Example_roots() {
}
defer clientSession.Close()
// ...and add a root. The server is notified about the change.
c.AddRoots(&mcp.Root{URI: "file://b"})
<-rootsChanged
// ...and call the tool. The client's multi round-trip driver fulfils the
// embedded roots/list request and retries the call.
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "roots"}); err != nil {
log.Fatal(err)
}
// Output: [file://a file://b]
}
```
@ -130,22 +133,35 @@ func Example_sampling() {
// Connect the server and client...
ct, st := mcp.NewInMemoryTransports()
// Create a server with a tool that requests sampling via the multi
// round-trip pattern (SEP-2322): server-to-client requests are no longer
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "sample"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"msg": &mcp.CreateMessageParams{}},
}, nil, nil
}
msg := req.Params.InputResponses["msg"].(*mcp.CreateMessageWithToolsResult)
return &mcp.CallToolResult{Content: msg.Content}, nil, nil
})
session, err := s.Connect(ctx, st, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
if _, err := c.Connect(ctx, ct, nil); err != nil {
log.Fatal(err)
}
msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{})
clientSession, err := c.Connect(ctx, ct, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Content.(*mcp.TextContent).Text)
res, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "sample"})
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
// Output: would have created a message
}
```
@ -175,7 +191,28 @@ func Example_elicitation() {
ctx := context.Background()
ct, st := mcp.NewInMemoryTransports()
// Create a server with a tool that requests elicitation via the multi
// round-trip pattern (SEP-2322): server-to-client requests are no longer
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "ask"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"input": &mcp.ElicitParams{
Message: "This should fail",
RequestedSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"test": {Type: "string"},
},
},
}},
}, nil, nil
}
res := req.Params.InputResponses["input"].(*mcp.ElicitResult)
fmt.Println(res.Content["test"])
return &mcp.CallToolResult{}, nil, nil
})
ss, err := s.Connect(ctx, st, nil)
if err != nil {
log.Fatal(err)
@ -187,22 +224,13 @@ func Example_elicitation() {
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
},
})
if _, err := c.Connect(ctx, ct, nil); err != nil {
log.Fatal(err)
}
res, err := ss.Elicit(ctx, &mcp.ElicitParams{
Message: "This should fail",
RequestedSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"test": {Type: "string"},
},
},
})
clientSession, err := c.Connect(ctx, ct, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Content["test"])
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "ask"}); err != nil {
log.Fatal(err)
}
// Output: value
}
```

View file

@ -2,8 +2,6 @@
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
//lint:file-ignore SA1019 examples exercise deprecated SEP-2577 APIs (roots, sampling) for demonstration.
package mcp_test
import (
@ -20,26 +18,27 @@ import (
func Example_roots() {
ctx := context.Background()
// Create a client with a single root.
// Create a client with two roots.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
c.AddRoots(&mcp.Root{URI: "file://a"})
c.AddRoots(&mcp.Root{URI: "file://a"}, &mcp.Root{URI: "file://b"})
// Now create a server with a handler to receive notifications about roots.
rootsChanged := make(chan struct{})
handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) {
rootList, err := req.Session.ListRoots(ctx, nil)
if err != nil {
log.Fatal(err)
// Create a server with a tool that requests roots via the multi round-trip
// pattern (SEP-2322): server-to-client requests are no longer sent as
// standalone JSON-RPC calls on protocol version >= 2026-07-28.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "roots"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"roots": &mcp.ListRootsParams{}},
}, nil, nil
}
rootList := req.Params.InputResponses["roots"].(*mcp.ListRootsResult)
var roots []string
for _, root := range rootList.Roots {
roots = append(roots, root.URI)
}
fmt.Println(roots)
close(rootsChanged)
}
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
RootsListChangedHandler: handleRootsChanged,
return &mcp.CallToolResult{}, nil, nil
})
// Connect the server and client...
@ -56,9 +55,11 @@ func Example_roots() {
}
defer clientSession.Close()
// ...and add a root. The server is notified about the change.
c.AddRoots(&mcp.Root{URI: "file://b"})
<-rootsChanged
// ...and call the tool. The client's multi round-trip driver fulfils the
// embedded roots/list request and retries the call.
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "roots"}); err != nil {
log.Fatal(err)
}
// Output: [file://a file://b]
}
@ -82,22 +83,35 @@ func Example_sampling() {
// Connect the server and client...
ct, st := mcp.NewInMemoryTransports()
// Create a server with a tool that requests sampling via the multi
// round-trip pattern (SEP-2322): server-to-client requests are no longer
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "sample"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"msg": &mcp.CreateMessageParams{}},
}, nil, nil
}
msg := req.Params.InputResponses["msg"].(*mcp.CreateMessageWithToolsResult)
return &mcp.CallToolResult{Content: msg.Content}, nil, nil
})
session, err := s.Connect(ctx, st, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
if _, err := c.Connect(ctx, ct, nil); err != nil {
log.Fatal(err)
}
msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{})
clientSession, err := c.Connect(ctx, ct, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Content.(*mcp.TextContent).Text)
res, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "sample"})
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
// Output: would have created a message
}
@ -109,7 +123,28 @@ func Example_elicitation() {
ctx := context.Background()
ct, st := mcp.NewInMemoryTransports()
// Create a server with a tool that requests elicitation via the multi
// round-trip pattern (SEP-2322): server-to-client requests are no longer
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "ask"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"input": &mcp.ElicitParams{
Message: "This should fail",
RequestedSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"test": {Type: "string"},
},
},
}},
}, nil, nil
}
res := req.Params.InputResponses["input"].(*mcp.ElicitResult)
fmt.Println(res.Content["test"])
return &mcp.CallToolResult{}, nil, nil
})
ss, err := s.Connect(ctx, st, nil)
if err != nil {
log.Fatal(err)
@ -121,22 +156,13 @@ func Example_elicitation() {
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
},
})
if _, err := c.Connect(ctx, ct, nil); err != nil {
log.Fatal(err)
}
res, err := ss.Elicit(ctx, &mcp.ElicitParams{
Message: "This should fail",
RequestedSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"test": {Type: "string"},
},
},
})
clientSession, err := c.Connect(ctx, ct, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Content["test"])
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "ask"}); err != nil {
log.Fatal(err)
}
// Output: value
}

View file

@ -114,7 +114,7 @@ func TestElicitationURLMode(t *testing.T) {
},
ElicitationHandler: tc.handler,
})
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -165,8 +165,18 @@ func TestElicitationCompleteNotification(t *testing.T) {
},
})
_, ss, cleanup := basicClientServerConnection(t, c, nil, nil)
defer cleanup()
ct, st := NewInMemoryTransports()
s := NewServer(testImpl, nil)
ss, err := s.Connect(ctx, st, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
defer cs.Close()
// 1. Server initiates a URL elicitation
elicitID := "testElicitationID-123"
@ -245,7 +255,7 @@ func TestElicitationNoValidationWithoutAccept(t *testing.T) {
return &ElicitResult{Action: tc.action, Content: tc.content}, nil
},
})
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}

View file

@ -854,7 +854,7 @@ func TestNoJSONNull(t *testing.T) {
}
c := NewClient(testImpl, nil)
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1023,7 +1023,7 @@ func TestElicitationUnsupportedMethod(t *testing.T) {
return &CreateMessageResult{Model: "aModel", Content: &TextContent{}}, nil
},
})
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1072,7 +1072,7 @@ func TestElicitationSchemaValidation(t *testing.T) {
return &ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
},
})
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1558,7 +1558,7 @@ func TestElicitContentValidation(t *testing.T) {
return &ElicitResult{Action: "accept", Content: map[string]any{"test": "potato"}}, nil
},
})
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1641,7 +1641,7 @@ func TestElicitationProgressToken(t *testing.T) {
return &ElicitResult{Action: "accept"}, nil
},
})
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1683,7 +1683,7 @@ func TestElicitationCapabilityDeclaration(t *testing.T) {
}
defer ss.Close()
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1720,7 +1720,7 @@ func TestElicitationCapabilityDeclaration(t *testing.T) {
}
defer ss.Close()
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1757,7 +1757,7 @@ func TestElicitationDefaultValues(t *testing.T) {
return &ElicitResult{Action: "accept", Content: map[string]any{"default": "response"}}, nil
},
})
cs, err := c.Connect(ctx, ct, nil)
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -2079,8 +2079,22 @@ func TestSynchronousNotifications(t *testing.T) {
return new(CallToolResult), nil, nil
})
}
cs, ss, cleanup := basicClientServerConnection(t, client, server, addTool)
defer cleanup()
ctx := context.Background()
ct, st := NewInMemoryTransports()
addTool(server)
ss, err := server.Connect(ctx, st, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = ss.Close() })
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = cs.Close()
ss.Wait()
})
t.Log("from client")
{

View file

@ -53,7 +53,7 @@ func TestSamplingWithTools_ToolUse(t *testing.T) {
}
defer ss.Close()
cs, err := client.Connect(ctx, ct, nil)
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -124,7 +124,7 @@ func TestSamplingWithTools_ToolResult(t *testing.T) {
}
defer ss.Close()
cs, err := client.Connect(ctx, ct, nil)
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -288,7 +288,7 @@ func TestSamplingWithTools_ToolResultWithError(t *testing.T) {
}
defer ss.Close()
cs, err := client.Connect(ctx, ct, nil)
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -350,7 +350,7 @@ func TestSamplingWithTools_ParallelToolCalls(t *testing.T) {
}
defer ss.Close()
cs, err := client.Connect(ctx, ct, nil)
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -420,7 +420,7 @@ func TestCreateMessage_MultipleContentError(t *testing.T) {
}
defer ss.Close()
cs, err := client.Connect(ctx, ct, nil)
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}

View file

@ -1512,6 +1512,23 @@ func (ss *ServerSession) ID() string {
return ""
}
// assertServerInitiatedRequestAllowed returns an error when the session is
// negotiated at protocol version >= 2026-07-28, where the spec (SEP-2322 /
// SEP-2575) forbids server-initiated JSON-RPC requests for elicitation,
// sampling, and roots: those interactions MUST be embedded as [InputRequests]
// in an [InputRequiredResult] returned from a handler for one of the multi
// round-trip methods (`tools/call`, `prompts/get`, `resources/read`).
func (ss *ServerSession) assertServerInitiatedRequestAllowed(method string) error {
if iparams := ss.InitializeParams(); iparams != nil &&
iparams.ProtocolVersion >= protocolVersion20260728 {
return fmt.Errorf(
"%q cannot be sent while serving a request on protocol version %s: "+
"return an InputRequests map instead (multi round-trip requests, SEP-2322)",
method, iparams.ProtocolVersion)
}
return nil
}
// Ping pings the client.
func (ss *ServerSession) Ping(ctx context.Context, params *PingParams) error {
_, err := handleSend[*emptyResult](ctx, methodPing, newServerRequest(ss, orZero[Params](params)))
@ -1529,6 +1546,9 @@ func (ss *ServerSession) ListRoots(ctx context.Context, params *ListRootsParams)
if err := ss.checkInitialized(methodListRoots); err != nil {
return nil, err
}
if err := ss.assertServerInitiatedRequestAllowed(methodListRoots); err != nil {
return nil, err
}
return handleSend[*ListRootsResult](ctx, methodListRoots, newServerRequest(ss, orZero[Params](params)))
}
@ -1547,6 +1567,9 @@ func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessag
if err := ss.checkInitialized(methodCreateMessage); err != nil {
return nil, err
}
if err := ss.assertServerInitiatedRequestAllowed(methodCreateMessage); err != nil {
return nil, err
}
if params == nil {
params = &CreateMessageParams{Messages: []*SamplingMessage{}}
}
@ -1590,6 +1613,9 @@ func (ss *ServerSession) CreateMessageWithTools(ctx context.Context, params *Cre
if err := ss.checkInitialized(methodCreateMessage); err != nil {
return nil, err
}
if err := ss.assertServerInitiatedRequestAllowed(methodCreateMessage); err != nil {
return nil, err
}
if params == nil {
params = &CreateMessageWithToolsParams{Messages: []*SamplingMessageV2{}}
}
@ -1606,6 +1632,9 @@ func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*Eli
if err := ss.checkInitialized(methodElicit); err != nil {
return nil, err
}
if err := ss.assertServerInitiatedRequestAllowed(methodElicit); err != nil {
return nil, err
}
if params == nil {
return nil, fmt.Errorf("%w: params cannot be nil", jsonrpc2.ErrInvalidParams)
}

View file

@ -1451,3 +1451,101 @@ func TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol(t *testing.T) {
})
}
}
// TestServerSession_RejectsServerInitiatedRequests verifies that
// SEP-2322 / SEP-2575 is enforced at the API surface: [ServerSession.Elicit],
// [ServerSession.CreateMessage], [ServerSession.CreateMessageWithTools], and
// [ServerSession.ListRoots] must refuse to send a server-to-client request
// when the session is negotiated at protocol version >= 2026-07-28, and must
// remain functional on pre-2026-07-28 sessions.
func TestServerSession_RejectsServerInitiated(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
call func(*ServerSession) error
}{
{
name: "Elicit",
call: func(ss *ServerSession) error {
_, err := ss.Elicit(ctx, &ElicitParams{Message: "hi"})
return err
},
},
{
name: "CreateMessage",
call: func(ss *ServerSession) error {
_, err := ss.CreateMessage(ctx, &CreateMessageParams{})
return err
},
},
{
name: "CreateMessageWithTools",
call: func(ss *ServerSession) error {
_, err := ss.CreateMessageWithTools(ctx, &CreateMessageWithToolsParams{})
return err
},
},
{
name: "ListRoots",
call: func(ss *ServerSession) error {
_, err := ss.ListRoots(ctx, nil)
return err
},
},
}
// Cover both branches of the era gate: modern must reject, legacy must let
// the call proceed to the wire (where it either succeeds or fails on
// something unrelated to the guard, such as missing client capabilities).
for _, protoVer := range []string{protocolVersion20260728, protocolVersion20251125} {
for _, tc := range tests {
t.Run(fmt.Sprintf("%s/%s", protoVer, tc.name), func(t *testing.T) {
ct, st := NewInMemoryTransports()
s := NewServer(testImpl, nil)
ss, err := s.Connect(ctx, st, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = ss.Close() })
// Client advertises every server-to-client capability so a
// legacy-era call is not rejected for capability reasons
// instead of era reasons.
c := NewClient(testImpl, &ClientOptions{
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "cancel"}, nil
},
CreateMessageHandler: func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) {
return &CreateMessageResult{Model: "m", Role: "assistant", Content: &TextContent{Text: "ok"}}, nil
},
})
c.AddRoots(&Root{URI: "file:///workspace"})
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protoVer})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = cs.Close() })
gotErr := tc.call(ss)
modern := protoVer >= protocolVersion20260728
if modern {
if gotErr == nil {
t.Fatalf("%s on %s: got nil error, want era-gate rejection", tc.name, protoVer)
}
wantSubstr := "cannot be sent while serving a request on protocol version " + protoVer
if !strings.Contains(gotErr.Error(), wantSubstr) {
t.Errorf("%s on %s: error %q does not contain %q", tc.name, protoVer, gotErr.Error(), wantSubstr)
}
if !strings.Contains(gotErr.Error(), "multi round-trip requests") {
t.Errorf("%s on %s: error %q does not steer to MRTR", tc.name, protoVer, gotErr.Error())
}
} else {
if gotErr != nil {
t.Errorf("%s on %s: got error %v, want nil (era gate must not fire on legacy)", tc.name, protoVer, gotErr)
}
}
})
}
}
}