Skip to content

Commit 037c8c8

Browse files
committed
feat: Support completion handling for prompts and resources
- Added completionCompleteHandler to registeredPrompt and registeredResource for handling completion requests. - Refactored handleCompletionComplete methods in promptManager and resourceManager to utilize new completion handlers. - Added utility functions for extracting parameters from JSON-RPC requests. - Updated lifecycleManager to check for completion availability in both prompt and resource managers. - Implemented tests for completion handling in both promptManager and resourceManager, ensuring correct behavior for valid and invalid requests.
1 parent 905a8ca commit 037c8c8

16 files changed

Lines changed: 1155 additions & 39 deletions

client.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type Connector interface {
5353
ListPrompts(ctx context.Context, req *ListPromptsRequest) (*ListPromptsResult, error)
5454
// GetPrompt retrieves a specific prompt by name.
5555
GetPrompt(ctx context.Context, req *GetPromptRequest) (*GetPromptResult, error)
56+
// CompleteCompletion provides parameter auto-completion suggestions for prompts and resource URIs.
57+
CompleteCompletion(ctx context.Context, req *CompleteCompletionRequest) (JSONRPCMessage, error)
5658
// ListResources retrieves all available resources from the server.
5759
ListResources(ctx context.Context, req *ListResourcesRequest) (*ListResourcesResult, error)
5860
// ReadResource reads the content of a specific resource.
@@ -548,6 +550,42 @@ func (c *Client) GetPrompt(ctx context.Context, getPromptReq *GetPromptRequest)
548550
return parseGetPromptResultFromJSON(rawResp)
549551
}
550552

553+
// CompleteCompletion provides parameter auto-completion suggestions for prompts and resource URIs.
554+
func (c *Client) CompleteCompletion(ctx context.Context, completeReq *CompleteCompletionRequest) (*CompleteCompletionResult, error) {
555+
// Check if initialized.
556+
if !c.initialized {
557+
return nil, errors.ErrNotInitialized
558+
}
559+
560+
// Create request.
561+
requestID := c.requestID.Add(1)
562+
req := &JSONRPCRequest{
563+
JSONRPC: JSONRPCVersion,
564+
ID: requestID,
565+
Request: Request{
566+
Method: MethodCompletionComplete,
567+
},
568+
Params: completeReq.Params,
569+
}
570+
571+
rawResp, err := c.transport.sendRequest(ctx, req)
572+
if err != nil {
573+
return nil, fmt.Errorf("complete completion request failed: %v", err)
574+
}
575+
576+
// Check for error response
577+
if isErrorResponse(rawResp) {
578+
errResp, err := parseRawMessageToError(rawResp)
579+
if err != nil {
580+
return nil, fmt.Errorf("failed to parse error response: %w", err)
581+
}
582+
return nil, fmt.Errorf("complete completion error: %s (code: %d)",
583+
errResp.Error.Message, errResp.Error.Code)
584+
}
585+
586+
return parseCompleteCompletionResultFromJSON(rawResp)
587+
}
588+
551589
// ListResources lists available resources.
552590
func (c *Client) ListResources(ctx context.Context, listResourcesReq *ListResourcesRequest) (*ListResourcesResult, error) {
553591
// Check if initialized.

examples/resource_prompt_example/client/main.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package main
88

99
import (
1010
"context"
11+
"encoding/json"
1112
"fmt"
1213
"os"
1314

@@ -38,6 +39,10 @@ func initializeClient(ctx context.Context) (*mcp.Client, error) {
3839
}
3940
log.Printf("Connected to server: %s %s", initResult.ServerInfo.Name, initResult.ServerInfo.Version)
4041

42+
// Check server capabilities
43+
capabilitiesJSON, _ := json.Marshal(initResult.Capabilities)
44+
log.Printf("Server capabilities:%v", string(capabilitiesJSON))
45+
4146
return newClient, nil
4247
}
4348

@@ -173,6 +178,77 @@ func handlePrompts(ctx context.Context, client *mcp.Client) error {
173178
return nil
174179
}
175180

181+
// handleCompletions demonstrates completion functionality for prompts and resources
182+
func handleCompletions(ctx context.Context, client *mcp.Client) error {
183+
log.Printf("===== Prompt completion =====")
184+
185+
// Test completion for prompt arguments (code_review -> language)
186+
promptCompletionReq := &mcp.CompleteCompletionRequest{}
187+
promptCompletionReq.Params.Ref.Type = "ref/prompt"
188+
promptCompletionReq.Params.Ref.Name = "code_review"
189+
promptCompletionReq.Params.Argument.Name = "language"
190+
promptCompletionReq.Params.Argument.Value = "p"
191+
192+
promptCompletionResult, err := client.CompleteCompletion(ctx, promptCompletionReq)
193+
if err != nil {
194+
log.Printf("Prompt completion error: %v", err)
195+
} else {
196+
log.Printf("Prompt completion for '%s' with prefix '%s': found %d suggestions",
197+
promptCompletionReq.Params.Ref.Name,
198+
promptCompletionReq.Params.Argument.Value,
199+
len(promptCompletionResult.Completion.Values))
200+
for i, value := range promptCompletionResult.Completion.Values {
201+
log.Printf(" [%d] %s", i, value)
202+
}
203+
}
204+
205+
log.Printf("===== Resource completion =====")
206+
// Test completion for resource (resource://example/completion)
207+
resourceCompletionReq := &mcp.CompleteCompletionRequest{}
208+
resourceCompletionReq.Params.Ref.Type = "ref/resource"
209+
resourceCompletionReq.Params.Ref.URI = "resource://example/completion"
210+
resourceCompletionReq.Params.Argument.Name = "query"
211+
resourceCompletionReq.Params.Argument.Value = ""
212+
213+
resourceCompletionResult, err := client.CompleteCompletion(ctx, resourceCompletionReq)
214+
if err != nil {
215+
log.Printf("Resource completion error: %v", err)
216+
} else {
217+
log.Printf("Resource completion from '%s' for '%s': found %d suggestions",
218+
resourceCompletionReq.Params.Ref.URI,
219+
resourceCompletionReq.Params.Argument.Name,
220+
len(resourceCompletionResult.Completion.Values))
221+
for i, value := range resourceCompletionResult.Completion.Values {
222+
log.Printf(" [%d] %s", i, value)
223+
}
224+
}
225+
226+
log.Printf("===== Resource template completion =====")
227+
228+
// Test completion for resource template (file-template -> path)
229+
resourceTemplateCompletionReq := &mcp.CompleteCompletionRequest{}
230+
resourceTemplateCompletionReq.Params.Ref.Type = "ref/resource"
231+
resourceTemplateCompletionReq.Params.Ref.URI = "file://test"
232+
resourceTemplateCompletionReq.Params.Argument.Name = "keyword"
233+
resourceTemplateCompletionReq.Params.Argument.Value = "completion"
234+
235+
resourceTemplateCompletionResult, err := client.CompleteCompletion(ctx, resourceTemplateCompletionReq)
236+
if err != nil {
237+
log.Printf("Resource template completion error: %v", err)
238+
} else {
239+
log.Printf("Resource template completion from '%s' for '%s' with keyword '%s': found %d suggestions",
240+
resourceTemplateCompletionReq.Params.Ref.URI,
241+
resourceTemplateCompletionReq.Params.Argument.Name,
242+
resourceTemplateCompletionReq.Params.Argument.Value,
243+
len(resourceTemplateCompletionResult.Completion.Values))
244+
for i, value := range resourceTemplateCompletionResult.Completion.Values {
245+
log.Printf(" [%d] %s", i, value)
246+
}
247+
}
248+
249+
return nil
250+
}
251+
176252
// handleTools manages tool-related operations including listing and calling tools
177253
func handleTools(ctx context.Context, client *mcp.Client) error {
178254
log.Printf("===== List tools =====")
@@ -223,6 +299,8 @@ func main() {
223299
}
224300
defer client.Close()
225301

302+
//
303+
226304
// Handle resources
227305
if err := handleResources(ctx, client); err != nil {
228306
log.Printf("Error: %v", err)
@@ -233,6 +311,11 @@ func main() {
233311
log.Printf("Error: %v", err)
234312
}
235313

314+
// Handle completions
315+
if err := handleCompletions(ctx, client); err != nil {
316+
log.Printf("Error: %v", err)
317+
}
318+
236319
// Handle tools
237320
if err := handleTools(ctx, client); err != nil {
238321
log.Printf("Error: %v", err)

examples/resource_prompt_example/server/main.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,77 @@ func registerExampleResources(s *mcp.Server) {
8383

8484
s.RegisterResource(imageResource, imageHandler)
8585
log.Printf("Registered image resource: %s", imageResource.Name)
86+
87+
// Register resource completion.
88+
textResourceCompletion := &mcp.Resource{
89+
URI: "resource://example/completion",
90+
Name: "example-text-completion",
91+
Description: "Example text resource with completion",
92+
MimeType: "text/plain",
93+
}
94+
95+
// Define completion resource handler
96+
resourceCompletionHandler := func(ctx context.Context, req *mcp.CompleteCompletionRequest) (*mcp.CompleteCompletionResult, error) {
97+
result := &mcp.CompleteCompletionResult{}
98+
if req.Params.Argument.Name == "query" {
99+
if req.Params.Context.Arguments != nil {
100+
// Use context argument if provided
101+
context, ok := req.Params.Context.Arguments["context"]
102+
if ok && context != "" {
103+
result.Completion.Values = []string{fmt.Sprintf("Context: %s", context)}
104+
} else {
105+
result.Completion.Values = []string{"Context is empty"}
106+
}
107+
} else {
108+
result.Completion.Values = []string{"First document text", "Second document text", "Third document text"}
109+
}
110+
} else {
111+
result.Completion.Values = []string{"Unknown argument"}
112+
}
113+
result.Completion.Total = len(result.Completion.Values)
114+
result.Completion.HasMore = false
115+
return result, nil
116+
}
117+
s.RegisterResource(textResourceCompletion, nil,
118+
mcp.WithResourceCompletion(resourceCompletionHandler),
119+
)
120+
log.Printf("Registered text resource completion: %s", textResourceCompletion.Name)
121+
122+
// Register resource template with completion
123+
fileTemplate := mcp.NewResourceTemplate(
124+
"file://{path}",
125+
"example-file-template-completion",
126+
mcp.WithTemplateDescription("Example file resource template with completion"),
127+
mcp.WithTemplateMIMEType("text/plain"),
128+
)
129+
130+
// Define completion handler for file paths
131+
fileCompletionHandler := func(ctx context.Context, req *mcp.CompleteCompletionRequest, params map[string]string) (*mcp.CompleteCompletionResult, error) {
132+
result := &mcp.CompleteCompletionResult{}
133+
if req.Params.Argument.Name == "keyword" {
134+
keyword := req.Params.Argument.Value
135+
result.Completion.Values = []string{
136+
fmt.Sprint("file://", params["path"], "_1?keyword=", keyword),
137+
fmt.Sprint("file://", params["path"], "_2?keyword=", keyword),
138+
fmt.Sprint("file://", params["path"], "_3?keyword=", keyword),
139+
}
140+
} else {
141+
result.Completion.Values = []string{"unknown argument"}
142+
}
143+
144+
total := len(result.Completion.Values)
145+
result.Completion.Total = total
146+
result.Completion.HasMore = false
147+
148+
return result, nil
149+
}
150+
151+
s.RegisterResourceTemplate(
152+
fileTemplate,
153+
nil,
154+
mcp.WithTemplateCompletion(fileCompletionHandler),
155+
)
156+
log.Printf("Registered file resource template completion: %s", fileTemplate.Name)
86157
}
87158

88159
// Register example prompts.
@@ -162,6 +233,73 @@ func registerExamplePrompts(s *mcp.Server) {
162233

163234
s.RegisterPrompt(advancedPrompt, advancedPromptHandler)
164235
log.Printf("Registered advanced prompt: %s", advancedPrompt.Name)
236+
237+
// Register prompt with completion support
238+
codeReviewPrompt := &mcp.Prompt{
239+
Name: "code_review",
240+
Description: "Code review prompt completion",
241+
Arguments: []mcp.PromptArgument{
242+
{
243+
Name: "language",
244+
Description: "Programming language of the code",
245+
Required: true,
246+
},
247+
},
248+
}
249+
250+
// Define completion prompt handler
251+
codeReviewPromptHandler := func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
252+
return &mcp.GetPromptResult{
253+
Messages: []mcp.PromptMessage{
254+
{
255+
Role: "user",
256+
Content: mcp.TextContent{
257+
Type: "text",
258+
Text: fmt.Sprintf("Please review the following %s code:\n%s", req.Params.Arguments["language"], req.Params.Arguments["code"]),
259+
},
260+
},
261+
},
262+
}, nil
263+
}
264+
265+
// Define completion handler for prompt arguments
266+
codeReviewPromptCompletionHandler := func(ctx context.Context, req *mcp.CompleteCompletionRequest) (*mcp.CompleteCompletionResult, error) {
267+
result := &mcp.CompleteCompletionResult{}
268+
if req.Params.Argument.Name == "language" {
269+
prefix := req.Params.Argument.Value
270+
categories := []string{
271+
"python", "pytorch", "javascript", "typescript",
272+
"go", "java", "c++", "c#", "ruby", "php",
273+
}
274+
275+
var matches []string
276+
for _, cat := range categories {
277+
if prefix == "" || len(prefix) == 0 {
278+
matches = append(matches, cat)
279+
} else if len(cat) >= len(prefix) && cat[:len(prefix)] == prefix {
280+
matches = append(matches, cat)
281+
}
282+
}
283+
284+
// Limit to first 10 matches
285+
if len(matches) > 10 {
286+
matches = matches[:10]
287+
}
288+
289+
result.Completion.Values = matches
290+
} else {
291+
result.Completion.Values = []string{"unknown argument"}
292+
}
293+
294+
total := len(result.Completion.Values)
295+
result.Completion.Total = total
296+
result.Completion.HasMore = false
297+
298+
return result, nil
299+
}
300+
301+
s.RegisterPrompt(codeReviewPrompt, codeReviewPromptHandler, mcp.WithPromptCompletion(codeReviewPromptCompletionHandler))
302+
log.Printf("Registered prompt completion: %s", codeReviewPrompt.Name)
165303
}
166304

167305
// Register example tools.

handler.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ package mcp
88

99
import (
1010
"context"
11+
12+
"trpc.group/trpc-go/trpc-mcp-go/internal/errors"
13+
"trpc.group/trpc-go/trpc-mcp-go/internal/utils"
1114
)
1215

1316
const (
@@ -199,7 +202,18 @@ func (h *mcpHandler) handlePromptsGet(ctx context.Context, req *JSONRPCRequest,
199202
}
200203

201204
func (h *mcpHandler) handleCompletionComplete(ctx context.Context, req *JSONRPCRequest, session Session) (JSONRPCMessage, error) {
202-
return h.promptManager.handleCompletionComplete(ctx, req)
205+
ref := utils.ExtractMap(req.Params.(map[string]interface{}), "ref")
206+
if ref == nil {
207+
return newJSONRPCErrorResponse(req.ID, ErrCodeInvalidParams, errors.ErrMissingParams.Error(), nil), nil
208+
}
209+
switch utils.ExtractString(ref, "type") {
210+
case "ref/prompt":
211+
return h.promptManager.handleCompletionComplete(ctx, req)
212+
case "ref/resource":
213+
return h.resourceManager.handleCompletionComplete(ctx, req)
214+
default:
215+
return newJSONRPCErrorResponse(req.ID, ErrCodeInvalidParams, errors.ErrInvalidParams.Error(), nil), nil
216+
}
203217
}
204218

205219
// handleNotification implements the handler interface's handleNotification method

internal/errors/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var (
1616
ErrInvalidToolFormat = errors.New("invalid tool format")
1717
ErrToolNotFound = errors.New("tool not found")
1818
ErrInvalidToolParams = errors.New("invalid tool parameters")
19+
ErrMethodNotFound = errors.New("method not found")
1920

2021
// JSON-RPC related errors
2122
ErrParseJSONRPC = errors.New("failed to parse JSON-RPC message")

manager_lifecycle.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,21 @@ func (m *lifecycleManager) updateCapabilities() {
127127
capMap["resources"] = map[string]interface{}{
128128
"listChanged": true,
129129
}
130+
131+
if m.resourceManager.hasCompletionCompleteHandler() {
132+
capMap["completions"] = map[string]interface{}{}
133+
}
130134
}
131135

132136
// If there is a prompt manager and prompts are registered, add prompt capabilities
133137
if m.promptManager != nil && len(m.promptManager.getPrompts()) > 0 {
134138
capMap["prompts"] = map[string]interface{}{
135139
"listChanged": true,
136140
}
141+
142+
if m.promptManager.hasCompletionCompleteHandler() {
143+
capMap["completions"] = map[string]interface{}{}
144+
}
137145
}
138146

139147
// Preserve existing experimental features

0 commit comments

Comments
 (0)