Skip to content

Commit cb0fcb7

Browse files
committed
mcp: add Capabilities fields to ServerOptions and ClientOptions
This allows users to configure default capabilities, disable defaults, or suppress listChanged notifications by setting ListChanged: false. Key changes: - Add ServerOptions.Capabilities and ClientOptions.Capabilities fields. - Merge user-provided capabilities with dynamically-added ones (tools, prompts, resources, sampling, elicitation). - Check ListChanged before sending list-changed notifications. - Sync Roots from RootsV2 for backward compatibility (#607). - Deprecate HasPrompts, HasResources, HasTools on ServerOptions. - Remove unreleased ElicitationModes field from ClientOptions. - Add clone methods to capability structs using shallowClone helper. - Add synctest-based tests for notification behavior (go1.25). - Add TestClientCapabilitiesOverWire and TestServerCapabilitiesOverWire - Document the new Capabilities API in docs/ Fixes #706
1 parent a5e369e commit cb0fcb7

File tree

14 files changed

+932
-53
lines changed

14 files changed

+932
-53
lines changed

docs/client.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
1. [Roots](#roots)
55
1. [Sampling](#sampling)
66
1. [Elicitation](#elicitation)
7+
1. [Capabilities](#capabilities)
78

89
## Roots
910

@@ -171,3 +172,45 @@ func Example_elicitation() {
171172
// Output: value
172173
}
173174
```
175+
176+
## Capabilities
177+
178+
Client capabilities are advertised to servers during the initialization
179+
handshake. By default, the SDK advertises the `roots` capability with
180+
`listChanged: true`. Additional capabilities are automatically added when
181+
handlers are set (e.g., setting `CreateMessageHandler` adds the `sampling`
182+
capability).
183+
184+
To customize capabilities, set
185+
[`ClientOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.Capabilities).
186+
This allows you to:
187+
188+
- **Disable default capabilities**: Pass an empty `&ClientCapabilities{}` to
189+
disable all defaults, including roots.
190+
- **Disable listChanged notifications**: Set `ListChanged: false` on a
191+
capability to prevent the client from sending list-changed notifications
192+
when roots are added or removed.
193+
- **Configure elicitation modes**: Specify which elicitation modes (form, URL)
194+
the client supports.
195+
196+
```go
197+
// Configure elicitation modes and disable roots
198+
client := mcp.NewClient(impl, &mcp.ClientOptions{
199+
Capabilities: &mcp.ClientCapabilities{
200+
Elicitation: &mcp.ElicitationCapabilities{
201+
Form: &mcp.FormElicitationCapabilities{},
202+
URL: &mcp.URLElicitationCapabilities{},
203+
},
204+
},
205+
ElicitationHandler: handler,
206+
})
207+
```
208+
209+
When handlers are set on `ClientOptions` (e.g., `CreateMessageHandler` or
210+
`ElicitationHandler`), the corresponding capability is automatically added if
211+
not already present. However, user-specified capability settings are preserved
212+
and not overridden.
213+
214+
For elicitation, if the handler is set but no `Capabilities.Elicitation` is
215+
specified, the client defaults to form elicitation. To enable URL elicitation
216+
or both modes, configure `Capabilities.Elicitation` explicitly.

docs/rough_edges.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,11 @@ v2.
3636

3737
**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
3838
semantics of other capability fields.
39+
40+
- Default capabilities should have been empty. Instead, servers default to
41+
advertising `logging`, and clients default to advertising `roots` with
42+
`listChanged: true`. This is confusing because a nil `Capabilities` field
43+
does not mean "no capabilities".
44+
45+
**Workaround**: set `ServerOptions.Capabilities` or `ClientOptions.Capabilities`
46+
to an empty `&ServerCapabilities{}` or `&ClientCapabilities{}` respectively.

docs/server.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
1. [Utilities](#utilities)
88
1. [Completion](#completion)
99
1. [Logging](#logging)
10+
1. [Capabilities](#capabilities)
1011
1. [Pagination](#pagination)
1112

1213
## Prompts
@@ -535,6 +536,43 @@ func Example_logging() {
535536
}
536537
```
537538

539+
### Capabilities
540+
541+
Server capabilities are advertised to clients during the initialization
542+
handshake. By default, the SDK advertises only the `logging` capability.
543+
Additional capabilities are automatically added when features are registered
544+
(e.g., adding a tool adds the `tools` capability).
545+
546+
To customize capabilities, set
547+
[`ServerOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.Capabilities).
548+
This allows you to:
549+
550+
- **Disable default capabilities**: Pass an empty `&ServerCapabilities{}` to
551+
disable all defaults, including logging.
552+
- **Disable listChanged notifications**: Set `ListChanged: false` on a
553+
capability to prevent the server from sending list-changed notifications
554+
when features are added or removed.
555+
- **Pre-declare capabilities**: Declare capabilities before features are
556+
registered, useful for servers that load features dynamically.
557+
558+
```go
559+
// Disable listChanged notifications for tools
560+
server := mcp.NewServer(impl, &mcp.ServerOptions{
561+
Capabilities: &mcp.ServerCapabilities{
562+
Logging: &mcp.LoggingCapabilities{},
563+
Tools: &mcp.ToolCapabilities{ListChanged: false},
564+
},
565+
})
566+
```
567+
568+
When features are added dynamically (e.g., via `Server.AddTool`), the
569+
corresponding capability is automatically added if not already present.
570+
However, user-specified capability settings (like `ListChanged: false`) are
571+
preserved and not overridden.
572+
573+
**Deprecated**: The `HasPrompts`, `HasResources`, and `HasTools` fields on
574+
`ServerOptions` are deprecated. Use `Capabilities` instead.
575+
538576
### Pagination
539577

540578
Server-side feature lists may be

internal/docs/client.src.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,45 @@ otherwise, elicitation returns an error.
5353
[`ServerSession.Elicit`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Elicit).
5454

5555
%include ../../mcp/client_example_test.go elicitation -
56+
57+
## Capabilities
58+
59+
Client capabilities are advertised to servers during the initialization
60+
handshake. By default, the SDK advertises the `roots` capability with
61+
`listChanged: true`. Additional capabilities are automatically added when
62+
handlers are set (e.g., setting `CreateMessageHandler` adds the `sampling`
63+
capability).
64+
65+
To customize capabilities, set
66+
[`ClientOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.Capabilities).
67+
This allows you to:
68+
69+
- **Disable default capabilities**: Pass an empty `&ClientCapabilities{}` to
70+
disable all defaults, including roots.
71+
- **Disable listChanged notifications**: Set `ListChanged: false` on a
72+
capability to prevent the client from sending list-changed notifications
73+
when roots are added or removed.
74+
- **Configure elicitation modes**: Specify which elicitation modes (form, URL)
75+
the client supports.
76+
77+
```go
78+
// Configure elicitation modes and disable roots
79+
client := mcp.NewClient(impl, &mcp.ClientOptions{
80+
Capabilities: &mcp.ClientCapabilities{
81+
Elicitation: &mcp.ElicitationCapabilities{
82+
Form: &mcp.FormElicitationCapabilities{},
83+
URL: &mcp.URLElicitationCapabilities{},
84+
},
85+
},
86+
ElicitationHandler: handler,
87+
})
88+
```
89+
90+
When handlers are set on `ClientOptions` (e.g., `CreateMessageHandler` or
91+
`ElicitationHandler`), the corresponding capability is automatically added if
92+
not already present. However, user-specified capability settings are preserved
93+
and not overridden.
94+
95+
For elicitation, if the handler is set but no `Capabilities.Elicitation` is
96+
specified, the client defaults to form elicitation. To enable URL elicitation
97+
or both modes, configure `Capabilities.Elicitation` explicitly.

internal/docs/rough_edges.src.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ v2.
3535

3636
**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
3737
semantics of other capability fields.
38+
39+
- Default capabilities should have been empty. Instead, servers default to
40+
advertising `logging`, and clients default to advertising `roots` with
41+
`listChanged: true`. This is confusing because a nil `Capabilities` field
42+
does not mean "no capabilities".
43+
44+
**Workaround**: set `ServerOptions.Capabilities` or `ClientOptions.Capabilities`
45+
to an empty `&ServerCapabilities{}` or `&ClientCapabilities{}` respectively.

internal/docs/server.src.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,43 @@ Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotoc
243243

244244
%include ../../mcp/server_example_test.go logging -
245245

246+
### Capabilities
247+
248+
Server capabilities are advertised to clients during the initialization
249+
handshake. By default, the SDK advertises only the `logging` capability.
250+
Additional capabilities are automatically added when features are registered
251+
(e.g., adding a tool adds the `tools` capability).
252+
253+
To customize capabilities, set
254+
[`ServerOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.Capabilities).
255+
This allows you to:
256+
257+
- **Disable default capabilities**: Pass an empty `&ServerCapabilities{}` to
258+
disable all defaults, including logging.
259+
- **Disable listChanged notifications**: Set `ListChanged: false` on a
260+
capability to prevent the server from sending list-changed notifications
261+
when features are added or removed.
262+
- **Pre-declare capabilities**: Declare capabilities before features are
263+
registered, useful for servers that load features dynamically.
264+
265+
```go
266+
// Disable listChanged notifications for tools
267+
server := mcp.NewServer(impl, &mcp.ServerOptions{
268+
Capabilities: &mcp.ServerCapabilities{
269+
Logging: &mcp.LoggingCapabilities{},
270+
Tools: &mcp.ToolCapabilities{ListChanged: false},
271+
},
272+
})
273+
```
274+
275+
When features are added dynamically (e.g., via `Server.AddTool`), the
276+
corresponding capability is automatically added if not already present.
277+
However, user-specified capability settings (like `ListChanged: false`) are
278+
preserved and not overridden.
279+
280+
**Deprecated**: The `HasPrompts`, `HasResources`, and `HasTools` fields on
281+
`ServerOptions` are deprecated. Use `Capabilities` instead.
282+
246283
### Pagination
247284

248285
Server-side feature lists may be

mcp/capabilities_go125_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.25
6+
7+
package mcp
8+
9+
import (
10+
"context"
11+
"sync/atomic"
12+
"testing"
13+
"testing/synctest"
14+
15+
"github.com/google/jsonschema-go/jsonschema"
16+
)
17+
18+
// TestServerListChangedNotifications verifies that listChanged notifications
19+
// are correctly sent or suppressed based on capability configuration.
20+
func TestServerListChangedNotifications(t *testing.T) {
21+
tool := &Tool{Name: "test-tool", InputSchema: &jsonschema.Schema{Type: "object"}}
22+
23+
testCases := []struct {
24+
name string
25+
serverOpts *ServerOptions
26+
wantNotifyCount int64
27+
}{
28+
{
29+
name: "Default: notification sent",
30+
serverOpts: nil,
31+
wantNotifyCount: 1,
32+
},
33+
{
34+
name: "ListChanged false: notification suppressed",
35+
serverOpts: &ServerOptions{
36+
Capabilities: &ServerCapabilities{
37+
Tools: &ToolCapabilities{ListChanged: false},
38+
},
39+
},
40+
wantNotifyCount: 0,
41+
},
42+
{
43+
name: "ListChanged true: notification sent",
44+
serverOpts: &ServerOptions{
45+
Capabilities: &ServerCapabilities{
46+
Tools: &ToolCapabilities{ListChanged: true},
47+
},
48+
},
49+
wantNotifyCount: 1,
50+
},
51+
}
52+
53+
for _, tc := range testCases {
54+
t.Run(tc.name, func(t *testing.T) {
55+
synctest.Test(t, func(t *testing.T) {
56+
ctx := context.Background()
57+
58+
// Create server.
59+
impl := &Implementation{Name: "testServer", Version: "v1.0.0"}
60+
server := NewServer(impl, tc.serverOpts)
61+
62+
// Track notifications.
63+
var notifyCount atomic.Int64
64+
65+
// Connect client and server.
66+
cTransport, sTransport := NewInMemoryTransports()
67+
ss, err := server.Connect(ctx, sTransport, nil)
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
defer ss.Close()
72+
73+
client := NewClient(&Implementation{Name: "testClient", Version: "v1.0.0"}, &ClientOptions{
74+
ToolListChangedHandler: func(ctx context.Context, req *ToolListChangedRequest) {
75+
notifyCount.Add(1)
76+
},
77+
})
78+
cs, err := client.Connect(ctx, cTransport, nil)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
defer cs.Close()
83+
84+
// Add a tool, which may or may not trigger notification.
85+
server.AddTool(tool, nil)
86+
87+
// Wait for all goroutines to be blocked (notification delivered).
88+
synctest.Wait()
89+
90+
if got, want := notifyCount.Load(), tc.wantNotifyCount; got != want {
91+
t.Errorf("notification count: got %d, want %d", got, want)
92+
}
93+
})
94+
})
95+
}
96+
}
97+
98+
// TestClientListChangedNotifications verifies that roots listChanged notifications
99+
// are correctly sent or suppressed based on client capability configuration.
100+
func TestClientListChangedNotifications(t *testing.T) {
101+
root := &Root{URI: "file:///test"}
102+
103+
testCases := []struct {
104+
name string
105+
clientOpts *ClientOptions
106+
wantNotifyCount int64
107+
}{
108+
{
109+
name: "Default: notification sent",
110+
clientOpts: nil,
111+
wantNotifyCount: 1,
112+
},
113+
{
114+
name: "ListChanged false: notification suppressed",
115+
clientOpts: &ClientOptions{
116+
Capabilities: &ClientCapabilities{
117+
RootsV2: &RootCapabilities{ListChanged: false},
118+
},
119+
},
120+
wantNotifyCount: 0,
121+
},
122+
{
123+
name: "ListChanged true: notification sent",
124+
clientOpts: &ClientOptions{
125+
Capabilities: &ClientCapabilities{
126+
RootsV2: &RootCapabilities{ListChanged: true},
127+
},
128+
},
129+
wantNotifyCount: 1,
130+
},
131+
}
132+
133+
for _, tc := range testCases {
134+
t.Run(tc.name, func(t *testing.T) {
135+
synctest.Test(t, func(t *testing.T) {
136+
ctx := context.Background()
137+
138+
// Track notifications.
139+
var notifyCount atomic.Int64
140+
141+
// Create server with roots list changed handler.
142+
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, &ServerOptions{
143+
RootsListChangedHandler: func(ctx context.Context, req *RootsListChangedRequest) {
144+
notifyCount.Add(1)
145+
},
146+
})
147+
148+
// Connect client and server.
149+
cTransport, sTransport := NewInMemoryTransports()
150+
ss, err := server.Connect(ctx, sTransport, nil)
151+
if err != nil {
152+
t.Fatal(err)
153+
}
154+
defer ss.Close()
155+
156+
client := NewClient(&Implementation{Name: "testClient", Version: "v1.0.0"}, tc.clientOpts)
157+
cs, err := client.Connect(ctx, cTransport, nil)
158+
if err != nil {
159+
t.Fatal(err)
160+
}
161+
defer cs.Close()
162+
163+
// Add a root, which may or may not trigger notification.
164+
client.AddRoots(root)
165+
166+
// Wait for all goroutines to be blocked (notification delivered).
167+
synctest.Wait()
168+
169+
if got, want := notifyCount.Load(), tc.wantNotifyCount; got != want {
170+
t.Errorf("notification count: got %d, want %d", got, want)
171+
}
172+
})
173+
})
174+
}
175+
}

0 commit comments

Comments
 (0)