Skip to content

Commit a5e369e

Browse files
authored
mcp: fix broken client root capabilities (#698)
To address #607, add ClientCapabilities.RootsV2, and populate it when constructing sending and receiving InitializeParams. Fixes #607
1 parent 307e32c commit a5e369e

File tree

10 files changed

+288
-57
lines changed

10 files changed

+288
-57
lines changed

docs/rough_edges.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@ v2.
3030

3131
- `AudioContent.MarshalJSON` should have had a pointer receiver, to be
3232
consistent with other content types.
33+
34+
- `ClientCapabilities.Roots` should have been a distinguished struct pointer
35+
([see #607](https://github.com/modelcontextprotocol/go-sdk/issues/607)).
36+
37+
**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
38+
semantics of other capability fields.

docs/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func ExampleLoggingTransport() {
5151

5252
// Output:
5353
// read: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.0.1"}}}
54-
// write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18"}}
54+
// write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true}}}}
5555
// write: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
5656
}
5757
```

internal/docs/rough_edges.src.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ v2.
2929

3030
- `AudioContent.MarshalJSON` should have had a pointer receiver, to be
3131
consistent with other content types.
32+
33+
- `ClientCapabilities.Roots` should have been a distinguished struct pointer
34+
([see #607](https://github.com/modelcontextprotocol/go-sdk/issues/607)).
35+
36+
**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
37+
semantics of other capability fields.

mcp/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ type ClientSessionOptions struct {
131131

132132
func (c *Client) capabilities() *ClientCapabilities {
133133
caps := &ClientCapabilities{}
134+
// Due to an oversight (#607), roots require special handling.
134135
caps.Roots.ListChanged = true
136+
caps.RootsV2 = &RootCapabilities{
137+
ListChanged: true,
138+
}
135139
if c.opts.CreateMessageHandler != nil {
136140
caps.Sampling = &SamplingCapabilities{}
137141
}

mcp/client_test.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,8 @@ func TestClientCapabilities(t *testing.T) {
202202
name: "With initial capabilities",
203203
configureClient: func(s *Client) {},
204204
wantCapabilities: &ClientCapabilities{
205-
Roots: struct {
206-
ListChanged bool "json:\"listChanged,omitempty\""
207-
}{ListChanged: true},
205+
Roots: RootCapabilities{ListChanged: true},
206+
RootsV2: &RootCapabilities{ListChanged: true},
208207
},
209208
},
210209
{
@@ -216,9 +215,8 @@ func TestClientCapabilities(t *testing.T) {
216215
},
217216
},
218217
wantCapabilities: &ClientCapabilities{
219-
Roots: struct {
220-
ListChanged bool "json:\"listChanged,omitempty\""
221-
}{ListChanged: true},
218+
Roots: RootCapabilities{ListChanged: true},
219+
RootsV2: &RootCapabilities{ListChanged: true},
222220
Sampling: &SamplingCapabilities{},
223221
},
224222
},
@@ -232,9 +230,8 @@ func TestClientCapabilities(t *testing.T) {
232230
},
233231
},
234232
wantCapabilities: &ClientCapabilities{
235-
Roots: struct {
236-
ListChanged bool "json:\"listChanged,omitempty\""
237-
}{ListChanged: true},
233+
Roots: RootCapabilities{ListChanged: true},
234+
RootsV2: &RootCapabilities{ListChanged: true},
238235
Elicitation: &ElicitationCapabilities{
239236
Form: &FormElicitationCapabilities{},
240237
},
@@ -250,9 +247,8 @@ func TestClientCapabilities(t *testing.T) {
250247
},
251248
},
252249
wantCapabilities: &ClientCapabilities{
253-
Roots: struct {
254-
ListChanged bool "json:\"listChanged,omitempty\""
255-
}{ListChanged: true},
250+
Roots: RootCapabilities{ListChanged: true},
251+
RootsV2: &RootCapabilities{ListChanged: true},
256252
Elicitation: &ElicitationCapabilities{
257253
URL: &URLElicitationCapabilities{},
258254
},
@@ -268,9 +264,8 @@ func TestClientCapabilities(t *testing.T) {
268264
},
269265
},
270266
wantCapabilities: &ClientCapabilities{
271-
Roots: struct {
272-
ListChanged bool "json:\"listChanged,omitempty\""
273-
}{ListChanged: true},
267+
Roots: RootCapabilities{ListChanged: true},
268+
RootsV2: &RootCapabilities{ListChanged: true},
274269
Elicitation: &ElicitationCapabilities{
275270
Form: &FormElicitationCapabilities{},
276271
URL: &URLElicitationCapabilities{},

mcp/protocol.go

Lines changed: 107 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -177,23 +177,62 @@ func (x *CancelledParams) isParams() {}
177177
func (x *CancelledParams) GetProgressToken() any { return getProgressToken(x) }
178178
func (x *CancelledParams) SetProgressToken(t any) { setProgressToken(x, t) }
179179

180+
// RootCapabilities describes a client's support for roots.
181+
type RootCapabilities struct {
182+
// ListChanged reports whether the client supports notifications for
183+
// changes to the roots list.
184+
ListChanged bool `json:"listChanged,omitempty"`
185+
}
186+
180187
// Capabilities a client may support. Known capabilities are defined here, in
181188
// this schema, but this is not a closed set: any client can define its own,
182189
// additional capabilities.
183190
type ClientCapabilities struct {
184-
// Experimental, non-standard capabilities that the client supports.
191+
// Experimental reports non-standard capabilities that the client supports.
185192
Experimental map[string]any `json:"experimental,omitempty"`
186-
// Present if the client supports listing roots.
193+
// Roots describes the client's support for roots.
194+
//
195+
// Deprecated: use RootsV2. As described in #607, Roots should have been a
196+
// pointer to a RootCapabilities value. Roots will be continue to be
197+
// populated, but any new fields will only be added in the RootsV2 field.
187198
Roots struct {
188-
// Whether the client supports notifications for changes to the roots list.
199+
// ListChanged reports whether the client supports notifications for
200+
// changes to the roots list.
189201
ListChanged bool `json:"listChanged,omitempty"`
190202
} `json:"roots,omitempty"`
191-
// Present if the client supports sampling from an LLM.
203+
// RootsV2 is present if the client supports roots.
204+
RootsV2 *RootCapabilities `json:"-"`
205+
// Sampling is present if the client supports sampling from an LLM.
192206
Sampling *SamplingCapabilities `json:"sampling,omitempty"`
193-
// Present if the client supports elicitation from the server.
207+
// Elicitation is present if the client supports elicitation from the server.
194208
Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"`
195209
}
196210

211+
func (c *ClientCapabilities) toV2() *clientCapabilitiesV2 {
212+
return &clientCapabilitiesV2{
213+
ClientCapabilities: *c,
214+
Roots: c.RootsV2,
215+
}
216+
}
217+
218+
// clientCapabilitiesV2 is a version of ClientCapabilities that fixes the bug
219+
// described in #607: Roots should have been a pointer to value type
220+
// RootCapabilities.
221+
type clientCapabilitiesV2 struct {
222+
ClientCapabilities
223+
Roots *RootCapabilities `json:"roots,omitempty"`
224+
}
225+
226+
func (c *clientCapabilitiesV2) toV1() *ClientCapabilities {
227+
caps := c.ClientCapabilities
228+
caps.RootsV2 = c.Roots
229+
// Sync Roots from RootsV2 for backward compatibility (#607).
230+
if caps.RootsV2 != nil {
231+
caps.Roots = *caps.RootsV2
232+
}
233+
return &caps
234+
}
235+
197236
type CompleteParamsArgument struct {
198237
// The name of the argument
199238
Name string `json:"name"`
@@ -373,27 +412,53 @@ type GetPromptResult struct {
373412

374413
func (*GetPromptResult) isResult() {}
375414

415+
// InitializeParams is sent by the client to initialize the session.
376416
type InitializeParams struct {
377417
// This property is reserved by the protocol to allow clients and servers to
378418
// attach additional metadata to their responses.
379-
Meta `json:"_meta,omitempty"`
419+
Meta `json:"_meta,omitempty"`
420+
// Capabilities describes the client's capabilities.
380421
Capabilities *ClientCapabilities `json:"capabilities"`
381-
ClientInfo *Implementation `json:"clientInfo"`
382-
// The latest version of the Model Context Protocol that the client supports.
383-
// The client may decide to support older versions as well.
422+
// ClientInfo provides information about the client.
423+
ClientInfo *Implementation `json:"clientInfo"`
424+
// ProtocolVersion is the latest version of the Model Context Protocol that
425+
// the client supports.
384426
ProtocolVersion string `json:"protocolVersion"`
385427
}
386428

429+
func (p *InitializeParams) toV2() *initializeParamsV2 {
430+
return &initializeParamsV2{
431+
InitializeParams: *p,
432+
Capabilities: p.Capabilities.toV2(),
433+
}
434+
}
435+
436+
// initializeParamsV2 works around the mistake in #607: Capabilities.Roots
437+
// should have been a pointer.
438+
type initializeParamsV2 struct {
439+
InitializeParams
440+
Capabilities *clientCapabilitiesV2 `json:"capabilities"`
441+
}
442+
443+
func (p *initializeParamsV2) toV1() *InitializeParams {
444+
p1 := p.InitializeParams
445+
if p.Capabilities != nil {
446+
p1.Capabilities = p.Capabilities.toV1()
447+
}
448+
return &p1
449+
}
450+
387451
func (x *InitializeParams) isParams() {}
388452
func (x *InitializeParams) GetProgressToken() any { return getProgressToken(x) }
389453
func (x *InitializeParams) SetProgressToken(t any) { setProgressToken(x, t) }
390454

391-
// After receiving an initialize request from the client, the server sends this
392-
// response.
455+
// InitializeResult is sent by the server in response to an initialize request
456+
// from the client.
393457
type InitializeResult struct {
394458
// This property is reserved by the protocol to allow clients and servers to
395459
// attach additional metadata to their responses.
396-
Meta `json:"_meta,omitempty"`
460+
Meta `json:"_meta,omitempty"`
461+
// Capabilities describes the server's capabilities.
397462
Capabilities *ServerCapabilities `json:"capabilities"`
398463
// Instructions describing how to use the server and its features.
399464
//
@@ -411,8 +476,8 @@ type InitializeResult struct {
411476
func (*InitializeResult) isResult() {}
412477

413478
type InitializedParams struct {
414-
// This property is reserved by the protocol to allow clients and servers to
415-
// attach additional metadata to their responses.
479+
// Meta is reserved by the protocol to allow clients and servers to attach
480+
// additional metadata to their responses.
416481
Meta `json:"_meta,omitempty"`
417482
}
418483

@@ -875,7 +940,10 @@ func (x *RootsListChangedParams) isParams() {}
875940
func (x *RootsListChangedParams) GetProgressToken() any { return getProgressToken(x) }
876941
func (x *RootsListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) }
877942

878-
// SamplingCapabilities describes the capabilities for sampling.
943+
// TODO: to be consistent with ServerCapabilities, move the capability types
944+
// below directly above ClientCapabilities.
945+
946+
// SamplingCapabilities describes the client's support for sampling.
879947
type SamplingCapabilities struct{}
880948

881949
// ElicitationCapabilities describes the capabilities for elicitation.
@@ -1160,50 +1228,52 @@ type Implementation struct {
11601228
Icons []Icon `json:"icons,omitempty"`
11611229
}
11621230

1163-
// Present if the server supports argument autocompletion suggestions.
1231+
// CompletionCapabilities describes the server's support for argument autocompletion.
11641232
type CompletionCapabilities struct{}
11651233

1166-
// Present if the server supports sending log messages to the client.
1234+
// LoggingCapabilities describes the server's support for sending log messages to the client.
11671235
type LoggingCapabilities struct{}
11681236

1169-
// Present if the server offers any prompt templates.
1237+
// PromptCapabilities describes the server's support for prompts.
11701238
type PromptCapabilities struct {
11711239
// Whether this server supports notifications for changes to the prompt list.
11721240
ListChanged bool `json:"listChanged,omitempty"`
11731241
}
11741242

1175-
// Present if the server offers any resources to read.
1243+
// ResourceCapabilities describes the server's support for resources.
11761244
type ResourceCapabilities struct {
1177-
// Whether this server supports notifications for changes to the resource list.
1245+
// ListChanged reports whether the client supports notifications for
1246+
// changes to the resource list.
11781247
ListChanged bool `json:"listChanged,omitempty"`
1179-
// Whether this server supports subscribing to resource updates.
1248+
// Subscribe reports whether this server supports subscribing to resource
1249+
// updates.
11801250
Subscribe bool `json:"subscribe,omitempty"`
11811251
}
11821252

1183-
// Capabilities that a server may support. Known capabilities are defined here,
1184-
// in this schema, but this is not a closed set: any server can define its own,
1185-
// additional capabilities.
1253+
// ToolCapabilities describes the server's support for tools.
1254+
type ToolCapabilities struct {
1255+
// ListChanged reports whether the client supports notifications for
1256+
// changes to the tool list.
1257+
ListChanged bool `json:"listChanged,omitempty"`
1258+
}
1259+
1260+
// ServerCapabilities describes capabilities that a server supports.
11861261
type ServerCapabilities struct {
1187-
// Present if the server supports argument autocompletion suggestions.
1188-
Completions *CompletionCapabilities `json:"completions,omitempty"`
1189-
// Experimental, non-standard capabilities that the server supports.
1262+
// Experimental reports non-standard capabilities that the server supports.
11901263
Experimental map[string]any `json:"experimental,omitempty"`
1191-
// Present if the server supports sending log messages to the client.
1264+
// Completions is present if the server supports argument autocompletion
1265+
// suggestions.
1266+
Completions *CompletionCapabilities `json:"completions,omitempty"`
1267+
// Logging is present if the server supports log messages.
11921268
Logging *LoggingCapabilities `json:"logging,omitempty"`
1193-
// Present if the server offers any prompt templates.
1269+
// Prompts is present if the server supports prompts.
11941270
Prompts *PromptCapabilities `json:"prompts,omitempty"`
1195-
// Present if the server offers any resources to read.
1271+
// Resources is present if the server supports resourcs.
11961272
Resources *ResourceCapabilities `json:"resources,omitempty"`
1197-
// Present if the server offers any tools to call.
1273+
// Tools is present if the supports tools.
11981274
Tools *ToolCapabilities `json:"tools,omitempty"`
11991275
}
12001276

1201-
// Present if the server offers any tools to call.
1202-
type ToolCapabilities struct {
1203-
// Whether this server supports notifications for changes to the tool list.
1204-
ListChanged bool `json:"listChanged,omitempty"`
1205-
}
1206-
12071277
const (
12081278
methodCallTool = "tools/call"
12091279
notificationCancelled = "notifications/cancelled"

mcp/server.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,7 @@ func (s *Server) AddReceivingMiddleware(middleware ...Middleware) {
11381138
// curating these method flags.
11391139
var serverMethodInfos = map[string]methodInfo{
11401140
methodComplete: newServerMethodInfo(serverMethod((*Server).complete), 0),
1141-
methodInitialize: newServerMethodInfo(serverSessionMethod((*ServerSession).initialize), 0),
1141+
methodInitialize: initializeMethodInfo(),
11421142
methodPing: newServerMethodInfo(serverSessionMethod((*ServerSession).ping), missingParamsOK),
11431143
methodListPrompts: newServerMethodInfo(serverMethod((*Server).listPrompts), missingParamsOK),
11441144
methodGetPrompt: newServerMethodInfo(serverMethod((*Server).getPrompt), 0),
@@ -1156,6 +1156,25 @@ var serverMethodInfos = map[string]methodInfo{
11561156
notificationProgress: newServerMethodInfo(serverSessionMethod((*ServerSession).callProgressNotificationHandler), notification),
11571157
}
11581158

1159+
// initializeMethodInfo handles the workaround for #607: we must set
1160+
// params.Capabilities.RootsV2.
1161+
func initializeMethodInfo() methodInfo {
1162+
info := newServerMethodInfo(serverSessionMethod((*ServerSession).initialize), 0)
1163+
info.unmarshalParams = func(m json.RawMessage) (Params, error) {
1164+
var params *initializeParamsV2
1165+
if m != nil {
1166+
if err := json.Unmarshal(m, &params); err != nil {
1167+
return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, params, err)
1168+
}
1169+
}
1170+
if params == nil {
1171+
return nil, fmt.Errorf(`missing required "params"`)
1172+
}
1173+
return params.toV1(), nil
1174+
}
1175+
return info
1176+
}
1177+
11591178
func (ss *ServerSession) sendingMethodInfos() map[string]methodInfo { return clientMethodInfos }
11601179

11611180
func (ss *ServerSession) receivingMethodInfos() map[string]methodInfo { return serverMethodInfos }

0 commit comments

Comments
 (0)