mcp: implement completion (#55)
This CL implements a completion handler for the server and a complete method for the client. Fixes: #12 <!-- Provide a brief summary of your changes --> ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> This adds support for completion. ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> Added tests and an example. ## Breaking Changes <!-- Will users need to update their code or configurations? --> N/A ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] 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 <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [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 <!-- Add any other context, implementation notes, or design decisions -->
This commit is contained in:
parent
92b6983c1c
commit
e1b1cd0072
6 changed files with 579 additions and 5 deletions
49
examples/completion/main.go
Normal file
49
examples/completion/main.go
Normal file
|
|
@ -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.")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
Loading…
Reference in a new issue