1
0
mirror of https://github.com/go-micro/go-micro.git synced 2026-06-03 18:44:36 +02:00
Files
go-micro/gateway/mcp/parser.go
2026-02-11 14:12:21 +00:00

340 lines
8.9 KiB
Go

package mcp
import (
"fmt"
"go/ast"
"go/doc"
"go/parser"
"go/token"
"path/filepath"
"reflect"
"regexp"
"strings"
"go-micro.dev/v5/registry"
)
// ToolDescription represents enhanced documentation for an MCP tool
type ToolDescription struct {
Summary string
Description string
Params []ParamDoc
Returns []ReturnDoc
Examples []string
}
// ParamDoc describes a parameter
type ParamDoc struct {
Name string
Type string
Description string
Required bool
}
// ReturnDoc describes a return value
type ReturnDoc struct {
Type string
Description string
}
var (
// Regex patterns for JSDoc-style tags
paramPattern = regexp.MustCompile(`@param\s+(\w+)\s+\{(\w+)\}\s+(.+)`)
returnPattern = regexp.MustCompile(`@return\s+\{(\w+)\}\s+(.+)`)
examplePattern = regexp.MustCompile(`@example\s+([\s\S]+?)(?:@\w+|$)`)
)
// parseServiceDocs attempts to parse Go source files to extract documentation
// for service methods. This enhances tool descriptions with godoc comments.
func parseServiceDocs(serviceName string, endpoint *registry.Endpoint) *ToolDescription {
// For now, return basic description
// Full implementation would:
// 1. Use go/parser to find service source files
// 2. Extract godoc comments for methods
// 3. Parse JSDoc-style tags (@param, @return, @example)
// 4. Return rich ToolDescription
desc := &ToolDescription{
Summary: fmt.Sprintf("Call %s on %s service", endpoint.Name, serviceName),
Description: "",
Params: parseEndpointParams(endpoint.Request),
Returns: parseEndpointReturns(endpoint.Response),
Examples: []string{},
}
return desc
}
// parseEndpointParams extracts parameter documentation from registry Value
func parseEndpointParams(value *registry.Value) []ParamDoc {
if value == nil || len(value.Values) == 0 {
return nil
}
params := make([]ParamDoc, 0, len(value.Values))
for _, field := range value.Values {
params = append(params, ParamDoc{
Name: field.Name,
Type: field.Type,
Description: formatFieldDescription(field.Name, field.Type),
Required: true, // Conservative default
})
}
return params
}
// parseEndpointReturns extracts return value documentation
func parseEndpointReturns(value *registry.Value) []ReturnDoc {
if value == nil {
return nil
}
return []ReturnDoc{{
Type: value.Name,
Description: fmt.Sprintf("Returns %s", value.Name),
}}
}
// formatFieldDescription creates a basic description for a field
func formatFieldDescription(name, typeName string) string {
// Convert camelCase/PascalCase to readable format
readable := toReadable(name)
return fmt.Sprintf("%s (%s)", readable, typeName)
}
// toReadable converts camelCase or PascalCase to readable format
func toReadable(s string) string {
// Insert spaces before uppercase letters
var result strings.Builder
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result.WriteRune(' ')
}
result.WriteRune(r)
}
return result.String()
}
// ParseGoDocComment parses a Go doc comment for JSDoc-style tags
func ParseGoDocComment(comment string) *ToolDescription {
desc := &ToolDescription{
Params: []ParamDoc{},
Returns: []ReturnDoc{},
Examples: []string{},
}
// Extract summary (first line)
lines := strings.Split(comment, "\n")
if len(lines) > 0 {
desc.Summary = strings.TrimSpace(lines[0])
}
// Extract full description (before first tag)
tagStart := strings.Index(comment, "@")
if tagStart > 0 {
desc.Description = strings.TrimSpace(comment[:tagStart])
} else {
desc.Description = strings.TrimSpace(comment)
}
// Parse @param tags
paramMatches := paramPattern.FindAllStringSubmatch(comment, -1)
for _, match := range paramMatches {
if len(match) == 4 {
desc.Params = append(desc.Params, ParamDoc{
Name: match[1],
Type: match[2],
Description: strings.TrimSpace(match[3]),
Required: true,
})
}
}
// Parse @return tags
returnMatches := returnPattern.FindAllStringSubmatch(comment, -1)
for _, match := range returnMatches {
if len(match) == 3 {
desc.Returns = append(desc.Returns, ReturnDoc{
Type: match[1],
Description: strings.TrimSpace(match[2]),
})
}
}
// Parse @example tags
exampleMatches := examplePattern.FindAllStringSubmatch(comment, -1)
for _, match := range exampleMatches {
if len(match) == 2 {
example := strings.TrimSpace(match[1])
desc.Examples = append(desc.Examples, example)
}
}
return desc
}
// enhanceToolDescription attempts to enhance a tool with parsed documentation
func enhanceToolDescription(tool *Tool, serviceName string, endpoint *registry.Endpoint) {
// Try to parse service documentation
toolDesc := parseServiceDocs(serviceName, endpoint)
// Update tool description with parsed info
if toolDesc.Summary != "" {
tool.Description = toolDesc.Summary
}
// Add detailed description to input schema
if toolDesc.Description != "" {
if tool.InputSchema == nil {
tool.InputSchema = make(map[string]interface{})
}
tool.InputSchema["description"] = toolDesc.Description
}
// Enhance parameter descriptions
if len(toolDesc.Params) > 0 {
properties, ok := tool.InputSchema["properties"].(map[string]interface{})
if ok {
for _, param := range toolDesc.Params {
if propSchema, exists := properties[param.Name]; exists {
if propMap, ok := propSchema.(map[string]interface{}); ok {
propMap["description"] = param.Description
if param.Required {
// Add to required array
required, _ := tool.InputSchema["required"].([]string)
required = append(required, param.Name)
tool.InputSchema["required"] = required
}
}
}
}
}
}
// Add examples if available
if len(toolDesc.Examples) > 0 {
tool.InputSchema["examples"] = toolDesc.Examples
}
}
// ParseStructTags extracts JSON schema information from struct tags
// This can be used to enhance parameter descriptions
func ParseStructTags(t reflect.Type) map[string]interface{} {
schema := map[string]interface{}{
"type": "object",
"properties": make(map[string]interface{}),
}
properties := schema["properties"].(map[string]interface{})
required := []string{}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Get JSON tag
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Parse JSON tag
jsonName := strings.Split(jsonTag, ",")[0]
omitempty := strings.Contains(jsonTag, "omitempty")
// Get description from validate tag or description tag
description := field.Tag.Get("description")
if description == "" {
description = formatFieldDescription(field.Name, field.Type.String())
}
// Build property schema
propSchema := map[string]interface{}{
"description": description,
}
// Add type information
propSchema["type"] = reflectTypeToJSONType(field.Type)
properties[jsonName] = propSchema
// Track required fields
if !omitempty {
required = append(required, jsonName)
}
}
if len(required) > 0 {
schema["required"] = required
}
return schema
}
// reflectTypeToJSONType converts Go reflect.Type to JSON schema type
func reflectTypeToJSONType(t reflect.Type) string {
switch t.Kind() {
case reflect.String:
return "string"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "integer"
case reflect.Float32, reflect.Float64:
return "number"
case reflect.Bool:
return "boolean"
case reflect.Slice, reflect.Array:
return "array"
case reflect.Map, reflect.Struct:
return "object"
default:
return "string"
}
}
// findServiceSource attempts to locate Go source files for a service
// This is used to extract godoc comments
func findServiceSource(serviceName string) ([]string, error) {
// This would search GOPATH/module cache for service sources
// For now, return empty - implementation would use:
// - go/packages to find module
// - Search for service struct definitions
// - Return list of source files
return nil, fmt.Errorf("source discovery not yet implemented")
}
// parseGoFile parses a Go source file and extracts method documentation
func parseGoFile(filename string, serviceName string) (map[string]*ToolDescription, error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return nil, err
}
docs := make(map[string]*ToolDescription)
// Use go/doc to extract documentation
pkg := &ast.Package{
Name: f.Name.Name,
Files: map[string]*ast.File{filename: f},
}
docPkg := doc.New(pkg, filepath.Dir(filename), doc.AllDecls)
// Extract method documentation
for _, typ := range docPkg.Types {
if !strings.Contains(typ.Name, serviceName) {
continue
}
for _, method := range typ.Methods {
toolDesc := ParseGoDocComment(method.Doc)
toolDesc.Summary = fmt.Sprintf("%s - %s", method.Name, toolDesc.Summary)
docs[method.Name] = toolDesc
}
}
return docs, nil
}