mcp: block server initiated requests (SEP-2322) (#1057)
This PR explicitly blocks server initiated requests in server.go
This commit is contained in:
parent
7f67c851d1
commit
deecf8a432
7 changed files with 288 additions and 83 deletions
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue