Skip to content

Commit 74f5edb

Browse files
authored
Server handlers implementation for auto-completion (#679)
* Define other types for completion * Define CompletionProvider and default implementation * Implement and generate handler for completion * Define tests for handleComplete * Follow interface segregation * Fix completions not reported to client * Refactor completion provider for context * Refactor "everything" example for completion * Add quick guide for auto completion in README * Simplify auto-completion README section * Prevent nil pointer dereference on mcp.Completion and return INVALID_REQUEST to match marshal error behavior
1 parent a1b6997 commit 74f5edb

File tree

11 files changed

+806
-24
lines changed

11 files changed

+806
-24
lines changed

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,114 @@ go generate ./...
778778
You need `go` installed and the `goimports` tool available. The generator runs
779779
`goimports` automatically to format and fix imports.
780780

781+
### Auto-completions
782+
783+
When users are filling in argument values for a specific prompt (identified by name) or resource template (identified by URI), servers can provide contextual suggestions.
784+
To enable completion support, use the `server.WithCompletions()` option when creating your server.
785+
786+
#### Completion Providers
787+
788+
You can provide completion logic for both prompt arguments and resource template arguments by implementing the respective interfaces and passing them to the server as options.
789+
790+
<details>
791+
<summary>Show Completion Provider Examples</summary>
792+
793+
```go
794+
type MyPromptCompletionProvider struct{}
795+
796+
func (p *MyPromptCompletionProvider) CompletePromptArgument(
797+
ctx context.Context,
798+
promptName string,
799+
argument mcp.CompleteArgument,
800+
context mcp.CompleteContext,
801+
) (*mcp.Completion, error) {
802+
// Example: provide style suggestions for a "code_review" prompt
803+
if promptName == "code_review" && argument.Name == "style" {
804+
styles := []string{"formal", "casual", "technical", "creative"}
805+
var suggestions []string
806+
807+
// Filter based on current input
808+
for _, style := range styles {
809+
if strings.HasPrefix(style, argument.Value) {
810+
suggestions = append(suggestions, style)
811+
}
812+
}
813+
814+
return &mcp.Completion{
815+
Values: suggestions,
816+
}, nil
817+
}
818+
819+
// Return empty suggestions for unhandled cases
820+
return &mcp.Completion{Values: []string{}}, nil
821+
}
822+
823+
type MyResourceCompletionProvider struct{}
824+
825+
func (p *MyResourceCompletionProvider) CompleteResourceArgument(
826+
ctx context.Context,
827+
uri string,
828+
argument mcp.CompleteArgument,
829+
context mcp.CompleteContext,
830+
) (*mcp.Completion, error) {
831+
// Example: provide file path completions
832+
if uri == "file:///{path}" && argument.Name == "path" {
833+
// You can access previously completed arguments from context.Arguments
834+
// context.Arguments is a map[string]string of already-resolved arguments
835+
836+
paths := getMatchingPaths(argument.Value) // Your custom logic
837+
838+
return &mcp.Completion{
839+
Values: paths[:min(len(paths), 100)], // Max 100 items
840+
Total: len(paths), // Total available matches
841+
HasMore: len(paths) > 100, // More results available
842+
}, nil
843+
}
844+
845+
return &mcp.Completion{Values: []string{}}, nil
846+
}
847+
848+
// Register the provider
849+
mcpServer := server.NewMCPServer(
850+
"my-server",
851+
"1.0.0",
852+
server.WithCompletions(),
853+
server.WithPromptCompletionProvider(&MyPromptCompletionProvider{}),
854+
server.WithResourceCompletionProvider(&MyResourceCompletionProvider{}),
855+
)
856+
```
857+
858+
</details>
859+
860+
#### Completion Context
861+
862+
For prompts or resource templates with multiple arguments, the `CompleteContext` parameter provides access to previously completed arguments. This allows you to provide contextual suggestions based on earlier choices.
863+
864+
<details>
865+
<summary>Show Completion Context Example</summary>
866+
867+
```go
868+
func (p *MyProvider) CompleteResourceArgument(
869+
ctx context.Context,
870+
uri string,
871+
argument mcp.CompleteArgument,
872+
context mcp.CompleteContext,
873+
) (*mcp.Completion, error) {
874+
// Access previously completed arguments
875+
if previousValue, ok := context.Arguments["previous_arg"]; ok {
876+
// Provide suggestions based on previous_arg value
877+
return getSuggestionsFor(argument.Value, previousValue), nil
878+
}
879+
880+
return &mcp.Completion{Values: []string{}}, nil
881+
}
882+
```
883+
884+
</details>
885+
886+
#### Response Constraints
887+
888+
When returning completion results:
889+
- Maximum 100 items per response
890+
- Use `Total` to indicate the total number of available matches
891+
- Use `HasMore` to signal if additional results exist beyond the returned values

examples/everything/id.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import "strconv"
4+
5+
func pow(n int, p int) int {
6+
result := 1
7+
for p > 0 {
8+
if (p & 1) == 1 {
9+
result *= n
10+
}
11+
n *= n
12+
p >>= 1
13+
}
14+
return result
15+
}
16+
17+
// n < 10000 is 4 digits
18+
var maxDigits = 4
19+
var maxId = pow(10, maxDigits)
20+
21+
func isIdInRange(num int) bool {
22+
return num >= 0 && num < maxId
23+
}
24+
25+
// getIdSuggestions returns 10 suggestions for a given number.
26+
// It returns the current argument value appended with 0-9.
27+
func getIdSuggestions(num int) []string {
28+
suggestions := make([]string, 0, 10)
29+
for i := range 10 {
30+
suggestions = append(suggestions, strconv.Itoa(num*10+i))
31+
}
32+
return suggestions
33+
}
34+
35+
func getTotalIds(digits int) int {
36+
// Leftover digits represent the total number of choices.
37+
return pow(10, maxDigits-digits)
38+
}
39+
40+
func hasMoreIds(digits int) bool {
41+
return digits < maxDigits-1
42+
}

examples/everything/main.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"log"
99
"strconv"
10+
"strings"
1011
"time"
1112

1213
"github.com/mark3labs/mcp-go/mcp"
@@ -69,6 +70,9 @@ func NewMCPServer() *server.MCPServer {
6970
server.WithToolCapabilities(true),
7071
server.WithLogging(),
7172
server.WithHooks(hooks),
73+
server.WithCompletions(),
74+
server.WithPromptCompletionProvider(&promptCompletionProvider{}),
75+
server.WithResourceCompletionProvider(&resourceCompletionProvider{}),
7276
)
7377

7478
mcpServer.AddResource(mcp.NewResource("test://static/resource",
@@ -501,13 +505,80 @@ func handleGetTinyImageTool(
501505
}, nil
502506
}
503507

508+
var styles = []string{"formal", "casual", "technical", "creative"}
509+
504510
func handleNotification(
505511
ctx context.Context,
506512
notification mcp.JSONRPCNotification,
507513
) {
508514
log.Printf("Received notification: %s", notification.Method)
509515
}
510516

517+
type promptCompletionProvider struct{}
518+
519+
func (p *promptCompletionProvider) CompletePromptArgument(ctx context.Context, promptName string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error) {
520+
switch promptName {
521+
case string(COMPLEX):
522+
if argument.Name == "style" {
523+
var suggestions []string
524+
for _, style := range styles {
525+
if argument.Value == "" || strings.HasPrefix(style, argument.Value) {
526+
suggestions = append(suggestions, style)
527+
}
528+
}
529+
return &mcp.Completion{
530+
Values: suggestions,
531+
}, nil
532+
}
533+
fallthrough
534+
default:
535+
return &mcp.Completion{
536+
Values: []string{},
537+
}, nil
538+
}
539+
}
540+
541+
type resourceCompletionProvider struct{}
542+
543+
// CompleteResourceArgument here is implemented to return suggestions for "test://dynamic/resource/{id}" template, specifically for the "id" argument.
544+
func (p *resourceCompletionProvider) CompleteResourceArgument(ctx context.Context, uri string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error) {
545+
if uri == "test://dynamic/resource/{id}" && argument.Name == "id" {
546+
if len(argument.Value) == 0 {
547+
return &mcp.Completion{
548+
Values: getIdSuggestions(0),
549+
Total: maxId,
550+
HasMore: true,
551+
}, nil
552+
}
553+
554+
// If the input is not a number, return empty suggestion.
555+
intArgVal, err := strconv.Atoi(argument.Value)
556+
if err != nil {
557+
return &mcp.Completion{
558+
Values: []string{},
559+
}, nil
560+
}
561+
562+
digits := len(argument.Value)
563+
564+
// If the input is out of range, return empty suggestion.
565+
if !isIdInRange(intArgVal) {
566+
return &mcp.Completion{
567+
Values: []string{},
568+
}, nil
569+
}
570+
571+
return &mcp.Completion{
572+
Values: getIdSuggestions(intArgVal),
573+
Total: getTotalIds(digits),
574+
HasMore: hasMoreIds(digits),
575+
}, nil
576+
}
577+
return &mcp.Completion{
578+
Values: []string{},
579+
}, nil
580+
}
581+
511582
func main() {
512583
var transport string
513584
flag.StringVar(&transport, "t", "stdio", "Transport type (stdio or http)")

mcp/types.go

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ const (
103103
// MethodNotificationTasksStatus notifies when a task's status changes.
104104
// https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks
105105
MethodNotificationTasksStatus = "notifications/tasks/status"
106+
107+
// MethodCompletionComplete returns completion suggestions for a given argument
108+
// https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion
109+
MethodCompletionComplete MCPMethod = "completion/complete"
106110
)
107111

108112
type URITemplate struct {
@@ -556,6 +560,8 @@ type ServerCapabilities struct {
556560
Roots *struct{} `json:"roots,omitempty"`
557561
// Present if the server supports task-based execution.
558562
Tasks *TasksCapability `json:"tasks,omitempty"`
563+
// Present if the server supports completions requests to the client.
564+
Completions *struct{} `json:"completions,omitempty"`
559565
}
560566

561567
// Icon represents a visual identifier for MCP entities.
@@ -1202,29 +1208,81 @@ type CompleteRequest struct {
12021208
Header http.Header `json:"-"`
12031209
}
12041210

1211+
// CompleteParams are the parameters for a completion/complete request
12051212
type CompleteParams struct {
1206-
Ref any `json:"ref"` // Can be PromptReference or ResourceReference
1207-
Argument struct {
1208-
// The name of the argument
1209-
Name string `json:"name"`
1210-
// The value of the argument to use for completion matching.
1211-
Value string `json:"value"`
1212-
} `json:"argument"`
1213+
Ref any `json:"ref"` // Can be PromptReference or ResourceReference
1214+
Argument CompleteArgument `json:"argument"`
1215+
Context CompleteContext `json:"context"`
1216+
}
1217+
1218+
func (p *CompleteParams) UnmarshalJSON(data []byte) error {
1219+
// Use a temporary type to avoid infinite recursion on UnmarshalJSON
1220+
type Alias CompleteParams
1221+
aux := &struct {
1222+
// Use RawMessage to delay unmarshalling until after the type is known
1223+
Ref json.RawMessage `json:"ref"`
1224+
*Alias
1225+
}{
1226+
Alias: (*Alias)(p),
1227+
}
1228+
if err := json.Unmarshal(data, aux); err != nil {
1229+
return err
1230+
}
1231+
// Use a temporary "type peek" struct to determine the type
1232+
var typePeek struct {
1233+
Type string `json:"type"`
1234+
}
1235+
if err := json.Unmarshal(aux.Ref, &typePeek); err != nil {
1236+
return err
1237+
}
1238+
switch typePeek.Type {
1239+
case "ref/prompt":
1240+
var prompt PromptReference
1241+
if err := json.Unmarshal(aux.Ref, &prompt); err != nil {
1242+
return err
1243+
}
1244+
p.Ref = prompt
1245+
case "ref/resource":
1246+
var resource ResourceReference
1247+
if err := json.Unmarshal(aux.Ref, &resource); err != nil {
1248+
return err
1249+
}
1250+
p.Ref = resource
1251+
default:
1252+
return fmt.Errorf("unknown reference type: %s", typePeek.Type)
1253+
}
1254+
return nil
12131255
}
12141256

12151257
// CompleteResult is the server's response to a completion/complete request
12161258
type CompleteResult struct {
12171259
Result
1218-
Completion struct {
1219-
// An array of completion values. Must not exceed 100 items.
1220-
Values []string `json:"values"`
1221-
// The total number of completion options available. This can exceed the
1222-
// number of values actually sent in the response.
1223-
Total int `json:"total,omitempty"`
1224-
// Indicates whether there are additional completion options beyond those
1225-
// provided in the current response, even if the exact total is unknown.
1226-
HasMore bool `json:"hasMore,omitempty"`
1227-
} `json:"completion"`
1260+
Completion Completion `json:"completion"`
1261+
}
1262+
1263+
// CompleteArgument is an argument to a completion request
1264+
type CompleteArgument struct {
1265+
// The name of the argument
1266+
Name string `json:"name"`
1267+
// The value of the argument to use for completion matching.
1268+
Value string `json:"value"`
1269+
}
1270+
1271+
// CompleteContext is the context about already-resolved arguments
1272+
type CompleteContext struct {
1273+
Arguments map[string]string `json:"arguments"`
1274+
}
1275+
1276+
// Completion is the server's response to a completion/complete request
1277+
type Completion struct {
1278+
// An array of completion values. Must not exceed 100 items.
1279+
Values []string `json:"values"`
1280+
// The total number of completion options available. This can exceed the
1281+
// number of values actually sent in the response.
1282+
Total int `json:"total,omitempty"`
1283+
// Indicates whether there are additional completion options beyond those
1284+
// provided in the current response, even if the exact total is unknown.
1285+
HasMore bool `json:"hasMore,omitempty"`
12281286
}
12291287

12301288
// ResourceReference is a reference to a resource or resource template definition.

0 commit comments

Comments
 (0)