Skip to content

Commit 48e9682

Browse files
committed
mcp: fix broken client root capabilities
To address #607, add ClientCapabilities.RootsV2, and populate it when constructing sending and receiving InitializeParams. Also generally improve documentation for protocol types related to capabilities. Fixes #607
1 parent d1c06cb commit 48e9682

File tree

6 files changed

+268
-47
lines changed

6 files changed

+268
-47
lines changed

mcp/client.go

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

129129
func (c *Client) capabilities() *ClientCapabilities {
130130
caps := &ClientCapabilities{}
131+
// Due to an oversight (#607), roots require special handling.
131132
caps.Roots.ListChanged = true
133+
caps.RootsV2 = &RootsCapabilities{
134+
ListChanged: true,
135+
}
132136
if c.opts.CreateMessageHandler != nil {
133137
caps.Sampling = &SamplingCapabilities{}
134138
}

mcp/client_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ func TestClientPaginateVariousPageSizes(t *testing.T) {
192192
}
193193

194194
func TestClientCapabilities(t *testing.T) {
195+
canListRoots := RootsCapabilities{ListChanged: true}
195196
testCases := []struct {
196197
name string
197198
configureClient func(s *Client)
@@ -202,9 +203,8 @@ func TestClientCapabilities(t *testing.T) {
202203
name: "With initial capabilities",
203204
configureClient: func(s *Client) {},
204205
wantCapabilities: &ClientCapabilities{
205-
Roots: struct {
206-
ListChanged bool "json:\"listChanged,omitempty\""
207-
}{ListChanged: true},
206+
Roots: canListRoots,
207+
RootsV2: &canListRoots,
208208
},
209209
},
210210
{
@@ -216,9 +216,8 @@ func TestClientCapabilities(t *testing.T) {
216216
},
217217
},
218218
wantCapabilities: &ClientCapabilities{
219-
Roots: struct {
220-
ListChanged bool "json:\"listChanged,omitempty\""
221-
}{ListChanged: true},
219+
Roots: canListRoots,
220+
RootsV2: &canListRoots,
222221
Sampling: &SamplingCapabilities{},
223222
},
224223
},
@@ -232,9 +231,8 @@ func TestClientCapabilities(t *testing.T) {
232231
},
233232
},
234233
wantCapabilities: &ClientCapabilities{
235-
Roots: struct {
236-
ListChanged bool "json:\"listChanged,omitempty\""
237-
}{ListChanged: true},
234+
Roots: canListRoots,
235+
RootsV2: &canListRoots,
238236
Elicitation: &ElicitationCapabilities{
239237
Form: &FormElicitationCapabilities{},
240238
},
@@ -253,6 +251,7 @@ func TestClientCapabilities(t *testing.T) {
253251
Roots: struct {
254252
ListChanged bool "json:\"listChanged,omitempty\""
255253
}{ListChanged: true},
254+
RootsV2: &RootsCapabilities{ListChanged: true},
256255
Elicitation: &ElicitationCapabilities{
257256
URL: &URLElicitationCapabilities{},
258257
},
@@ -271,6 +270,7 @@ func TestClientCapabilities(t *testing.T) {
271270
Roots: struct {
272271
ListChanged bool "json:\"listChanged,omitempty\""
273272
}{ListChanged: true},
273+
RootsV2: &RootsCapabilities{ListChanged: true},
274274
Elicitation: &ElicitationCapabilities{
275275
Form: &FormElicitationCapabilities{},
276276
URL: &URLElicitationCapabilities{},

mcp/protocol.go

Lines changed: 103 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -177,23 +177,58 @@ 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+
// RootsCapabilities describes a client's support for roots.
181+
type RootsCapabilities 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 RootsCapabilities 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 *RootsCapabilities `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+
// RootsCapabilities.
221+
type clientCapabilitiesV2 struct {
222+
ClientCapabilities
223+
Roots *RootsCapabilities `json:"roots,omitempty"`
224+
}
225+
226+
func (c *clientCapabilitiesV2) toV1() *ClientCapabilities {
227+
caps := c.ClientCapabilities
228+
caps.RootsV2 = c.Roots
229+
return &caps
230+
}
231+
197232
type CompleteParamsArgument struct {
198233
// The name of the argument
199234
Name string `json:"name"`
@@ -373,27 +408,53 @@ type GetPromptResult struct {
373408

374409
func (*GetPromptResult) isResult() {}
375410

411+
// InitializeParams is sent by the client to initialize the session.
376412
type InitializeParams struct {
377413
// This property is reserved by the protocol to allow clients and servers to
378414
// attach additional metadata to their responses.
379-
Meta `json:"_meta,omitempty"`
415+
Meta `json:"_meta,omitempty"`
416+
// Capabilities describes the client's capabilities.
380417
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.
418+
// ClientInfo provides information about the client.
419+
ClientInfo *Implementation `json:"clientInfo"`
420+
// ProtocolVersion is the latest version of the Model Context Protocol that
421+
// the client supports.
384422
ProtocolVersion string `json:"protocolVersion"`
385423
}
386424

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

391-
// After receiving an initialize request from the client, the server sends this
392-
// response.
451+
// InitializeResult is sent by the server in response to an initialize request
452+
// from the client.
393453
type InitializeResult struct {
394454
// This property is reserved by the protocol to allow clients and servers to
395455
// attach additional metadata to their responses.
396-
Meta `json:"_meta,omitempty"`
456+
Meta `json:"_meta,omitempty"`
457+
// Capabilities describes the server's capabilities.
397458
Capabilities *ServerCapabilities `json:"capabilities"`
398459
// Instructions describing how to use the server and its features.
399460
//
@@ -411,8 +472,8 @@ type InitializeResult struct {
411472
func (*InitializeResult) isResult() {}
412473

413474
type InitializedParams struct {
414-
// This property is reserved by the protocol to allow clients and servers to
415-
// attach additional metadata to their responses.
475+
// Meta is reserved by the protocol to allow clients and servers to attach
476+
// additional metadata to their responses.
416477
Meta `json:"_meta,omitempty"`
417478
}
418479

@@ -875,7 +936,10 @@ func (x *RootsListChangedParams) isParams() {}
875936
func (x *RootsListChangedParams) GetProgressToken() any { return getProgressToken(x) }
876937
func (x *RootsListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) }
877938

878-
// SamplingCapabilities describes the capabilities for sampling.
939+
// TODO: to be consistent with ServerCapabilities, move the capability types
940+
// below directly above ClientCapabilities.
941+
942+
// SamplingCapabilities describes the client's support for sampling.
879943
type SamplingCapabilities struct{}
880944

881945
// ElicitationCapabilities describes the capabilities for elicitation.
@@ -1160,50 +1224,52 @@ type Implementation struct {
11601224
Icons []Icon `json:"icons,omitempty"`
11611225
}
11621226

1163-
// Present if the server supports argument autocompletion suggestions.
1227+
// CompletionCapabilities describes the server's support for argument autocompletion.
11641228
type CompletionCapabilities struct{}
11651229

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

1169-
// Present if the server offers any prompt templates.
1233+
// PromptCapabilities describes the server's support for prompts.
11701234
type PromptCapabilities struct {
11711235
// Whether this server supports notifications for changes to the prompt list.
11721236
ListChanged bool `json:"listChanged,omitempty"`
11731237
}
11741238

1175-
// Present if the server offers any resources to read.
1239+
// ResourceCapabilities describes the server's support for resources.
11761240
type ResourceCapabilities struct {
1177-
// Whether this server supports notifications for changes to the resource list.
1241+
// ListChanged reports whether the client supports notifications for
1242+
// changes to the resource list.
11781243
ListChanged bool `json:"listChanged,omitempty"`
1179-
// Whether this server supports subscribing to resource updates.
1244+
// Subscribe reports whether this server supports subscribing to resource
1245+
// updates.
11801246
Subscribe bool `json:"subscribe,omitempty"`
11811247
}
11821248

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.
1249+
// ToolCapabilities describes the server's support for tools.
1250+
type ToolCapabilities struct {
1251+
// ListChanged reports whether the client supports notifications for
1252+
// changes to the tool list.
1253+
ListChanged bool `json:"listChanged,omitempty"`
1254+
}
1255+
1256+
// ServerCapabilities describes capabilities that a server supports.
11861257
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.
1258+
// Experimental reports non-standard capabilities that the server supports.
11901259
Experimental map[string]any `json:"experimental,omitempty"`
1191-
// Present if the server supports sending log messages to the client.
1260+
// Completions is present if the server supports argument autocompletion
1261+
// suggestions.
1262+
Completions *CompletionCapabilities `json:"completions,omitempty"`
1263+
// Logging is present if the server supports log messages.
11921264
Logging *LoggingCapabilities `json:"logging,omitempty"`
1193-
// Present if the server offers any prompt templates.
1265+
// Prompts is present if the server supports prompts.
11941266
Prompts *PromptCapabilities `json:"prompts,omitempty"`
1195-
// Present if the server offers any resources to read.
1267+
// Resources is present if the server supports resourcs.
11961268
Resources *ResourceCapabilities `json:"resources,omitempty"`
1197-
// Present if the server offers any tools to call.
1269+
// Tools is present if the supports tools.
11981270
Tools *ToolCapabilities `json:"tools,omitempty"`
11991271
}
12001272

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-
12071273
const (
12081274
methodCallTool = "tools/call"
12091275
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)