diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a1bbf998 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +### Go.AllowList template +# 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 +!*/ \ No newline at end of file diff --git a/mcp/client.go b/mcp/client.go index b386294c..e443a2e5 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -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 { + 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. diff --git a/mcp/client_test.go b/mcp/client_test.go index 73fe09e6..a8446d58 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -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") + } +} diff --git a/mcp/protocol.go b/mcp/protocol.go index 00dcd14d..133757c2 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -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 @@ -907,46 +907,51 @@ type Implementation struct { Version string `json:"version"` } +// CompletionCapabilities represents server capabilities for argument autocompletion suggestions. // 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"` } diff --git a/mcp/server.go b/mcp/server.go index 960d35f2..84c2bb21 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -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 } diff --git a/mcp/server_test.go b/mcp/server_test.go index 9a36c962..eea8dfa8 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -236,14 +236,14 @@ 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{}, }, }, { @@ -251,10 +251,10 @@ func TestServerCapabilities(t *testing.T) { 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}, }, }, { @@ -262,10 +262,10 @@ func TestServerCapabilities(t *testing.T) { 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}, }, }, { @@ -273,10 +273,10 @@ func TestServerCapabilities(t *testing.T) { 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}, }, }, { @@ -292,10 +292,10 @@ 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}, }, }, { @@ -303,10 +303,10 @@ func TestServerCapabilities(t *testing.T) { 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}, }, }, { @@ -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}, }, }, } diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index 864265e5..5381b57b 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -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"},