Files
probo/third_party/mcpgen/internal/codegen/parser.go
Cursor Agent 5b47a83160 Annotate MCP tools with titles and hints
Claude and other MCP clients use title, readOnlyHint, and
destructiveHint to present reads, writes, and deletes accurately.
Add a title to every tool, mark missing delete/unlink/cancel/void
tools as destructive, and teach mcpgen to emit those annotations
(including destructiveHint: false for non-destructive writes).

Temporary third_party/mcpgen fork until title support lands
upstream.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-07-30 09:15:07 +00:00

192 lines
4.9 KiB
Go

package codegen
import (
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"strings"
)
type HandlerInfo struct {
Name string
RecvType string
SourceCode string
IsOrphaned bool
}
type ResolverParser struct {
filePath string
fset *token.FileSet
file *ast.File
}
func NewResolverParser(filePath string) (*ResolverParser, error) {
fset := token.NewFileSet()
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil, fmt.Errorf("resolver file not found: %s", filePath)
}
file, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse resolver file: %w", err)
}
return &ResolverParser{
filePath: filePath,
fset: fset,
file: file,
}, nil
}
func (p *ResolverParser) ExtractHandlers(resolverType string) (map[string]*HandlerInfo, error) {
handlers := make(map[string]*HandlerInfo)
// Extract from both old wrapper types and new direct Resolver type
allowedTypes := map[string]bool{
// Old wrapper types (for backward compatibility during migration)
"toolResolver": true,
"*toolResolver": true,
"promptResolver": true,
"*promptResolver": true,
"resourceResolver": true,
"*resourceResolver": true,
// New direct Resolver type
resolverType: true,
"*" + resolverType: true,
}
for _, decl := range p.file.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
if funcDecl.Recv == nil {
continue
}
recvType := p.getReceiverType(funcDecl.Recv)
if !allowedTypes[recvType] {
continue
}
methodName := funcDecl.Name.Name
sourceCode, err := p.extractFunctionSource(funcDecl)
if err != nil {
return nil, fmt.Errorf("failed to extract source for %s: %w", methodName, err)
}
// Transform receiver type from old wrapper types to main Resolver type
sourceCode = TransformReceiverType(sourceCode, resolverType)
handlers[methodName] = &HandlerInfo{
Name: methodName,
RecvType: "*" + resolverType, // Always use main Resolver type
SourceCode: sourceCode,
IsOrphaned: false,
}
}
return handlers, nil
}
func (p *ResolverParser) getReceiverType(recv *ast.FieldList) string {
if recv == nil || len(recv.List) == 0 {
return ""
}
field := recv.List[0]
switch typ := field.Type.(type) {
case *ast.Ident:
return typ.Name
case *ast.StarExpr:
if ident, ok := typ.X.(*ast.Ident); ok {
return "*" + ident.Name
}
}
return ""
}
func (p *ResolverParser) extractFunctionSource(funcDecl *ast.FuncDecl) (string, error) {
var buf strings.Builder
cfg := printer.Config{
Mode: printer.UseSpaces | printer.TabIndent,
Tabwidth: 8,
}
if err := cfg.Fprint(&buf, p.fset, funcDecl); err != nil {
return "", err
}
return buf.String(), nil
}
// TransformReceiverType rewrites the receiver type in handler source code from old wrapper types
// (toolResolver, promptResolver, resourceResolver) to the main Resolver type
func TransformReceiverType(sourceCode, resolverType string) string {
// Replace old wrapper types with main Resolver type
sourceCode = strings.ReplaceAll(sourceCode, "*toolResolver)", "*"+resolverType+")")
sourceCode = strings.ReplaceAll(sourceCode, "*promptResolver)", "*"+resolverType+")")
sourceCode = strings.ReplaceAll(sourceCode, "*resourceResolver)", "*"+resolverType+")")
return sourceCode
}
func IdentifyOrphanedHandlers(existingHandlers map[string]*HandlerInfo, requiredHandlers []string) {
requiredSet := make(map[string]bool)
for _, name := range requiredHandlers {
requiredSet[name] = true
}
for name, handler := range existingHandlers {
if !requiredSet[name] {
handler.IsOrphaned = true
}
}
}
func FormatOrphanedHandlers(handlers map[string]*HandlerInfo) string {
var orphaned []*HandlerInfo
for _, handler := range handlers {
if handler.IsOrphaned {
orphaned = append(orphaned, handler)
}
}
if len(orphaned) == 0 {
return ""
}
var buf strings.Builder
buf.WriteString("\n\n// ==============================================================================\n")
buf.WriteString("// Orphaned Handlers\n")
buf.WriteString("// ==============================================================================\n")
buf.WriteString("// The following handlers were found in the resolver file but are no longer\n")
buf.WriteString("// defined in the MCP specification. They have been preserved here as comments\n")
buf.WriteString("// in case you need to reference or restore them.\n")
buf.WriteString("// ==============================================================================\n\n")
for _, handler := range orphaned {
buf.WriteString(fmt.Sprintf("// Orphaned: %s\n", handler.Name))
buf.WriteString("// Uncomment and update signature if you want to restore this handler.\n")
lines := strings.Split(handler.SourceCode, "\n")
for _, line := range lines {
if strings.TrimSpace(line) != "" {
buf.WriteString("// ")
}
buf.WriteString(line)
buf.WriteString("\n")
}
buf.WriteString("\n")
}
return buf.String()
}