From e1b1cd0072e4f5ca6963f4bc389bf41bc9833d58 Mon Sep 17 00:00:00 2001 From: Sam Thanawalla <17936816+samthanawalla@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:09:15 -0400 Subject: [PATCH] mcp: implement completion (#55) This CL implements a completion handler for the server and a complete method for the client. Fixes: #12 ## Motivation and Context This adds support for completion. ## How Has This Been Tested? Added tests and an example. ## Breaking Changes N/A ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context --- examples/completion/main.go | 49 +++++ mcp/client.go | 5 + mcp/protocol.go | 93 +++++++- mcp/protocol_test.go | 417 ++++++++++++++++++++++++++++++++++++ mcp/server.go | 11 + mcp/streamable_test.go | 9 +- 6 files changed, 579 insertions(+), 5 deletions(-) create mode 100644 examples/completion/main.go diff --git a/examples/completion/main.go b/examples/completion/main.go new file mode 100644 index 0000000..a24299b --- /dev/null +++ b/examples/completion/main.go @@ -0,0 +1,49 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "fmt" + "log" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// This example demonstrates the minimal code to declare and assign +// a CompletionHandler to an MCP Server's options. +func main() { + // Define your custom CompletionHandler logic. + myCompletionHandler := func(_ context.Context, _ *mcp.ServerSession, params *mcp.CompleteParams) (*mcp.CompleteResult, error) { + // In a real application, you'd implement actual completion logic here. + // For this example, we return a fixed set of suggestions. + var suggestions []string + switch params.Ref.Type { + case "ref/prompt": + suggestions = []string{"suggestion1", "suggestion2", "suggestion3"} + case "ref/resource": + suggestions = []string{"suggestion4", "suggestion5", "suggestion6"} + default: + return nil, fmt.Errorf("unrecognized content type %s", params.Ref.Type) + } + + return &mcp.CompleteResult{ + Completion: mcp.CompletionResultDetails{ + HasMore: false, + Total: len(suggestions), + Values: suggestions, + }, + }, nil + } + + // Create the MCP Server instance and assign the handler. + // No server running, just showing the configuration. + _ = mcp.NewServer("myServer", "v1.0.0", &mcp.ServerOptions{ + CompletionHandler: myCompletionHandler, + }) + + log.Println("MCP Server instance created with a CompletionHandler assigned (but not running).") + log.Println("This example demonstrates configuration, not live interaction.") +} diff --git a/mcp/client.go b/mcp/client.go index 856df56..a156ae2 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -257,6 +257,7 @@ func (c *Client) AddReceivingMiddleware(middleware ...Middleware[*ClientSession] // clientMethodInfos maps from the RPC method name to serverMethodInfos. var clientMethodInfos = map[string]methodInfo{ + methodComplete: newMethodInfo(sessionMethod((*ClientSession).Complete)), methodPing: newMethodInfo(sessionMethod((*ClientSession).ping)), methodListRoots: newMethodInfo(clientMethod((*Client).listRoots)), methodCreateMessage: newMethodInfo(clientMethod((*Client).createMessage)), @@ -352,6 +353,10 @@ func (cs *ClientSession) ReadResource(ctx context.Context, params *ReadResourceP return handleSend[*ReadResourceResult](ctx, cs, methodReadResource, orZero[Params](params)) } +func (cs *ClientSession) Complete(ctx context.Context, params *CompleteParams) (*CompleteResult, error) { + return handleSend[*CompleteResult](ctx, cs, methodComplete, orZero[Params](params)) +} + func (c *Client) callToolChangedHandler(ctx context.Context, s *ClientSession, params *ToolListChangedParams) (Result, error) { return callNotificationHandler(ctx, c.opts.ToolListChangedHandler, s, params) } diff --git a/mcp/protocol.go b/mcp/protocol.go index 73e133a..eba9e73 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -12,6 +12,7 @@ package mcp import ( "encoding/json" + "fmt" "github.com/modelcontextprotocol/go-sdk/jsonschema" ) @@ -133,6 +134,93 @@ type ClientCapabilities struct { Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"` } +type CompleteParamsArgument struct { + // The name of the argument + Name string `json:"name"` + // The value of the argument to use for completion matching. + Value string `json:"value"` +} + +// CompleteContext represents additional, optional context for completions. +type CompleteContext struct { + // Previously-resolved variables in a URI template or prompt. + Arguments map[string]string `json:"arguments,omitempty"` +} + +// CompleteReference represents a completion reference type (ref/prompt ref/resource). +// The Type field determines which other fields are relevant. +type CompleteReference struct { + Type string `json:"type"` + // Name is relevant when Type is "ref/prompt". + Name string `json:"name,omitempty"` + // URI is relevant when Type is "ref/resource". + URI string `json:"uri,omitempty"` +} + +func (r *CompleteReference) UnmarshalJSON(data []byte) error { + type wireCompleteReference CompleteReference // for naive unmarshaling + var r2 wireCompleteReference + if err := json.Unmarshal(data, &r2); err != nil { + return err + } + switch r2.Type { + case "ref/prompt", "ref/resource": + if r2.Type == "ref/prompt" && r2.URI != "" { + return fmt.Errorf("reference of type %q must not have a URI set", r2.Type) + } + if r2.Type == "ref/resource" && r2.Name != "" { + return fmt.Errorf("reference of type %q must not have a Name set", r2.Type) + } + default: + return fmt.Errorf("unrecognized content type %q", r2.Type) + } + *r = CompleteReference(r2) + return nil +} + +func (r *CompleteReference) MarshalJSON() ([]byte, error) { + // Validation for marshalling: ensure consistency before converting to JSON. + switch r.Type { + case "ref/prompt": + if r.URI != "" { + return nil, fmt.Errorf("reference of type %q must not have a URI set for marshalling", r.Type) + } + case "ref/resource": + if r.Name != "" { + return nil, fmt.Errorf("reference of type %q must not have a Name set for marshalling", r.Type) + } + default: + return nil, fmt.Errorf("unrecognized reference type %q for marshalling", r.Type) + } + + type wireReference CompleteReference + return json.Marshal(wireReference(*r)) +} + +type CompleteParams struct { + // This property is reserved by the protocol to allow clients and servers to + // attach additional metadata to their responses. + Meta `json:"_meta,omitempty"` + // The argument's information + Argument CompleteParamsArgument `json:"argument"` + Context *CompleteContext `json:"context,omitempty"` + Ref *CompleteReference `json:"ref"` +} + +type CompletionResultDetails struct { + HasMore bool `json:"hasMore,omitempty"` + Total int `json:"total,omitempty"` + Values []string `json:"values"` +} + +// The server's response to a completion/complete request +type CompleteResult struct { + // This property is reserved by the protocol to allow clients and servers to + // attach additional metadata to their responses. + Meta `json:"_meta,omitempty"` + Completion CompletionResultDetails `json:"completion"` +} + type CreateMessageParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -787,6 +875,9 @@ type implementation struct { Version string `json:"version"` } +// Present if the server supports argument autocompletion suggestions. +type completionCapabilities struct{} + // Present if the server supports sending log messages to the client. type loggingCapabilities struct{} @@ -809,7 +900,7 @@ type resourceCapabilities struct { // additional capabilities. type serverCapabilities struct { // Present if the server supports argument autocompletion suggestions. - Completions struct{} `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. diff --git a/mcp/protocol_test.go b/mcp/protocol_test.go index 823c409..dba80a8 100644 --- a/mcp/protocol_test.go +++ b/mcp/protocol_test.go @@ -70,6 +70,423 @@ func TestParamsMeta(t *testing.T) { p.SetProgressToken(int64(1)) } +func TestCompleteReference(t *testing.T) { + marshalTests := []struct { + name string + in CompleteReference // The Go struct to marshal + want string // The expected JSON string output + wantErr bool // True if json.Marshal is expected to return an error + }{ + { + name: "ValidPrompt", + in: CompleteReference{Type: "ref/prompt", Name: "my_prompt"}, + want: `{"type":"ref/prompt","name":"my_prompt"}`, + wantErr: false, + }, + { + name: "ValidResource", + in: CompleteReference{Type: "ref/resource", URI: "file:///path/to/resource.txt"}, + want: `{"type":"ref/resource","uri":"file:///path/to/resource.txt"}`, + wantErr: false, + }, + { + name: "ValidPromptEmptyName", + in: CompleteReference{Type: "ref/prompt", Name: ""}, + want: `{"type":"ref/prompt"}`, + wantErr: false, + }, + { + name: "ValidResourceEmptyURI", + in: CompleteReference{Type: "ref/resource", URI: ""}, + want: `{"type":"ref/resource"}`, + wantErr: false, + }, + // Error cases for MarshalJSON + { + name: "InvalidType", + in: CompleteReference{Type: "ref/unknown", Name: "something"}, + wantErr: true, + }, + { + name: "PromptWithURI", + in: CompleteReference{Type: "ref/prompt", Name: "my_prompt", URI: "unexpected_uri"}, + wantErr: true, + }, + { + name: "ResourceWithName", + in: CompleteReference{Type: "ref/resource", URI: "my_uri", Name: "unexpected_name"}, + wantErr: true, + }, + { + name: "MissingTypeField", + in: CompleteReference{Name: "missing"}, // Type is "" + wantErr: true, + }, + } + + // Define test cases specifically for Unmarshalling + unmarshalTests := []struct { + name string + in string // The JSON string input + want CompleteReference // The expected Go struct output + wantErr bool // True if json.Unmarshal is expected to return an error + }{ + { + name: "ValidPrompt", + in: `{"type":"ref/prompt","name":"my_prompt"}`, + want: CompleteReference{Type: "ref/prompt", Name: "my_prompt"}, + wantErr: false, + }, + { + name: "ValidResource", + in: `{"type":"ref/resource","uri":"file:///path/to/resource.txt"}`, + want: CompleteReference{Type: "ref/resource", URI: "file:///path/to/resource.txt"}, + wantErr: false, + }, + // Error cases for UnmarshalJSON + { + name: "UnrecognizedType", + in: `{"type":"ref/unknown","name":"something"}`, + want: CompleteReference{}, // placeholder, as unmarshal will fail + wantErr: true, + }, + { + name: "PromptWithURI", + in: `{"type":"ref/prompt","name":"my_prompt","uri":"unexpected_uri"}`, + want: CompleteReference{}, // placeholder + wantErr: true, + }, + { + name: "ResourceWithName", + in: `{"type":"ref/resource","uri":"my_uri","name":"unexpected_name"}`, + want: CompleteReference{}, // placeholder + wantErr: true, + }, + { + name: "MissingType", + in: `{"name":"missing"}`, + want: CompleteReference{}, // placeholder + wantErr: true, + }, + { + name: "InvalidJSON", + in: `invalid json`, + want: CompleteReference{}, // placeholder + wantErr: true, // json.Unmarshal will fail natively + }, + } + + // Run Marshal Tests + for _, test := range marshalTests { + t.Run("Marshal/"+test.name, func(t *testing.T) { + gotBytes, err := json.Marshal(&test.in) + if (err != nil) != test.wantErr { + t.Errorf("json.Marshal(%v) got error %v (want error %t)", test.in, err, test.wantErr) + } + if !test.wantErr { // Only check JSON output if marshal was expected to succeed + if diff := cmp.Diff(test.want, string(gotBytes)); diff != "" { + t.Errorf("json.Marshal(%v) mismatch (-want +got):\n%s", test.in, diff) + } + } + }) + } + + // Run Unmarshal Tests + for _, test := range unmarshalTests { + t.Run("Unmarshal/"+test.name, func(t *testing.T) { + var got CompleteReference + err := json.Unmarshal([]byte(test.in), &got) + + if (err != nil) != test.wantErr { + t.Errorf("json.Unmarshal(%q) got error %v (want error %t)", test.in, err, test.wantErr) + } + if !test.wantErr { // Only check content if unmarshal was expected to succeed + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("json.Unmarshal(%q) mismatch (-want +got):\n%s", test.in, diff) + } + } + }) + } +} +func TestCompleteParams(t *testing.T) { + // Define test cases specifically for Marshalling + marshalTests := []struct { + name string + in CompleteParams + want string // Expected JSON output + }{ + { + name: "BasicPromptCompletion", + in: CompleteParams{ + Ref: &CompleteReference{ + Type: "ref/prompt", + Name: "my_prompt", + }, + Argument: CompleteParamsArgument{ + Name: "language", + Value: "go", + }, + }, + want: `{"argument":{"name":"language","value":"go"},"ref":{"type":"ref/prompt","name":"my_prompt"}}`, + }, + { + name: "ResourceCompletionRequest", + in: CompleteParams{ + Ref: &CompleteReference{ + Type: "ref/resource", + URI: "file:///src/main.java", + }, + Argument: CompleteParamsArgument{ + Name: "class", + Value: "MyClas", + }, + }, + want: `{"argument":{"name":"class","value":"MyClas"},"ref":{"type":"ref/resource","uri":"file:///src/main.java"}}`, + }, + { + name: "PromptCompletionEmptyArgumentValue", + in: CompleteParams{ + Ref: &CompleteReference{ + Type: "ref/prompt", + Name: "another_prompt", + }, + Argument: CompleteParamsArgument{ + Name: "query", + Value: "", + }, + }, + want: `{"argument":{"name":"query","value":""},"ref":{"type":"ref/prompt","name":"another_prompt"}}`, + }, + { + name: "PromptCompletionWithContext", + in: CompleteParams{ + Ref: &CompleteReference{ + Type: "ref/prompt", + Name: "my_prompt", + }, + Argument: CompleteParamsArgument{ + Name: "language", + Value: "go", + }, + Context: &CompleteContext{ + Arguments: map[string]string{ + "framework": "mcp", + "language": "python", + }, + }, + }, + want: `{"argument":{"name":"language","value":"go"},"context":{"arguments":{"framework":"mcp","language":"python"}},"ref":{"type":"ref/prompt","name":"my_prompt"}}`, + }, + { + name: "PromptCompletionEmptyContextArguments", + in: CompleteParams{ + Ref: &CompleteReference{ + Type: "ref/prompt", + Name: "my_prompt", + }, + Argument: CompleteParamsArgument{ + Name: "language", + Value: "go", + }, + Context: &CompleteContext{ + Arguments: map[string]string{}, + }, + }, + want: `{"argument":{"name":"language","value":"go"},"context":{},"ref":{"type":"ref/prompt","name":"my_prompt"}}`, + }, + } + + // Define test cases specifically for Unmarshalling + unmarshalTests := []struct { + name string + in string // JSON string input + want CompleteParams // Expected Go struct output + }{ + { + name: "BasicPromptCompletion", + in: `{"argument":{"name":"language","value":"go"},"ref":{"type":"ref/prompt","name":"my_prompt"}}`, + want: CompleteParams{ + Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"}, + Argument: CompleteParamsArgument{Name: "language", Value: "go"}, + }, + }, + { + name: "ResourceCompletionRequest", + in: `{"argument":{"name":"class","value":"MyClas"},"ref":{"type":"ref/resource","uri":"file:///src/main.java"}}`, + want: CompleteParams{ + Ref: &CompleteReference{Type: "ref/resource", URI: "file:///src/main.java"}, + Argument: CompleteParamsArgument{Name: "class", Value: "MyClas"}, + }, + }, + { + name: "PromptCompletionWithContext", + in: `{"argument":{"name":"language","value":"go"},"context":{"arguments":{"framework":"mcp","language":"python"}},"ref":{"type":"ref/prompt","name":"my_prompt"}}`, + want: CompleteParams{ + Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"}, + Argument: CompleteParamsArgument{Name: "language", Value: "go"}, + Context: &CompleteContext{Arguments: map[string]string{ + "framework": "mcp", + "language": "python", + }}, + }, + }, + { + name: "PromptCompletionEmptyContextArguments", + in: `{"argument":{"name":"language","value":"go"},"context":{"arguments":{}},"ref":{"type":"ref/prompt","name":"my_prompt"}}`, + want: CompleteParams{ + Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"}, + Argument: CompleteParamsArgument{Name: "language", Value: "go"}, + Context: &CompleteContext{Arguments: map[string]string{}}, + }, + }, + { + name: "PromptCompletionNilContext", // JSON `null` for context + in: `{"argument":{"name":"language","value":"go"},"context":null,"ref":{"type":"ref/prompt","name":"my_prompt"}}`, + want: CompleteParams{ + Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"}, + Argument: CompleteParamsArgument{Name: "language", Value: "go"}, + Context: nil, // Should unmarshal to nil pointer + }, + }, + } + + // Run Marshal Tests + for _, test := range marshalTests { + t.Run("Marshal/"+test.name, func(t *testing.T) { + got, err := json.Marshal(&test.in) // Marshal takes a pointer + if err != nil { + t.Fatalf("json.Marshal(CompleteParams) failed: %v", err) + } + if diff := cmp.Diff(test.want, string(got)); diff != "" { + t.Errorf("CompleteParams marshal mismatch (-want +got):\n%s", diff) + } + }) + } + + // Run Unmarshal Tests + for _, test := range unmarshalTests { + t.Run("Unmarshal/"+test.name, func(t *testing.T) { + var got CompleteParams + if err := json.Unmarshal([]byte(test.in), &got); err != nil { + t.Fatalf("json.Unmarshal(CompleteParams) failed: %v", err) + } + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("CompleteParams unmarshal mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestCompleteResult(t *testing.T) { + // Define test cases specifically for Marshalling + marshalTests := []struct { + name string + in CompleteResult + want string // Expected JSON output + }{ + { + name: "BasicCompletionResult", + in: CompleteResult{ + Completion: CompletionResultDetails{ + Values: []string{"golang", "google", "goroutine"}, + Total: 10, + HasMore: true, + }, + }, + want: `{"completion":{"hasMore":true,"total":10,"values":["golang","google","goroutine"]}}`, + }, + { + name: "CompletionResultNoTotalNoHasMore", + in: CompleteResult{ + Completion: CompletionResultDetails{ + Values: []string{"only"}, + HasMore: false, + Total: 0, + }, + }, + want: `{"completion":{"values":["only"]}}`, + }, + { + name: "CompletionResultEmptyValues", + in: CompleteResult{ + Completion: CompletionResultDetails{ + Values: []string{}, + Total: 0, + HasMore: false, + }, + }, + want: `{"completion":{"values":[]}}`, + }, + } + + // Define test cases specifically for Unmarshalling + unmarshalTests := []struct { + name string + in string // JSON string input + want CompleteResult // Expected Go struct output + }{ + { + name: "BasicCompletionResult", + in: `{"completion":{"hasMore":true,"total":10,"values":["golang","google","goroutine"]}}`, + want: CompleteResult{ + Completion: CompletionResultDetails{ + Values: []string{"golang", "google", "goroutine"}, + Total: 10, + HasMore: true, + }, + }, + }, + { + name: "CompletionResultNoTotalNoHasMore", + in: `{"completion":{"values":["only"]}}`, + want: CompleteResult{ + Completion: CompletionResultDetails{ + Values: []string{"only"}, + HasMore: false, + Total: 0, + }, + }, + }, + { + name: "CompletionResultEmptyValues", + in: `{"completion":{"hasMore":false,"total":0,"values":[]}}`, + want: CompleteResult{ + Completion: CompletionResultDetails{ + Values: []string{}, + Total: 0, + HasMore: false, + }, + }, + }, + } + + // Run Marshal Tests + for _, test := range marshalTests { + t.Run("Marshal/"+test.name, func(t *testing.T) { + got, err := json.Marshal(&test.in) // Marshal takes a pointer + if err != nil { + t.Fatalf("json.Marshal(CompleteResult) failed: %v", err) + } + if diff := cmp.Diff(test.want, string(got)); diff != "" { + t.Errorf("CompleteResult marshal mismatch (-want +got):\n%s", diff) + } + }) + } + + // Run Unmarshal Tests + for _, test := range unmarshalTests { + t.Run("Unmarshal/"+test.name, func(t *testing.T) { + var got CompleteResult + if err := json.Unmarshal([]byte(test.in), &got); err != nil { + t.Fatalf("json.Unmarshal(CompleteResult) failed: %v", err) + } + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("CompleteResult unmarshal mismatch (-want +got):\n%s", diff) + } + }) + } +} + func TestContentUnmarshal(t *testing.T) { // Verify that types with a Content field round-trip properly. roundtrip := func(in, out any) { diff --git a/mcp/server.go b/mcp/server.go index 44ec7aa..2bc0c8b 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -58,6 +58,8 @@ type ServerOptions struct { RootsListChangedHandler func(context.Context, *ServerSession, *RootsListChangedParams) // If non-nil, called when "notifications/progress" is received. ProgressNotificationHandler func(context.Context, *ServerSession, *ProgressNotificationParams) + // If non-nil, called when "completion/complete" is received. + CompletionHandler func(context.Context, *ServerSession, *CompleteParams) (*CompleteResult, error) // If non-zero, defines an interval for regular "ping" requests. // If the peer fails to respond to pings originating from the keepalive check, // the session is automatically closed. @@ -231,6 +233,13 @@ func (s *Server) RemoveResourceTemplates(uriTemplates ...string) { func() bool { return s.resourceTemplates.remove(uriTemplates...) }) } +func (s *Server) complete(ctx context.Context, ss *ServerSession, params *CompleteParams) (Result, error) { + if s.opts.CompletionHandler == nil { + return nil, jsonrpc2.ErrMethodNotFound + } + return s.opts.CompletionHandler(ctx, ss, params) +} + // changeAndNotify is called when a feature is added or removed. // It calls change, which should do the work and report whether a change actually occurred. // If there was a change, it notifies a snapshot of the sessions. @@ -570,6 +579,7 @@ func (s *Server) AddReceivingMiddleware(middleware ...Middleware[*ServerSession] // serverMethodInfos maps from the RPC method name to serverMethodInfos. var serverMethodInfos = map[string]methodInfo{ + methodComplete: newMethodInfo(serverMethod((*Server).complete)), methodInitialize: newMethodInfo(sessionMethod((*ServerSession).initialize)), methodPing: newMethodInfo(sessionMethod((*ServerSession).ping)), methodListPrompts: newMethodInfo(serverMethod((*Server).listPrompts)), @@ -653,6 +663,7 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam // reject unsupported features. ProtocolVersion: version, Capabilities: &serverCapabilities{ + Completions: &completionCapabilities{}, Prompts: &promptCapabilities{ ListChanged: true, }, diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index d496f83..963d614 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -143,10 +143,11 @@ func TestStreamableServerTransport(t *testing.T) { initReq := req(1, "initialize", &InitializeParams{}) initResp := resp(1, &InitializeResult{ Capabilities: &serverCapabilities{ - Logging: &loggingCapabilities{}, - Prompts: &promptCapabilities{ListChanged: true}, - Resources: &resourceCapabilities{ListChanged: true}, - Tools: &toolCapabilities{ListChanged: true}, + Completions: &completionCapabilities{}, + Logging: &loggingCapabilities{}, + Prompts: &promptCapabilities{ListChanged: true}, + Resources: &resourceCapabilities{ListChanged: true}, + Tools: &toolCapabilities{ListChanged: true}, }, ProtocolVersion: "2025-03-26", ServerInfo: &implementation{Name: "testServer", Version: "v1.0.0"},