Skip to content

Commit deecf8a

Browse files
mcp: block server initiated requests (SEP-2322) (#1057)
This PR explicitly blocks server initiated requests in server.go
1 parent 7f67c85 commit deecf8a

7 files changed

Lines changed: 288 additions & 83 deletions

File tree

docs/client.md

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,27 @@ pattern.
4545
func Example_roots() {
4646
ctx := context.Background()
4747

48-
// Create a client with a single root.
48+
// Create a client with two roots.
4949
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
50-
c.AddRoots(&mcp.Root{URI: "file://a"})
51-
52-
// Now create a server with a handler to receive notifications about roots.
53-
rootsChanged := make(chan struct{})
54-
handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) {
55-
rootList, err := req.Session.ListRoots(ctx, nil)
56-
if err != nil {
57-
log.Fatal(err)
50+
c.AddRoots(&mcp.Root{URI: "file://a"}, &mcp.Root{URI: "file://b"})
51+
52+
// Create a server with a tool that requests roots via the multi round-trip
53+
// pattern (SEP-2322): server-to-client requests are no longer sent as
54+
// standalone JSON-RPC calls on protocol version >= 2026-07-28.
55+
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
56+
mcp.AddTool(s, &mcp.Tool{Name: "roots"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
57+
if len(req.Params.InputResponses) == 0 {
58+
return &mcp.CallToolResult{
59+
InputRequests: mcp.InputRequestMap{"roots": &mcp.ListRootsParams{}},
60+
}, nil, nil
5861
}
62+
rootList := req.Params.InputResponses["roots"].(*mcp.ListRootsResult)
5963
var roots []string
6064
for _, root := range rootList.Roots {
6165
roots = append(roots, root.URI)
6266
}
6367
fmt.Println(roots)
64-
close(rootsChanged)
65-
}
66-
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
67-
RootsListChangedHandler: handleRootsChanged,
68+
return &mcp.CallToolResult{}, nil, nil
6869
})
6970

7071
// Connect the server and client...
@@ -81,9 +82,11 @@ func Example_roots() {
8182
}
8283
defer clientSession.Close()
8384

84-
// ...and add a root. The server is notified about the change.
85-
c.AddRoots(&mcp.Root{URI: "file://b"})
86-
<-rootsChanged
85+
// ...and call the tool. The client's multi round-trip driver fulfils the
86+
// embedded roots/list request and retries the call.
87+
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "roots"}); err != nil {
88+
log.Fatal(err)
89+
}
8790
// Output: [file://a file://b]
8891
}
8992
```
@@ -130,22 +133,35 @@ func Example_sampling() {
130133

131134
// Connect the server and client...
132135
ct, st := mcp.NewInMemoryTransports()
136+
// Create a server with a tool that requests sampling via the multi
137+
// round-trip pattern (SEP-2322): server-to-client requests are no longer
138+
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
133139
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
140+
mcp.AddTool(s, &mcp.Tool{Name: "sample"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
141+
if len(req.Params.InputResponses) == 0 {
142+
return &mcp.CallToolResult{
143+
InputRequests: mcp.InputRequestMap{"msg": &mcp.CreateMessageParams{}},
144+
}, nil, nil
145+
}
146+
msg := req.Params.InputResponses["msg"].(*mcp.CreateMessageWithToolsResult)
147+
return &mcp.CallToolResult{Content: msg.Content}, nil, nil
148+
})
134149
session, err := s.Connect(ctx, st, nil)
135150
if err != nil {
136151
log.Fatal(err)
137152
}
138153
defer session.Close()
139154

140-
if _, err := c.Connect(ctx, ct, nil); err != nil {
155+
clientSession, err := c.Connect(ctx, ct, nil)
156+
if err != nil {
141157
log.Fatal(err)
142158
}
143159

144-
msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{})
160+
res, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "sample"})
145161
if err != nil {
146162
log.Fatal(err)
147163
}
148-
fmt.Println(msg.Content.(*mcp.TextContent).Text)
164+
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
149165
// Output: would have created a message
150166
}
151167
```
@@ -175,7 +191,28 @@ func Example_elicitation() {
175191
ctx := context.Background()
176192
ct, st := mcp.NewInMemoryTransports()
177193

194+
// Create a server with a tool that requests elicitation via the multi
195+
// round-trip pattern (SEP-2322): server-to-client requests are no longer
196+
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
178197
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
198+
mcp.AddTool(s, &mcp.Tool{Name: "ask"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
199+
if len(req.Params.InputResponses) == 0 {
200+
return &mcp.CallToolResult{
201+
InputRequests: mcp.InputRequestMap{"input": &mcp.ElicitParams{
202+
Message: "This should fail",
203+
RequestedSchema: &jsonschema.Schema{
204+
Type: "object",
205+
Properties: map[string]*jsonschema.Schema{
206+
"test": {Type: "string"},
207+
},
208+
},
209+
}},
210+
}, nil, nil
211+
}
212+
res := req.Params.InputResponses["input"].(*mcp.ElicitResult)
213+
fmt.Println(res.Content["test"])
214+
return &mcp.CallToolResult{}, nil, nil
215+
})
179216
ss, err := s.Connect(ctx, st, nil)
180217
if err != nil {
181218
log.Fatal(err)
@@ -187,22 +224,13 @@ func Example_elicitation() {
187224
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
188225
},
189226
})
190-
if _, err := c.Connect(ctx, ct, nil); err != nil {
227+
clientSession, err := c.Connect(ctx, ct, nil)
228+
if err != nil {
191229
log.Fatal(err)
192230
}
193-
res, err := ss.Elicit(ctx, &mcp.ElicitParams{
194-
Message: "This should fail",
195-
RequestedSchema: &jsonschema.Schema{
196-
Type: "object",
197-
Properties: map[string]*jsonschema.Schema{
198-
"test": {Type: "string"},
199-
},
200-
},
201-
})
202-
if err != nil {
231+
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "ask"}); err != nil {
203232
log.Fatal(err)
204233
}
205-
fmt.Println(res.Content["test"])
206234
// Output: value
207235
}
208236
```

mcp/client_example_test.go

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Use of this source code is governed by an MIT-style
33
// license that can be found in the LICENSE file.
44

5-
//lint:file-ignore SA1019 examples exercise deprecated SEP-2577 APIs (roots, sampling) for demonstration.
6-
75
package mcp_test
86

97
import (
@@ -20,26 +18,27 @@ import (
2018
func Example_roots() {
2119
ctx := context.Background()
2220

23-
// Create a client with a single root.
21+
// Create a client with two roots.
2422
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
25-
c.AddRoots(&mcp.Root{URI: "file://a"})
26-
27-
// Now create a server with a handler to receive notifications about roots.
28-
rootsChanged := make(chan struct{})
29-
handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) {
30-
rootList, err := req.Session.ListRoots(ctx, nil)
31-
if err != nil {
32-
log.Fatal(err)
23+
c.AddRoots(&mcp.Root{URI: "file://a"}, &mcp.Root{URI: "file://b"})
24+
25+
// Create a server with a tool that requests roots via the multi round-trip
26+
// pattern (SEP-2322): server-to-client requests are no longer sent as
27+
// standalone JSON-RPC calls on protocol version >= 2026-07-28.
28+
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
29+
mcp.AddTool(s, &mcp.Tool{Name: "roots"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
30+
if len(req.Params.InputResponses) == 0 {
31+
return &mcp.CallToolResult{
32+
InputRequests: mcp.InputRequestMap{"roots": &mcp.ListRootsParams{}},
33+
}, nil, nil
3334
}
35+
rootList := req.Params.InputResponses["roots"].(*mcp.ListRootsResult)
3436
var roots []string
3537
for _, root := range rootList.Roots {
3638
roots = append(roots, root.URI)
3739
}
3840
fmt.Println(roots)
39-
close(rootsChanged)
40-
}
41-
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
42-
RootsListChangedHandler: handleRootsChanged,
41+
return &mcp.CallToolResult{}, nil, nil
4342
})
4443

4544
// Connect the server and client...
@@ -56,9 +55,11 @@ func Example_roots() {
5655
}
5756
defer clientSession.Close()
5857

59-
// ...and add a root. The server is notified about the change.
60-
c.AddRoots(&mcp.Root{URI: "file://b"})
61-
<-rootsChanged
58+
// ...and call the tool. The client's multi round-trip driver fulfils the
59+
// embedded roots/list request and retries the call.
60+
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "roots"}); err != nil {
61+
log.Fatal(err)
62+
}
6263
// Output: [file://a file://b]
6364
}
6465

@@ -82,22 +83,35 @@ func Example_sampling() {
8283

8384
// Connect the server and client...
8485
ct, st := mcp.NewInMemoryTransports()
86+
// Create a server with a tool that requests sampling via the multi
87+
// round-trip pattern (SEP-2322): server-to-client requests are no longer
88+
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
8589
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
90+
mcp.AddTool(s, &mcp.Tool{Name: "sample"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
91+
if len(req.Params.InputResponses) == 0 {
92+
return &mcp.CallToolResult{
93+
InputRequests: mcp.InputRequestMap{"msg": &mcp.CreateMessageParams{}},
94+
}, nil, nil
95+
}
96+
msg := req.Params.InputResponses["msg"].(*mcp.CreateMessageWithToolsResult)
97+
return &mcp.CallToolResult{Content: msg.Content}, nil, nil
98+
})
8699
session, err := s.Connect(ctx, st, nil)
87100
if err != nil {
88101
log.Fatal(err)
89102
}
90103
defer session.Close()
91104

92-
if _, err := c.Connect(ctx, ct, nil); err != nil {
105+
clientSession, err := c.Connect(ctx, ct, nil)
106+
if err != nil {
93107
log.Fatal(err)
94108
}
95109

96-
msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{})
110+
res, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "sample"})
97111
if err != nil {
98112
log.Fatal(err)
99113
}
100-
fmt.Println(msg.Content.(*mcp.TextContent).Text)
114+
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
101115
// Output: would have created a message
102116
}
103117

@@ -109,7 +123,28 @@ func Example_elicitation() {
109123
ctx := context.Background()
110124
ct, st := mcp.NewInMemoryTransports()
111125

126+
// Create a server with a tool that requests elicitation via the multi
127+
// round-trip pattern (SEP-2322): server-to-client requests are no longer
128+
// sent as standalone JSON-RPC calls on protocol version >= 2026-07-28.
112129
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
130+
mcp.AddTool(s, &mcp.Tool{Name: "ask"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
131+
if len(req.Params.InputResponses) == 0 {
132+
return &mcp.CallToolResult{
133+
InputRequests: mcp.InputRequestMap{"input": &mcp.ElicitParams{
134+
Message: "This should fail",
135+
RequestedSchema: &jsonschema.Schema{
136+
Type: "object",
137+
Properties: map[string]*jsonschema.Schema{
138+
"test": {Type: "string"},
139+
},
140+
},
141+
}},
142+
}, nil, nil
143+
}
144+
res := req.Params.InputResponses["input"].(*mcp.ElicitResult)
145+
fmt.Println(res.Content["test"])
146+
return &mcp.CallToolResult{}, nil, nil
147+
})
113148
ss, err := s.Connect(ctx, st, nil)
114149
if err != nil {
115150
log.Fatal(err)
@@ -121,22 +156,13 @@ func Example_elicitation() {
121156
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil
122157
},
123158
})
124-
if _, err := c.Connect(ctx, ct, nil); err != nil {
159+
clientSession, err := c.Connect(ctx, ct, nil)
160+
if err != nil {
125161
log.Fatal(err)
126162
}
127-
res, err := ss.Elicit(ctx, &mcp.ElicitParams{
128-
Message: "This should fail",
129-
RequestedSchema: &jsonschema.Schema{
130-
Type: "object",
131-
Properties: map[string]*jsonschema.Schema{
132-
"test": {Type: "string"},
133-
},
134-
},
135-
})
136-
if err != nil {
163+
if _, err := clientSession.CallTool(ctx, &mcp.CallToolParams{Name: "ask"}); err != nil {
137164
log.Fatal(err)
138165
}
139-
fmt.Println(res.Content["test"])
140166
// Output: value
141167
}
142168

mcp/elicitation_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func TestElicitationURLMode(t *testing.T) {
114114
},
115115
ElicitationHandler: tc.handler,
116116
})
117-
cs, err := c.Connect(ctx, ct, nil)
117+
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
118118
if err != nil {
119119
t.Fatal(err)
120120
}
@@ -165,8 +165,18 @@ func TestElicitationCompleteNotification(t *testing.T) {
165165
},
166166
})
167167

168-
_, ss, cleanup := basicClientServerConnection(t, c, nil, nil)
169-
defer cleanup()
168+
ct, st := NewInMemoryTransports()
169+
s := NewServer(testImpl, nil)
170+
ss, err := s.Connect(ctx, st, nil)
171+
if err != nil {
172+
t.Fatal(err)
173+
}
174+
defer ss.Close()
175+
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
176+
if err != nil {
177+
t.Fatal(err)
178+
}
179+
defer cs.Close()
170180

171181
// 1. Server initiates a URL elicitation
172182
elicitID := "testElicitationID-123"
@@ -245,7 +255,7 @@ func TestElicitationNoValidationWithoutAccept(t *testing.T) {
245255
return &ElicitResult{Action: tc.action, Content: tc.content}, nil
246256
},
247257
})
248-
cs, err := c.Connect(ctx, ct, nil)
258+
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
249259
if err != nil {
250260
t.Fatal(err)
251261
}

0 commit comments

Comments
 (0)