mirror of
https://github.com/go-micro/go-micro.git
synced 2026-06-03 18:44:36 +02:00
340 lines
8.9 KiB
Go
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
|
|
}
|