Skip to content

Commit f0a767e

Browse files
authored
Merge branch 'main' into proposal/virtual-mcp-server
2 parents 9312f00 + 5b0826f commit f0a767e

File tree

11 files changed

+1489
-39
lines changed

11 files changed

+1489
-39
lines changed

cmd/thv/app/registry.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,7 @@ func printTextServerInfo(name string, server registry.ServerMetadata) {
318318

319319
// Print example command
320320
fmt.Println("\nExample Command:")
321-
if server.IsRemote() {
322-
fmt.Printf(" thv proxy %s\n", name)
323-
} else {
324-
fmt.Printf(" thv run %s\n", name)
325-
}
321+
fmt.Printf(" thv run %s\n", name)
326322
}
327323

328324
// truncateString truncates a string to the specified length and adds "..." if truncated

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)