Skip to content
Closed
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
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
### Go.AllowList template
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this file from PR

# Allowlisting gitignore template for GO projects prevents us
# from adding various unwanted local files, such as generated
# files, developer configurations or IDE-specific files etc.
#
# Recommended: Go.AllowList.gitignore

# Ignore everything
*

# But not these files...
!/.gitignore

!*.go
!go.*

!*.sh

!README.md
!CONTRIBUTING.md
!LICENSE

!design/**
!.github/**

# ...even if they are in subdirectories
!*/
53 changes: 53 additions & 0 deletions mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,59 @@ func (cs *ClientSession) ID() string {
return cs.mcpConn.SessionID()
}

// ServerCapabilities returns a copy of the server capabilities obtained during initialization.
// If the session has not been initialized or capabilities are not available, it returns nil.
func (cs *ClientSession) ServerCapabilities() *ServerCapabilities {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can just use an exported field. The doc will say not to modify it, but even if someone does, they're only hurting themselves, as far as I can tell.

Or if we're not sure, let's use the function, but not bother copying until we discover a case where it could matter. But really I think the field suffices.

/cc @samthanawalla @findleyr

Copy link
Contributor

@samthanawalla samthanawalla Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with not copying right now.

I think we should go with the cs.ServerCapabilities() function because cs.InitializeResult.Capabilities is a bit cumbersome/less obvious to use. Plus, like you said, we can add copying to ServerCapabilities later.

if cs.initializeResult == nil || cs.initializeResult.Capabilities == nil {
return nil
}

// Create a copy of the capabilities
caps := &ServerCapabilities{}

// Copy experimental capabilities
if cs.initializeResult.Capabilities.Experimental != nil {
caps.Experimental = make(map[string]struct{})
for k, v := range cs.initializeResult.Capabilities.Experimental {
caps.Experimental[k] = v
}
}

// Copy completion capabilities
if cs.initializeResult.Capabilities.Completions != nil {
caps.Completions = &CompletionCapabilities{}
}

// Copy logging capabilities
if cs.initializeResult.Capabilities.Logging != nil {
caps.Logging = &LoggingCapabilities{}
}

// Copy prompt capabilities
if cs.initializeResult.Capabilities.Prompts != nil {
caps.Prompts = &PromptCapabilities{
ListChanged: cs.initializeResult.Capabilities.Prompts.ListChanged,
}
}

// Copy resource capabilities
if cs.initializeResult.Capabilities.Resources != nil {
caps.Resources = &ResourceCapabilities{
ListChanged: cs.initializeResult.Capabilities.Resources.ListChanged,
Subscribe: cs.initializeResult.Capabilities.Resources.Subscribe,
}
}

// Copy tool capabilities
if cs.initializeResult.Capabilities.Tools != nil {
caps.Tools = &ToolCapabilities{
ListChanged: cs.initializeResult.Capabilities.Tools.ListChanged,
}
}

return caps
}

// Close performs a graceful close of the connection, preventing new requests
// from being handled, and waiting for ongoing requests to return. Close then
// terminates the connection.
Expand Down
51 changes: 51 additions & 0 deletions mcp/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,54 @@ func TestClientPaginateVariousPageSizes(t *testing.T) {
})
}
}

func TestClientSession_ServerCapabilities_ReturnsACopy(t *testing.T) {
// Test that modifying the returned capabilities doesn't affect the original
session := &ClientSession{
initializeResult: &InitializeResult{
Capabilities: &ServerCapabilities{
Experimental: map[string]struct{}{
"original": {},
},
Prompts: &PromptCapabilities{
ListChanged: true,
},
Resources: &ResourceCapabilities{
ListChanged: true,
Subscribe: true,
},
},
},
}

// Get the capabilities
caps := session.ServerCapabilities()
if caps == nil {
t.Fatal("expected non-nil capabilities")
}

// Modify the returned capabilities
caps.Experimental["new_key"] = struct{}{}
caps.Prompts.ListChanged = false
caps.Resources.Subscribe = false

// Get capabilities again to verify the original is unchanged
caps2 := session.ServerCapabilities()
if caps2 == nil {
t.Fatal("expected non-nil capabilities")
}

// Verify original values are unchanged
if _, exists := caps2.Experimental["new_key"]; exists {
t.Error("experimental capabilities were not properly copied")
}
if _, exists := caps2.Experimental["original"]; !exists {
t.Error("original experimental capability was lost")
}
if caps2.Prompts.ListChanged != true {
t.Error("prompts ListChanged was modified in original")
}
if caps2.Resources.Subscribe != true {
t.Error("resources Subscribe was modified in original")
}
}
31 changes: 18 additions & 13 deletions mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ 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"`
Capabilities *serverCapabilities `json:"capabilities"`
Capabilities *ServerCapabilities `json:"capabilities"`
// Instructions describing how to use the server and its features.
//
// This can be used by clients to improve the LLM's understanding of available
Expand Down Expand Up @@ -907,46 +907,51 @@ type Implementation struct {
Version string `json:"version"`
}

// CompletionCapabilities represents server capabilities for argument autocompletion suggestions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We say "is" or "are" instead of represents.
Change throughout.

// Present if the server supports argument autocompletion suggestions.
type completionCapabilities struct{}
type CompletionCapabilities struct{}

// LoggingCapabilities represents server capabilities for sending log messages to the client.
// Present if the server supports sending log messages to the client.
type loggingCapabilities struct{}
type LoggingCapabilities struct{}

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

// ResourceCapabilities represents server capabilities for resources.
// Present if the server offers any resources to read.
type resourceCapabilities struct {
type ResourceCapabilities struct {
// Whether this server supports notifications for changes to the resource list.
ListChanged bool `json:"listChanged,omitempty"`
// Whether this server supports subscribing to resource updates.
Subscribe bool `json:"subscribe,omitempty"`
}

// Capabilities that a server may support. Known capabilities are defined here,
// ServerCapabilities represents the 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.
type serverCapabilities struct {
type ServerCapabilities struct {
// Present if the server supports argument autocompletion suggestions.
Completions *completionCapabilities `json:"completions,omitempty"`
Completions *CompletionCapabilities `json:"completions,omitempty"`
// Experimental, non-standard capabilities that the server supports.
Experimental map[string]struct{} `json:"experimental,omitempty"`
// Present if the server supports sending log messages to the client.
Logging *loggingCapabilities `json:"logging,omitempty"`
Logging *LoggingCapabilities `json:"logging,omitempty"`
// Present if the server offers any prompt templates.
Prompts *promptCapabilities `json:"prompts,omitempty"`
Prompts *PromptCapabilities `json:"prompts,omitempty"`
// Present if the server offers any resources to read.
Resources *resourceCapabilities `json:"resources,omitempty"`
Resources *ResourceCapabilities `json:"resources,omitempty"`
// Present if the server offers any tools to call.
Tools *toolCapabilities `json:"tools,omitempty"`
Tools *ToolCapabilities `json:"tools,omitempty"`
}

// ToolCapabilities represents server capabilities for tools.
// Present if the server offers any tools to call.
type toolCapabilities struct {
type ToolCapabilities struct {
// Whether this server supports notifications for changes to the tool list.
ListChanged bool `json:"listChanged,omitempty"`
}
Expand Down
14 changes: 7 additions & 7 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,22 +225,22 @@ func (s *Server) RemoveResourceTemplates(uriTemplates ...string) {
func() bool { return s.resourceTemplates.remove(uriTemplates...) })
}

func (s *Server) capabilities() *serverCapabilities {
func (s *Server) capabilities() *ServerCapabilities {
s.mu.Lock()
defer s.mu.Unlock()

caps := &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
caps := &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
}
if s.tools.len() > 0 {
caps.Tools = &toolCapabilities{ListChanged: true}
caps.Tools = &ToolCapabilities{ListChanged: true}
}
if s.prompts.len() > 0 {
caps.Prompts = &promptCapabilities{ListChanged: true}
caps.Prompts = &PromptCapabilities{ListChanged: true}
}
if s.resources.len() > 0 || s.resourceTemplates.len() > 0 {
caps.Resources = &resourceCapabilities{ListChanged: true}
caps.Resources = &ResourceCapabilities{ListChanged: true}
if s.opts.SubscribeHandler != nil {
caps.Resources.Subscribe = true
}
Expand Down
60 changes: 30 additions & 30 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,47 +236,47 @@ func TestServerCapabilities(t *testing.T) {
name string
configureServer func(s *Server)
serverOpts ServerOptions
wantCapabilities *serverCapabilities
wantCapabilities *ServerCapabilities
}{
{
name: "No capabilities",
configureServer: func(s *Server) {},
wantCapabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
wantCapabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
},
},
{
name: "With prompts",
configureServer: func(s *Server) {
s.AddPrompt(&Prompt{Name: "p"}, nil)
},
wantCapabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
Prompts: &promptCapabilities{ListChanged: true},
wantCapabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
Prompts: &PromptCapabilities{ListChanged: true},
},
},
{
name: "With resources",
configureServer: func(s *Server) {
s.AddResource(&Resource{URI: "file:///r"}, nil)
},
wantCapabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
Resources: &resourceCapabilities{ListChanged: true},
wantCapabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
Resources: &ResourceCapabilities{ListChanged: true},
},
},
{
name: "With resource templates",
configureServer: func(s *Server) {
s.AddResourceTemplate(&ResourceTemplate{URITemplate: "file:///rt"}, nil)
},
wantCapabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
Resources: &resourceCapabilities{ListChanged: true},
wantCapabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
Resources: &ResourceCapabilities{ListChanged: true},
},
},
{
Expand All @@ -292,21 +292,21 @@ func TestServerCapabilities(t *testing.T) {
return nil
},
},
wantCapabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
Resources: &resourceCapabilities{ListChanged: true, Subscribe: true},
wantCapabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
Resources: &ResourceCapabilities{ListChanged: true, Subscribe: true},
},
},
{
name: "With tools",
configureServer: func(s *Server) {
s.AddTool(tool, nil)
},
wantCapabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
Tools: &toolCapabilities{ListChanged: true},
wantCapabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
Tools: &ToolCapabilities{ListChanged: true},
},
},
{
Expand All @@ -325,12 +325,12 @@ func TestServerCapabilities(t *testing.T) {
return nil
},
},
wantCapabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
Prompts: &promptCapabilities{ListChanged: true},
Resources: &resourceCapabilities{ListChanged: true, Subscribe: true},
Tools: &toolCapabilities{ListChanged: true},
wantCapabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
Prompts: &PromptCapabilities{ListChanged: true},
Resources: &ResourceCapabilities{ListChanged: true, Subscribe: true},
Tools: &ToolCapabilities{ListChanged: true},
},
},
}
Expand Down
8 changes: 4 additions & 4 deletions mcp/streamable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,10 @@ func TestStreamableServerTransport(t *testing.T) {
// Predefined steps, to avoid repetition below.
initReq := req(1, "initialize", &InitializeParams{})
initResp := resp(1, &InitializeResult{
Capabilities: &serverCapabilities{
Completions: &completionCapabilities{},
Logging: &loggingCapabilities{},
Tools: &toolCapabilities{ListChanged: true},
Capabilities: &ServerCapabilities{
Completions: &CompletionCapabilities{},
Logging: &LoggingCapabilities{},
Tools: &ToolCapabilities{ListChanged: true},
},
ProtocolVersion: latestProtocolVersion,
ServerInfo: &Implementation{Name: "testServer", Version: "v1.0.0"},
Expand Down