Skip to content

Commit 4cbc301

Browse files
authored
Update MCP parser to support latest specification (#1993)
* Update MCP parser to support latest specification Add support for new MCP methods from the 2025-06-18 specification: - elicitation/create for user input requests - sampling/createMessage for message creation - resources/subscribe and resources/unsubscribe for resource subscriptions - resources/templates/list for template listing - roots/list for listing roots - notifications/progress for progress notifications - notifications/cancelled for cancellation notifications - Additional notification methods for list changes Update completion/complete handler to properly handle both PromptReference and ResourceTemplateReference types as specified in the latest schema. Add comprehensive test coverage for all new methods to ensure proper extraction of resource IDs and arguments. * test: add comprehensive test coverage for MCP parser - Added tests for all new MCP methods from 2025-06-18 specification - Added edge case tests for error handling and malformed inputs - Added tests for JSON-RPC notifications (messages without ID) - Improved test coverage from ~66% to 91.7% for pkg/mcp - All parser functions now have 83-100% coverage * Address PR feedback Signed-off-by: Juan Antonio Osorio <[email protected]> --------- Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent bc1d063 commit 4cbc301

File tree

2 files changed

+859
-18
lines changed

2 files changed

+859
-18
lines changed

pkg/mcp/parser.go

Lines changed: 138 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"io"
99
"net/http"
10+
"strconv"
1011
"strings"
1112

1213
"golang.org/x/exp/jsonrpc2"
@@ -135,18 +136,25 @@ func parseMCPRequest(bodyBytes []byte) *ParsedMCPRequest {
135136
return nil
136137
}
137138

138-
// Handle only request messages
139+
// Handle only request messages (both calls with ID and notifications without ID)
139140
req, ok := msg.(*jsonrpc2.Request)
140141
if !ok {
142+
// Response or error messages are not parsed here
141143
return nil
142144
}
143145

144146
// Extract resource ID and arguments based on the method
145147
resourceID, arguments := extractResourceAndArguments(req.Method, req.Params)
146148

149+
// Determine the ID - will be nil for notifications
150+
var id interface{}
151+
if req.ID.IsValid() {
152+
id = req.ID.Raw()
153+
}
154+
147155
return &ParsedMCPRequest{
148156
Method: req.Method,
149-
ID: req.ID.Raw(),
157+
ID: id,
150158
Params: req.Params,
151159
ResourceID: resourceID,
152160
Arguments: arguments,
@@ -162,24 +170,36 @@ type methodHandler func(map[string]interface{}) (string, map[string]interface{})
162170

163171
// methodHandlers maps MCP methods to their respective handlers
164172
var methodHandlers = map[string]methodHandler{
165-
"initialize": handleInitializeMethod,
166-
"tools/call": handleNamedResourceMethod,
167-
"prompts/get": handleNamedResourceMethod,
168-
"resources/read": handleResourceReadMethod,
169-
"resources/list": handleListMethod,
170-
"tools/list": handleListMethod,
171-
"prompts/list": handleListMethod,
172-
"progress/update": handleProgressMethod,
173-
"notifications/message": handleNotificationMethod,
174-
"logging/setLevel": handleLoggingMethod,
175-
"completion/complete": handleCompletionMethod,
173+
"initialize": handleInitializeMethod,
174+
"tools/call": handleNamedResourceMethod,
175+
"prompts/get": handleNamedResourceMethod,
176+
"resources/read": handleResourceReadMethod,
177+
"resources/list": handleListMethod,
178+
"tools/list": handleListMethod,
179+
"prompts/list": handleListMethod,
180+
"progress/update": handleProgressMethod,
181+
"notifications/message": handleNotificationMethod,
182+
"logging/setLevel": handleLoggingMethod,
183+
"completion/complete": handleCompletionMethod,
184+
"elicitation/create": handleElicitationMethod,
185+
"sampling/createMessage": handleSamplingMethod,
186+
"resources/subscribe": handleResourceSubscribeMethod,
187+
"resources/unsubscribe": handleResourceUnsubscribeMethod,
188+
"resources/templates/list": handleListMethod,
189+
"roots/list": handleListMethod,
190+
"notifications/progress": handleProgressNotificationMethod,
191+
"notifications/cancelled": handleCancelledNotificationMethod,
176192
}
177193

178194
// staticResourceIDs maps methods to their static resource IDs
179195
var staticResourceIDs = map[string]string{
180-
"ping": "ping",
181-
"notifications/roots/list_changed": "roots",
182-
"notifications/initialized": "initialized",
196+
"ping": "ping",
197+
"notifications/roots/list_changed": "roots",
198+
"notifications/initialized": "initialized",
199+
"notifications/prompts/list_changed": "prompts",
200+
"notifications/resources/list_changed": "resources",
201+
"notifications/resources/updated": "resources",
202+
"notifications/tools/list_changed": "tools",
183203
}
184204

185205
func extractResourceAndArguments(method string, params json.RawMessage) (string, map[string]interface{}) {
@@ -277,14 +297,114 @@ func handleLoggingMethod(paramsMap map[string]interface{}) (string, map[string]i
277297
return "", nil
278298
}
279299

280-
// handleCompletionMethod extracts resource ID for completion requests
300+
// handleCompletionMethod extracts resource ID for completion requests.
301+
// For PromptReference: extracts the prompt name
302+
// For ResourceTemplateReference: extracts the template URI
303+
// For legacy string ref: returns the string value
304+
// Always returns paramsMap as arguments since completion requests need the full context
305+
// including the argument being completed and any context from previous completions.
281306
func handleCompletionMethod(paramsMap map[string]interface{}) (string, map[string]interface{}) {
307+
// Check if ref is a map (PromptReference or ResourceTemplateReference)
308+
if ref, ok := paramsMap["ref"].(map[string]interface{}); ok {
309+
// Try to extract name for PromptReference
310+
if name, ok := ref["name"].(string); ok {
311+
return name, paramsMap
312+
}
313+
// Try to extract uri for ResourceTemplateReference
314+
if uri, ok := ref["uri"].(string); ok {
315+
return uri, paramsMap
316+
}
317+
}
318+
// Fallback to string ref (legacy support)
282319
if ref, ok := paramsMap["ref"].(string); ok {
283-
return ref, nil
320+
return ref, paramsMap
321+
}
322+
return "", paramsMap
323+
}
324+
325+
// handleElicitationMethod extracts resource ID for elicitation requests
326+
func handleElicitationMethod(paramsMap map[string]interface{}) (string, map[string]interface{}) {
327+
// The message field could be used as a resource identifier
328+
if message, ok := paramsMap["message"].(string); ok {
329+
return message, paramsMap
330+
}
331+
return "", paramsMap
332+
}
333+
334+
// handleSamplingMethod extracts resource ID for sampling/createMessage requests.
335+
// Returns the model name from modelPreferences if available, otherwise returns a
336+
// truncated version of the systemPrompt. The 50-character truncation provides a
337+
// reasonable balance between uniqueness and readability for authorization and audit logs.
338+
func handleSamplingMethod(paramsMap map[string]interface{}) (string, map[string]interface{}) {
339+
// Use model preferences or system prompt as identifier if available
340+
if modelPrefs, ok := paramsMap["modelPreferences"].(map[string]interface{}); ok && modelPrefs != nil {
341+
// Try direct name field first (simplified structure)
342+
if name, ok := modelPrefs["name"].(string); ok && name != "" {
343+
return name, paramsMap
344+
}
345+
// Try to get model name from hints array (full spec structure)
346+
if hints, ok := modelPrefs["hints"].([]interface{}); ok && len(hints) > 0 {
347+
if hint, ok := hints[0].(map[string]interface{}); ok {
348+
if name, ok := hint["name"].(string); ok && name != "" {
349+
return name, paramsMap
350+
}
351+
}
352+
}
353+
}
354+
if systemPrompt, ok := paramsMap["systemPrompt"].(string); ok && systemPrompt != "" {
355+
// Use first 50 chars of system prompt as identifier
356+
// This provides a reasonable balance between uniqueness and readability
357+
if len(systemPrompt) > 50 {
358+
return systemPrompt[:50], paramsMap
359+
}
360+
return systemPrompt, paramsMap
361+
}
362+
return "", paramsMap
363+
}
364+
365+
// handleResourceSubscribeMethod extracts resource ID for resource subscribe operations
366+
func handleResourceSubscribeMethod(paramsMap map[string]interface{}) (string, map[string]interface{}) {
367+
if uri, ok := paramsMap["uri"].(string); ok {
368+
return uri, nil
284369
}
285370
return "", nil
286371
}
287372

373+
// handleResourceUnsubscribeMethod extracts resource ID for resource unsubscribe operations
374+
func handleResourceUnsubscribeMethod(paramsMap map[string]interface{}) (string, map[string]interface{}) {
375+
if uri, ok := paramsMap["uri"].(string); ok {
376+
return uri, nil
377+
}
378+
return "", nil
379+
}
380+
381+
// handleProgressNotificationMethod extracts resource ID for progress notifications.
382+
// Extracts the progressToken which can be either a string or numeric value.
383+
func handleProgressNotificationMethod(paramsMap map[string]interface{}) (string, map[string]interface{}) {
384+
if token, ok := paramsMap["progressToken"].(string); ok {
385+
return token, paramsMap
386+
}
387+
// Also handle numeric progress tokens
388+
if token, ok := paramsMap["progressToken"].(float64); ok {
389+
return strconv.FormatFloat(token, 'f', 0, 64), paramsMap
390+
}
391+
return "", paramsMap
392+
}
393+
394+
// handleCancelledNotificationMethod extracts resource ID for cancelled notifications.
395+
// Extracts the requestId which can be either a string or numeric value.
396+
func handleCancelledNotificationMethod(paramsMap map[string]interface{}) (string, map[string]interface{}) {
397+
// Extract request ID as the resource identifier
398+
if requestId, ok := paramsMap["requestId"].(string); ok {
399+
return requestId, paramsMap
400+
}
401+
// Handle numeric request IDs
402+
if requestId, ok := paramsMap["requestId"].(float64); ok {
403+
return strconv.FormatFloat(requestId, 'f', 0, 64), paramsMap
404+
}
405+
return "", paramsMap
406+
}
407+
288408
// GetMCPMethod is a convenience function to get the MCP method from the context.
289409
func GetMCPMethod(ctx context.Context) string {
290410
if parsed := GetParsedMCPRequest(ctx); parsed != nil {

0 commit comments

Comments
 (0)