Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/rough_edges.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ v2.

- `AudioContent.MarshalJSON` should have had a pointer receiver, to be
consistent with other content types.

- `ClientCapabilities.Roots` should have been a distinguished struct pointer
([see #607](https://github.com/modelcontextprotocol/go-sdk/issues/607)).

**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
semantics of other capability fields.
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func ExampleLoggingTransport() {

// Output:
// read: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.0.1"}}}
// write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18"}}
// write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true}}}}
// write: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
}
```
Expand Down
6 changes: 6 additions & 0 deletions internal/docs/rough_edges.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ v2.

- `AudioContent.MarshalJSON` should have had a pointer receiver, to be
consistent with other content types.

- `ClientCapabilities.Roots` should have been a distinguished struct pointer
([see #607](https://github.com/modelcontextprotocol/go-sdk/issues/607)).

**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
semantics of other capability fields.
4 changes: 4 additions & 0 deletions mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ type ClientSessionOptions struct {

func (c *Client) capabilities() *ClientCapabilities {
caps := &ClientCapabilities{}
// Due to an oversight (#607), roots require special handling.
caps.Roots.ListChanged = true
caps.RootsV2 = &RootCapabilities{
ListChanged: true,
}
if c.opts.CreateMessageHandler != nil {
caps.Sampling = &SamplingCapabilities{}
}
Expand Down
25 changes: 10 additions & 15 deletions mcp/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,8 @@ func TestClientCapabilities(t *testing.T) {
name: "With initial capabilities",
configureClient: func(s *Client) {},
wantCapabilities: &ClientCapabilities{
Roots: struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{ListChanged: true},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
},
},
{
Expand All @@ -216,9 +215,8 @@ func TestClientCapabilities(t *testing.T) {
},
},
wantCapabilities: &ClientCapabilities{
Roots: struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{ListChanged: true},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Sampling: &SamplingCapabilities{},
},
},
Expand All @@ -232,9 +230,8 @@ func TestClientCapabilities(t *testing.T) {
},
},
wantCapabilities: &ClientCapabilities{
Roots: struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{ListChanged: true},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
Form: &FormElicitationCapabilities{},
},
Expand All @@ -250,9 +247,8 @@ func TestClientCapabilities(t *testing.T) {
},
},
wantCapabilities: &ClientCapabilities{
Roots: struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{ListChanged: true},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
Expand All @@ -268,9 +264,8 @@ func TestClientCapabilities(t *testing.T) {
},
},
wantCapabilities: &ClientCapabilities{
Roots: struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{ListChanged: true},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
Form: &FormElicitationCapabilities{},
URL: &URLElicitationCapabilities{},
Expand Down
144 changes: 107 additions & 37 deletions mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,23 +177,62 @@ func (x *CancelledParams) isParams() {}
func (x *CancelledParams) GetProgressToken() any { return getProgressToken(x) }
func (x *CancelledParams) SetProgressToken(t any) { setProgressToken(x, t) }

// RootCapabilities describes a client's support for roots.
type RootCapabilities struct {
// ListChanged reports whether the client supports notifications for
// changes to the roots list.
ListChanged bool `json:"listChanged,omitempty"`
}

// Capabilities a client may support. Known capabilities are defined here, in
// this schema, but this is not a closed set: any client can define its own,
// additional capabilities.
type ClientCapabilities struct {
// Experimental, non-standard capabilities that the client supports.
// Experimental reports non-standard capabilities that the client supports.
Experimental map[string]any `json:"experimental,omitempty"`
// Present if the client supports listing roots.
// Roots describes the client's support for roots.
//
// Deprecated: use RootsV2. As described in #607, Roots should have been a
// pointer to a RootCapabilities value. Roots will be continue to be
// populated, but any new fields will only be added in the RootsV2 field.
Roots struct {
// Whether the client supports notifications for changes to the roots list.
// ListChanged reports whether the client supports notifications for
// changes to the roots list.
ListChanged bool `json:"listChanged,omitempty"`
} `json:"roots,omitempty"`
// Present if the client supports sampling from an LLM.
// RootsV2 is present if the client supports roots.
RootsV2 *RootCapabilities `json:"-"`
// Sampling is present if the client supports sampling from an LLM.
Sampling *SamplingCapabilities `json:"sampling,omitempty"`
// Present if the client supports elicitation from the server.
// Elicitation is present if the client supports elicitation from the server.
Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"`
}

func (c *ClientCapabilities) toV2() *clientCapabilitiesV2 {
return &clientCapabilitiesV2{
ClientCapabilities: *c,
Roots: c.RootsV2,
}
}

// clientCapabilitiesV2 is a version of ClientCapabilities that fixes the bug
// described in #607: Roots should have been a pointer to value type
// RootCapabilities.
type clientCapabilitiesV2 struct {
ClientCapabilities
Roots *RootCapabilities `json:"roots,omitempty"`
}

func (c *clientCapabilitiesV2) toV1() *ClientCapabilities {
caps := c.ClientCapabilities
caps.RootsV2 = c.Roots
// Sync Roots from RootsV2 for backward compatibility (#607).
if caps.RootsV2 != nil {
caps.Roots = *caps.RootsV2
}
return &caps
}

type CompleteParamsArgument struct {
// The name of the argument
Name string `json:"name"`
Expand Down Expand Up @@ -373,27 +412,53 @@ type GetPromptResult struct {

func (*GetPromptResult) isResult() {}

// InitializeParams is sent by the client to initialize the session.
type InitializeParams struct {
// This property is reserved by the protocol to allow clients and servers to
// attach additional metadata to their responses.
Meta `json:"_meta,omitempty"`
Meta `json:"_meta,omitempty"`
// Capabilities describes the client's capabilities.
Capabilities *ClientCapabilities `json:"capabilities"`
ClientInfo *Implementation `json:"clientInfo"`
// The latest version of the Model Context Protocol that the client supports.
// The client may decide to support older versions as well.
// ClientInfo provides information about the client.
ClientInfo *Implementation `json:"clientInfo"`
// ProtocolVersion is the latest version of the Model Context Protocol that
// the client supports.
ProtocolVersion string `json:"protocolVersion"`
}

func (p *InitializeParams) toV2() *initializeParamsV2 {
return &initializeParamsV2{
InitializeParams: *p,
Capabilities: p.Capabilities.toV2(),
}
}

// initializeParamsV2 works around the mistake in #607: Capabilities.Roots
// should have been a pointer.
type initializeParamsV2 struct {
InitializeParams
Capabilities *clientCapabilitiesV2 `json:"capabilities"`
}

func (p *initializeParamsV2) toV1() *InitializeParams {
p1 := p.InitializeParams
if p.Capabilities != nil {
p1.Capabilities = p.Capabilities.toV1()
}
return &p1
}

func (x *InitializeParams) isParams() {}
func (x *InitializeParams) GetProgressToken() any { return getProgressToken(x) }
func (x *InitializeParams) SetProgressToken(t any) { setProgressToken(x, t) }

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

type InitializedParams struct {
// This property is reserved by the protocol to allow clients and servers to
// attach additional metadata to their responses.
// Meta is reserved by the protocol to allow clients and servers to attach
// additional metadata to their responses.
Meta `json:"_meta,omitempty"`
}

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

// SamplingCapabilities describes the capabilities for sampling.
// TODO: to be consistent with ServerCapabilities, move the capability types
// below directly above ClientCapabilities.

// SamplingCapabilities describes the client's support for sampling.
type SamplingCapabilities struct{}

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

// Present if the server supports argument autocompletion suggestions.
// CompletionCapabilities describes the server's support for argument autocompletion.
type CompletionCapabilities struct{}

// Present if the server supports sending log messages to the client.
// LoggingCapabilities describes the server's support for sending log messages to the client.
type LoggingCapabilities struct{}

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

// Present if the server offers any resources to read.
// ResourceCapabilities describes the server's support for resources.
type ResourceCapabilities struct {
// Whether this server supports notifications for changes to the resource list.
// ListChanged reports whether the client supports notifications for
// changes to the resource list.
ListChanged bool `json:"listChanged,omitempty"`
// Whether this server supports subscribing to resource updates.
// Subscribe reports whether this server supports subscribing to resource
// updates.
Subscribe bool `json:"subscribe,omitempty"`
}

// Capabilities that a server may support. Known capabilities are defined here,
// in this schema, but this is not a closed set: any server can define its own,
// additional capabilities.
// ToolCapabilities describes the server's support for tools.
type ToolCapabilities struct {
// ListChanged reports whether the client supports notifications for
// changes to the tool list.
ListChanged bool `json:"listChanged,omitempty"`
}

// ServerCapabilities describes capabilities that a server supports.
type ServerCapabilities struct {
// Present if the server supports argument autocompletion suggestions.
Completions *CompletionCapabilities `json:"completions,omitempty"`
// Experimental, non-standard capabilities that the server supports.
// Experimental reports non-standard capabilities that the server supports.
Experimental map[string]any `json:"experimental,omitempty"`
// Present if the server supports sending log messages to the client.
// Completions is present if the server supports argument autocompletion
// suggestions.
Completions *CompletionCapabilities `json:"completions,omitempty"`
// Logging is present if the server supports log messages.
Logging *LoggingCapabilities `json:"logging,omitempty"`
// Present if the server offers any prompt templates.
// Prompts is present if the server supports prompts.
Prompts *PromptCapabilities `json:"prompts,omitempty"`
// Present if the server offers any resources to read.
// Resources is present if the server supports resourcs.
Resources *ResourceCapabilities `json:"resources,omitempty"`
// Present if the server offers any tools to call.
// Tools is present if the supports tools.
Tools *ToolCapabilities `json:"tools,omitempty"`
}

// Present if the server offers any tools to call.
type ToolCapabilities struct {
// Whether this server supports notifications for changes to the tool list.
ListChanged bool `json:"listChanged,omitempty"`
}

const (
methodCallTool = "tools/call"
notificationCancelled = "notifications/cancelled"
Expand Down
21 changes: 20 additions & 1 deletion mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@ func (s *Server) AddReceivingMiddleware(middleware ...Middleware) {
// curating these method flags.
var serverMethodInfos = map[string]methodInfo{
methodComplete: newServerMethodInfo(serverMethod((*Server).complete), 0),
methodInitialize: newServerMethodInfo(serverSessionMethod((*ServerSession).initialize), 0),
methodInitialize: initializeMethodInfo(),
methodPing: newServerMethodInfo(serverSessionMethod((*ServerSession).ping), missingParamsOK),
methodListPrompts: newServerMethodInfo(serverMethod((*Server).listPrompts), missingParamsOK),
methodGetPrompt: newServerMethodInfo(serverMethod((*Server).getPrompt), 0),
Expand All @@ -1156,6 +1156,25 @@ var serverMethodInfos = map[string]methodInfo{
notificationProgress: newServerMethodInfo(serverSessionMethod((*ServerSession).callProgressNotificationHandler), notification),
}

// initializeMethodInfo handles the workaround for #607: we must set
// params.Capabilities.RootsV2.
func initializeMethodInfo() methodInfo {
info := newServerMethodInfo(serverSessionMethod((*ServerSession).initialize), 0)
info.unmarshalParams = func(m json.RawMessage) (Params, error) {
var params *initializeParamsV2
if m != nil {
if err := json.Unmarshal(m, &params); err != nil {
return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, params, err)
}
}
if params == nil {
return nil, fmt.Errorf(`missing required "params"`)
}
return params.toV1(), nil
}
return info
}

func (ss *ServerSession) sendingMethodInfos() map[string]methodInfo { return clientMethodInfos }

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