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() {
|
func Example_roots() {
|
||||||
ctx := context.Background()
|
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 := 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.
|
// Create a server with a tool that requests roots via the multi round-trip
|
||||||
rootsChanged := make(chan struct{})
|
// pattern (SEP-2322): server-to-client requests are no longer sent as
|
||||||
handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) {
|
// standalone JSON-RPC calls on protocol version >= 2026-07-28.
|
||||||
rootList, err := req.Session.ListRoots(ctx, nil)
|
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||||
if err != nil {
|
mcp.AddTool(s, &mcp.Tool{Name: "roots"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
|
||||||
log.Fatal(err)
|
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
|
var roots []string
|
||||||
for _, root := range rootList.Roots {
|
for _, root := range rootList.Roots {
|
||||||
roots = append(roots, root.URI)
|
roots = append(roots, root.URI)
|
||||||
}
|
}
|
||||||
fmt.Println(roots)
|
fmt.Println(roots)
|
||||||
close(rootsChanged)
|
return &mcp.CallToolResult{}, nil, nil
|
||||||
}
|
|
||||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
|
||||||
RootsListChangedHandler: handleRootsChanged,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Connect the server and client...
|
// Connect the server and client...
|
||||||
|
|
@ -81,9 +82,11 @@ func Example_roots() {
|
||||||
}
|
}
|
||||||
defer clientSession.Close()
|
defer clientSession.Close()
|
||||||
|
|
||||||
// ...and add a root. The server is notified about the change.
|
// ...and call the tool. The client's multi round-trip driver fulfils the
|
||||||
c.AddRoots(&mcp.Root{URI: "file://b"})
|
// embedded roots/list request and retries the call.
|
||||||
<-rootsChanged
|
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "roots"}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
// Output: [file://a file://b]
|
// Output: [file://a file://b]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -130,22 +133,35 @@ func Example_sampling() {
|
||||||
|
|
||||||
// Connect the server and client...
|
// Connect the server and client...
|
||||||
ct, st := mcp.NewInMemoryTransports()
|
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)
|
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)
|
session, err := s.Connect(ctx, st, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
if _, err := c.Connect(ctx, ct, nil); err != nil {
|
clientSession, err := c.Connect(ctx, ct, nil)
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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
|
// Output: would have created a message
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -175,7 +191,28 @@ func Example_elicitation() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ct, st := mcp.NewInMemoryTransports()
|
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)
|
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)
|
ss, err := s.Connect(ctx, st, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
@ -187,22 +224,13 @@ func Example_elicitation() {
|
||||||
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
|
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if _, err := c.Connect(ctx, ct, nil); err != nil {
|
clientSession, err := c.Connect(ctx, ct, 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"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Println(res.Content["test"])
|
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "ask"}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
// Output: value
|
// Output: value
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
// Use of this source code is governed by an MIT-style
|
// Use of this source code is governed by an MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// 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
|
package mcp_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -20,26 +18,27 @@ import (
|
||||||
func Example_roots() {
|
func Example_roots() {
|
||||||
ctx := context.Background()
|
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 := 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.
|
// Create a server with a tool that requests roots via the multi round-trip
|
||||||
rootsChanged := make(chan struct{})
|
// pattern (SEP-2322): server-to-client requests are no longer sent as
|
||||||
handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) {
|
// standalone JSON-RPC calls on protocol version >= 2026-07-28.
|
||||||
rootList, err := req.Session.ListRoots(ctx, nil)
|
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
||||||
if err != nil {
|
mcp.AddTool(s, &mcp.Tool{Name: "roots"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
|
||||||
log.Fatal(err)
|
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
|
var roots []string
|
||||||
for _, root := range rootList.Roots {
|
for _, root := range rootList.Roots {
|
||||||
roots = append(roots, root.URI)
|
roots = append(roots, root.URI)
|
||||||
}
|
}
|
||||||
fmt.Println(roots)
|
fmt.Println(roots)
|
||||||
close(rootsChanged)
|
return &mcp.CallToolResult{}, nil, nil
|
||||||
}
|
|
||||||
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
|
|
||||||
RootsListChangedHandler: handleRootsChanged,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Connect the server and client...
|
// Connect the server and client...
|
||||||
|
|
@ -56,9 +55,11 @@ func Example_roots() {
|
||||||
}
|
}
|
||||||
defer clientSession.Close()
|
defer clientSession.Close()
|
||||||
|
|
||||||
// ...and add a root. The server is notified about the change.
|
// ...and call the tool. The client's multi round-trip driver fulfils the
|
||||||
c.AddRoots(&mcp.Root{URI: "file://b"})
|
// embedded roots/list request and retries the call.
|
||||||
<-rootsChanged
|
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "roots"}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
// Output: [file://a file://b]
|
// Output: [file://a file://b]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,22 +83,35 @@ func Example_sampling() {
|
||||||
|
|
||||||
// Connect the server and client...
|
// Connect the server and client...
|
||||||
ct, st := mcp.NewInMemoryTransports()
|
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)
|
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)
|
session, err := s.Connect(ctx, st, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
if _, err := c.Connect(ctx, ct, nil); err != nil {
|
clientSession, err := c.Connect(ctx, ct, nil)
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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
|
// Output: would have created a message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +123,28 @@ func Example_elicitation() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ct, st := mcp.NewInMemoryTransports()
|
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)
|
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)
|
ss, err := s.Connect(ctx, st, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
@ -121,22 +156,13 @@ func Example_elicitation() {
|
||||||
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
|
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if _, err := c.Connect(ctx, ct, nil); err != nil {
|
clientSession, err := c.Connect(ctx, ct, 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"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Println(res.Content["test"])
|
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "ask"}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
// Output: value
|
// Output: value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ func TestElicitationURLMode(t *testing.T) {
|
||||||
},
|
},
|
||||||
ElicitationHandler: tc.handler,
|
ElicitationHandler: tc.handler,
|
||||||
})
|
})
|
||||||
cs, err := c.Connect(ctx, ct, nil)
|
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -165,8 +165,18 @@ func TestElicitationCompleteNotification(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
_, ss, cleanup := basicClientServerConnection(t, c, nil, nil)
|
ct, st := NewInMemoryTransports()
|
||||||
defer cleanup()
|
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
|
// 1. Server initiates a URL elicitation
|
||||||
elicitID := "testElicitationID-123"
|
elicitID := "testElicitationID-123"
|
||||||
|
|
@ -245,7 +255,7 @@ func TestElicitationNoValidationWithoutAccept(t *testing.T) {
|
||||||
return &ElicitResult{Action: tc.action, Content: tc.content}, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -854,7 +854,7 @@ func TestNoJSONNull(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c := NewClient(testImpl, nil)
|
c := NewClient(testImpl, nil)
|
||||||
cs, err := c.Connect(ctx, ct, nil)
|
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -1023,7 +1023,7 @@ func TestElicitationUnsupportedMethod(t *testing.T) {
|
||||||
return &CreateMessageResult{Model: "aModel", Content: &TextContent{}}, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -1072,7 +1072,7 @@ func TestElicitationSchemaValidation(t *testing.T) {
|
||||||
return &ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -1558,7 +1558,7 @@ func TestElicitContentValidation(t *testing.T) {
|
||||||
return &ElicitResult{Action: "accept", Content: map[string]any{"test": "potato"}}, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -1641,7 +1641,7 @@ func TestElicitationProgressToken(t *testing.T) {
|
||||||
return &ElicitResult{Action: "accept"}, nil
|
return &ElicitResult{Action: "accept"}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cs, err := c.Connect(ctx, ct, nil)
|
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -1683,7 +1683,7 @@ func TestElicitationCapabilityDeclaration(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
|
|
||||||
cs, err := c.Connect(ctx, ct, nil)
|
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -1720,7 +1720,7 @@ func TestElicitationCapabilityDeclaration(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
|
|
||||||
cs, err := c.Connect(ctx, ct, nil)
|
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -1757,7 +1757,7 @@ func TestElicitationDefaultValues(t *testing.T) {
|
||||||
return &ElicitResult{Action: "accept", Content: map[string]any{"default": "response"}}, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -2079,8 +2079,22 @@ func TestSynchronousNotifications(t *testing.T) {
|
||||||
return new(CallToolResult), nil, nil
|
return new(CallToolResult), nil, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
cs, ss, cleanup := basicClientServerConnection(t, client, server, addTool)
|
ctx := context.Background()
|
||||||
defer cleanup()
|
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")
|
t.Log("from client")
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func TestSamplingWithTools_ToolUse(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
|
|
||||||
cs, err := client.Connect(ctx, ct, nil)
|
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +124,7 @@ func TestSamplingWithTools_ToolResult(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
|
|
||||||
cs, err := client.Connect(ctx, ct, nil)
|
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -288,7 +288,7 @@ func TestSamplingWithTools_ToolResultWithError(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
|
|
||||||
cs, err := client.Connect(ctx, ct, nil)
|
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -350,7 +350,7 @@ func TestSamplingWithTools_ParallelToolCalls(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
|
|
||||||
cs, err := client.Connect(ctx, ct, nil)
|
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -420,7 +420,7 @@ func TestCreateMessage_MultipleContentError(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
|
|
||||||
cs, err := client.Connect(ctx, ct, nil)
|
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1512,6 +1512,23 @@ func (ss *ServerSession) ID() string {
|
||||||
return ""
|
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.
|
// Ping pings the client.
|
||||||
func (ss *ServerSession) Ping(ctx context.Context, params *PingParams) error {
|
func (ss *ServerSession) Ping(ctx context.Context, params *PingParams) error {
|
||||||
_, err := handleSend[*emptyResult](ctx, methodPing, newServerRequest(ss, orZero[Params](params)))
|
_, 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 {
|
if err := ss.checkInitialized(methodListRoots); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ss.assertServerInitiatedRequestAllowed(methodListRoots); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return handleSend[*ListRootsResult](ctx, methodListRoots, newServerRequest(ss, orZero[Params](params)))
|
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 {
|
if err := ss.checkInitialized(methodCreateMessage); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ss.assertServerInitiatedRequestAllowed(methodCreateMessage); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if params == nil {
|
if params == nil {
|
||||||
params = &CreateMessageParams{Messages: []*SamplingMessage{}}
|
params = &CreateMessageParams{Messages: []*SamplingMessage{}}
|
||||||
}
|
}
|
||||||
|
|
@ -1590,6 +1613,9 @@ func (ss *ServerSession) CreateMessageWithTools(ctx context.Context, params *Cre
|
||||||
if err := ss.checkInitialized(methodCreateMessage); err != nil {
|
if err := ss.checkInitialized(methodCreateMessage); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ss.assertServerInitiatedRequestAllowed(methodCreateMessage); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if params == nil {
|
if params == nil {
|
||||||
params = &CreateMessageWithToolsParams{Messages: []*SamplingMessageV2{}}
|
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 {
|
if err := ss.checkInitialized(methodElicit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ss.assertServerInitiatedRequestAllowed(methodElicit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if params == nil {
|
if params == nil {
|
||||||
return nil, fmt.Errorf("%w: params cannot be nil", jsonrpc2.ErrInvalidParams)
|
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