Переглянути джерело

feat(mcp): 添加工具函数以简化 MCP 服务的创建

- 新增 ServerAddTools 函数,用于自动添加 MCP 工具- 实现 serverAddToolsByMethod 函数,根据方法生成 MCP工具
- 添加 getFiledMessageParamProperties 函数,获取字段消息参数属性
- 更新 go.mod 和 go.sum 文件,添加相关依赖
dcsunny 5 місяців тому
батько
коміт
7536bc0f93
3 змінених файлів з 179 додано та 1 видалено
  1. 5 1
      go.mod
  2. 9 0
      go.sum
  3. 165 0
      mcp/tools.go

+ 5 - 1
go.mod

@@ -14,7 +14,7 @@ require (
 	github.com/go-kratos/kratos/v2 v2.8.3
 	github.com/go-resty/resty/v2 v2.7.0
 	github.com/google/gnostic v0.7.0
-	github.com/google/uuid v1.4.0
+	github.com/google/uuid v1.6.0
 	github.com/jhump/protoreflect v1.17.0
 	github.com/lestrrat-go/jwx v1.2.25
 	github.com/vmihailenco/msgpack/v5 v5.3.0
@@ -25,6 +25,7 @@ require (
 )
 
 require (
+	git.ikuban.com/server/base-protobuf v0.0.0-20250530011656-faa573c94d9a // indirect
 	github.com/bufbuild/protocompile v0.14.1 // indirect
 	github.com/coreos/go-semver v0.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.3.2 // indirect
@@ -41,10 +42,13 @@ require (
 	github.com/lestrrat-go/httpcc v1.0.1 // indirect
 	github.com/lestrrat-go/iter v1.0.1 // indirect
 	github.com/lestrrat-go/option v1.0.0 // indirect
+	github.com/mark3labs/mcp-go v0.30.1 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	go.etcd.io/etcd/api/v3 v3.5.18 // indirect
 	go.etcd.io/etcd/client/pkg/v3 v3.5.18 // indirect
 	go.etcd.io/etcd/client/v3 v3.5.18 // indirect

+ 9 - 0
go.sum

@@ -593,6 +593,8 @@ cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT
 cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
+git.ikuban.com/server/base-protobuf v0.0.0-20250530011656-faa573c94d9a h1:hzndtRfgdoxJ+RjplcRtgaXnCFdQwO4RVAjn5tfmfB4=
+git.ikuban.com/server/base-protobuf v0.0.0-20250530011656-faa573c94d9a/go.mod h1:cZ54FWN2AIukt0UparfDiKxjtul2ltS8NCzFLZpCNfU=
 git.ikuban.com/server/json v0.0.0-20210408053838-50ac5ceda83a h1:2OcIUm+cnO7dbUNPxoylWFkNizpeLI1RxiV4jVHSDbc=
 git.ikuban.com/server/json v0.0.0-20210408053838-50ac5ceda83a/go.mod h1:tRbbUpdE5PLoYhhkgt+XjE4RiydCsgm2r/Vjq/LtZic=
 git.ikuban.com/server/kratos-etcd v0.0.0-20250225030354-ebd49a034588 h1:tGIGsvkSas/jnqdjVsR+7WRqt4GkN/92XDG5kVKW3c0=
@@ -799,6 +801,7 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
 github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
 github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
 github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
@@ -863,6 +866,8 @@ github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFe
 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
 github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
+github.com/mark3labs/mcp-go v0.30.1 h1:3R1BPvNT/rC1iPpLx+EMXFy+gvux/Mz/Nio3c6XEU9E=
+github.com/mark3labs/mcp-go v0.30.1/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@@ -900,6 +905,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
 github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -924,6 +931,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

+ 165 - 0
mcp/tools.go

@@ -0,0 +1,165 @@
+package mcp
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"strings"
+
+	"git.ikuban.com/server/base-protobuf/kuban/options"
+	openapi_v3 "github.com/google/gnostic/openapiv3"
+	mcp2 "github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+	"google.golang.org/genproto/googleapis/api/annotations"
+	"google.golang.org/grpc"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/reflect/protoregistry"
+)
+
+func ServerAddTools(s *server.MCPServer, srv any, svcDesc grpc.ServiceDesc) error {
+	serviceName := strings.ReplaceAll(svcDesc.ServiceName, ".", "_")
+
+	handlerMap := make(map[string]grpc.MethodDesc)
+
+	for _, _v := range svcDesc.Methods {
+		v := _v
+		mapK := serviceName + "_" + v.MethodName
+		handlerMap[mapK] = v
+	}
+	d, err := protoregistry.GlobalFiles.FindFileByPath(svcDesc.Metadata.(string))
+	if err != nil {
+
+		return err
+	}
+	if d.Services().Len() == 0 {
+		return nil
+	}
+	ser := d.Services().Get(0)
+
+	for j := 0; j < ser.Methods().Len(); j++ {
+		method := ser.Methods().Get(j)
+		t, h := serverAddToolsByMethod(serviceName, srv, method, handlerMap)
+		s.AddTool(*t, h)
+	}
+	return nil
+}
+
+func serverAddToolsByMethod(serviceName string, srv any, method protoreflect.MethodDescriptor, handlerMap map[string]grpc.MethodDesc) (*mcp2.Tool, server.ToolHandlerFunc) {
+	methodMcpOpts, _ := proto.GetExtension(method.Options(), options.E_McpOptions).(*options.McpOptions)
+	if methodMcpOpts == nil || !methodMcpOpts.Enabled {
+		return nil, nil
+	}
+	methodOperation, _ := proto.GetExtension(method.Options(), openapi_v3.E_Operation).(*openapi_v3.Operation)
+	description := ""
+	if methodOperation != nil {
+		description = methodOperation.Description
+		if description == "" {
+			description = methodOperation.Summary
+		}
+	}
+	toolOptions := []mcp2.ToolOption{mcp2.WithDescription(description)}
+	for k := 0; k < method.Input().Fields().Len(); k++ {
+		input := method.Input().Fields().Get(k)
+		inputOperation, _ := proto.GetExtension(input.Options(), openapi_v3.E_Property).(*openapi_v3.Schema)
+		inputOperation2, _ := proto.GetExtension(input.Options(), annotations.E_FieldBehavior).([]annotations.FieldBehavior)
+		inputDescription := ""
+		if inputOperation != nil {
+			inputDescription = inputOperation.GetDescription()
+		}
+		propertyOption := []mcp2.PropertyOption{mcp2.Description(inputDescription)}
+		if inputOperation2 != nil && len(inputOperation2) > 0 && inputOperation2[0] == annotations.FieldBehavior_REQUIRED {
+			propertyOption = append(propertyOption, mcp2.Required())
+		}
+		switch input.Kind() {
+		case protoreflect.StringKind:
+			toolOptions = append(toolOptions, mcp2.WithString(string(input.Name()), propertyOption...))
+		case protoreflect.BoolKind:
+			toolOptions = append(toolOptions, mcp2.WithBoolean(string(input.Name()), propertyOption...))
+		case protoreflect.DoubleKind, protoreflect.FloatKind,
+			protoreflect.Sfixed64Kind, protoreflect.Sfixed32Kind,
+			protoreflect.Fixed64Kind, protoreflect.Fixed32Kind,
+			protoreflect.Sint64Kind, protoreflect.Sint32Kind,
+			protoreflect.Uint64Kind, protoreflect.Uint32Kind,
+			protoreflect.Int64Kind, protoreflect.Int32Kind:
+			toolOptions = append(toolOptions, mcp2.WithNumber(string(input.Name()), propertyOption...))
+		case protoreflect.MessageKind:
+			propertyOption = append(propertyOption, mcp2.Properties(getFiledMessageParamProperties(input.Message())))
+			toolOptions = append(toolOptions, mcp2.WithObject(string(input.Name()), propertyOption...))
+		}
+	}
+	toolName := serviceName + "_" + string(method.Name())
+	t := mcp2.NewTool(toolName, toolOptions...)
+
+	h := func(ctx context.Context, request mcp2.CallToolRequest) (*mcp2.CallToolResult, error) {
+
+		if _, ok := handlerMap[toolName]; !ok {
+			return nil, errors.New("没有实现")
+		}
+		arg := request.GetArguments()
+		argJson, _ := json.Marshal(arg)
+		dec := func(in any) error {
+			decErr := json.Unmarshal(argJson, &in)
+			if decErr != nil {
+				return decErr
+			}
+			return nil
+		}
+		interceptor := func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
+			return handler(ctx, req)
+		}
+
+		handler := handlerMap[toolName]
+		out, outErr := handler.Handler(srv, ctx, dec, interceptor)
+		if outErr != nil {
+			return nil, outErr
+		}
+
+		outJson, _ := json.Marshal(out)
+
+		callToolResult := &mcp2.CallToolResult{
+			Content: []mcp2.Content{
+				mcp2.TextContent{
+					Type: "text",
+					Text: string(outJson),
+				},
+			},
+		}
+		return callToolResult, nil
+	}
+	return &t, h
+}
+
+func getFiledMessageParamProperties(message protoreflect.MessageDescriptor) map[string]any {
+
+	messageParamMap := make(map[string]any)
+
+	for i := 0; i < message.Fields().Len(); i++ {
+		input := message.Fields().Get(i)
+		paramMap := make(map[string]any)
+		inputOperation, _ := proto.GetExtension(input.Options(), openapi_v3.E_Property).(*openapi_v3.Schema)
+		inputDescription := ""
+		if inputOperation != nil {
+			inputDescription = inputOperation.GetDescription()
+		}
+		paramMap["description"] = inputDescription
+
+		switch input.Kind() {
+		case protoreflect.StringKind:
+			paramMap["type"] = "string"
+		case protoreflect.BoolKind:
+			paramMap["type"] = "boolean"
+		case protoreflect.DoubleKind, protoreflect.FloatKind,
+			protoreflect.Sfixed64Kind, protoreflect.Sfixed32Kind,
+			protoreflect.Fixed64Kind, protoreflect.Fixed32Kind,
+			protoreflect.Sint64Kind, protoreflect.Sint32Kind,
+			protoreflect.Uint64Kind, protoreflect.Uint32Kind,
+			protoreflect.Int64Kind, protoreflect.Int32Kind:
+			paramMap["type"] = "number"
+		default:
+			break
+		}
+		messageParamMap[string(input.Name())] = paramMap
+	}
+	return messageParamMap
+}