From 58d02bcf0859d0c69274d21c8b0b0f721e8b439c Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Wed, 9 Jul 2025 18:48:35 +0800 Subject: [PATCH] Optimizing the parameter tiling of MCP Tool to facilitate AI understanding --- controller/mcp/iml.go | 23 ++++ controller/mcp/mcp.go | 1 + go.mod | 8 +- go.sum | 18 ++- log-driver/loki/loki_test.go | 20 +-- mcp-server/param.go | 87 +++++++++++++ mcp-server/server.go | 231 +++++++++++++++++++++++++++++++---- mcp-server/tool.go | 104 +++++++--------- module/ai-api/schema.go | 39 +++--- module/catalogue/iml.go | 49 -------- module/mcp/iml.go | 145 +++++++++++----------- module/mcp/module.go | 4 +- module/publish/iml.go | 65 +--------- module/service/iml.go | 64 +--------- plugins/openapi/mcp.go | 10 +- stores/api/model.go | 2 +- 16 files changed, 498 insertions(+), 372 deletions(-) create mode 100644 mcp-server/param.go diff --git a/controller/mcp/iml.go b/controller/mcp/iml.go index 804b051e..bd1871ad 100644 --- a/controller/mcp/iml.go +++ b/controller/mcp/iml.go @@ -190,6 +190,7 @@ func (i *imlMcpController) OnComplete() { i.server["ja-JP"] = server.NewSSEServer(i.generateJPMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath))) i.openServer = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/")))) + } func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) { @@ -263,6 +264,28 @@ func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) { i.handleMessage(ctx, mcp_server.DefaultMCPServer()) } +func (i *imlMcpController) ServiceHandleStreamHTTP(ctx *gin.Context) { + apikey := ctx.Request.URL.Query().Get("apikey") + serviceId := ctx.Param("serviceId") + if serviceId == "" { + ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"}) + return + } + ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey) + if err != nil { + ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"}) + return + } + if !ok { + ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"}) + return + } + cfg := i.settingModule.Get(ctx) + req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress)) + req = req.WithContext(utils.SetLabel(req.Context(), "apikey", apikey)) + mcp_server.DefaultMCPServer().ServeHTTP(ctx.Writer, req) +} + func (i *imlMcpController) handleMessage(ctx *gin.Context, server http.Handler) { sessionId := ctx.Request.URL.Query().Get("sessionId") apikey, ok := i.sessionKeys.Load(sessionId) diff --git a/controller/mcp/mcp.go b/controller/mcp/mcp.go index 780a0aa9..fd66e263 100644 --- a/controller/mcp/mcp.go +++ b/controller/mcp/mcp.go @@ -15,6 +15,7 @@ type IMcpController interface { ServiceHandleSSE(ctx *gin.Context) ServiceHandleMessage(ctx *gin.Context) GlobalMCPConfig(ctx *gin.Context) (string, error) + ServiceHandleStreamHTTP(ctx *gin.Context) } func init() { diff --git a/go.mod b/go.mod index 95c0a6af..a0e81c20 100644 --- a/go.mod +++ b/go.mod @@ -9,13 +9,13 @@ require ( github.com/eolinker/eosc v0.18.3 github.com/eolinker/go-common v1.1.7 github.com/gabriel-vasile/mimetype v1.4.4 - github.com/getkin/kin-openapi v0.127.0 + github.com/getkin/kin-openapi v0.132.0 github.com/gin-contrib/gzip v1.0.1 github.com/gin-gonic/gin v1.10.0 github.com/go-sql-driver/mysql v1.7.0 github.com/google/uuid v1.6.0 github.com/influxdata/influxdb-client-go/v2 v2.14.0 - github.com/mark3labs/mcp-go v0.17.0 + github.com/mark3labs/mcp-go v0.33.0 github.com/mitchellh/mapstructure v1.5.0 github.com/nsqio/go-nsq v1.1.0 github.com/ollama/ollama v0.5.8 @@ -46,7 +46,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -59,10 +58,13 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oapi-codegen/runtime v1.0.0 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/redis/go-redis/v9 v9.5.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect diff --git a/go.sum b/go.sum index 42d19a99..431be974 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,12 @@ github.com/eolinker/eosc v0.18.3 h1:3IK5HkAPnJRfLbQ0FR7kWsZr6Y/OiqqGazvN1q2BL5A= github.com/eolinker/eosc v0.18.3/go.mod h1:O9PQQXFCpB6fjHf+oFt/LN6EOAv779ItbMixMKCfTfk= github.com/eolinker/go-common v1.1.7 h1:bi7wDmlCYQGjS3k8Bz/o+Mo9aMJAzmPsBLXWurxPfwk= github.com/eolinker/go-common v1.1.7/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= -github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= -github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE= @@ -78,8 +80,6 @@ github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjw github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -101,8 +101,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= -github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= +github.com/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvTCc= +github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -118,6 +118,10 @@ github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/ollama/ollama v0.5.8 h1:b2S6YdZ18/ntCsWzoy/HmB3BHGW4GX0Qp7RARrJtJXU= github.com/ollama/ollama v0.5.8/go.mod h1:ibdmDvb/TjKY1OArBWIazL3pd1DHTk8eG2MMjEkWhiI= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -134,6 +138,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 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= diff --git a/log-driver/loki/loki_test.go b/log-driver/loki/loki_test.go index 0ca5a61f..0cc93cd0 100644 --- a/log-driver/loki/loki_test.go +++ b/log-driver/loki/loki_test.go @@ -33,12 +33,12 @@ func TestLoki(t *testing.T) { // headers["Content-Type"] = "application/json" // headers["X-Scope-OrgID"] = "tenant1" // queries := url.Values{} -// queries.Set("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`") +// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`") // now := time.Now() // start := now.Add(-time.Hour * 24 * 30) -// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10)) -// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10)) -// queries.Set("limit", "100") +// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10)) +// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10)) +// queries.SetMCPServer("limit", "100") // a := time.Now() // result, err := send[LogInfo](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "") // if err != nil { @@ -57,8 +57,8 @@ func TestLoki(t *testing.T) { // headers["Content-Type"] = "application/json" // headers["X-Scope-OrgID"] = "tenant1" // queries := url.Values{} -// //queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)") -// queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))") +// //queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)") +// queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))") // result, err := send[LogCount](http.MethodGet, "http://localhost:3100/loki/api/v1/query", headers, queries, "") // if err != nil { // t.Fatalf("failed to send request: %v", err) @@ -75,12 +75,12 @@ func TestLoki(t *testing.T) { // headers["Content-Type"] = "application/json" // headers["X-Scope-OrgID"] = "tenant1" // queries := url.Values{} -// queries.Set("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"") +// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"") // now := time.Now() // start := now.Add(-time.Hour * 24 * 30) -// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10)) -// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10)) -// queries.Set("limit", "1") +// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10)) +// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10)) +// queries.SetMCPServer("limit", "1") // now = time.Now() // result, err := send[map[string]interface{}](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "") // t.LogItem(time.Now().Sub(now)) diff --git a/mcp-server/param.go b/mcp-server/param.go new file mode 100644 index 00000000..f4f73459 --- /dev/null +++ b/mcp-server/param.go @@ -0,0 +1,87 @@ +package mcp_server + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +var client = http.Client{} + +type Position string + +const ( + PositionHeader Position = "header" + PositionBody Position = "body" + PositionQuery Position = "query" + PositionPath Position = "path" +) + +type ContentType string + +const ( + ContentTypeJSON ContentType = "application/json" + ContentTypeXML ContentType = "application/xml" + ContentTypeHTML ContentType = "text/html" + ContentTypeText ContentType = "text/plain" + ContentTypeForm ContentType = "application/x-www-form-urlencoded" + ContentTypeFile ContentType = "multipart/form-data" +) + +func NewParam(position Position, required bool, description string) *Param { + return &Param{position: position, required: required, description: description} +} + +type Param struct { + position Position + required bool + description string +} + +func (p *Param) Description() string { + return p.description +} + +func (p *Param) Required() bool { + return p.required +} + +type BodyParam struct { + contentType ContentType + params map[string]interface{} +} + +func NewBodyParam(contentType string) *BodyParam { + t := ContentType(contentType) + if t == "" { + t = ContentTypeJSON + } + return &BodyParam{contentType: t} +} + +func (p *BodyParam) Set(k string, v interface{}) { + if p.params == nil { + p.params = make(map[string]interface{}) + } + p.params[k] = v +} + +func (p *BodyParam) Encode() (string, error) { + switch p.contentType { + case ContentTypeJSON: + data, err := json.Marshal(p.params) + if err != nil { + return "", fmt.Errorf("body param encode error: %w", err) + } + return string(data), nil + case ContentTypeForm, ContentTypeFile: + data := url.Values{} + for k, v := range p.params { + data.Set(k, fmt.Sprintf("%v", v)) + } + return data.Encode(), nil + default: + return "", fmt.Errorf("unsupported content type: %s", p.contentType) + } +} diff --git a/mcp-server/server.go b/mcp-server/server.go index ed6cbd6f..dd6c8a80 100644 --- a/mcp-server/server.go +++ b/mcp-server/server.go @@ -4,10 +4,12 @@ import ( "fmt" "net/http" "strings" + "sync" - "github.com/mark3labs/mcp-go/server" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mitchellh/mapstructure" - "github.com/eolinker/eosc" + "github.com/mark3labs/mcp-go/server" ) var ( @@ -18,55 +20,120 @@ var ( func NewServer() *Server { return &Server{ - sseServers: eosc.BuildUntyped[string, *server.SSEServer](), + servers: make(map[string]*Handler), } } type Server struct { - sseServers eosc.Untyped[string, *server.SSEServer] + servers map[string]*Handler + locker sync.RWMutex } -func (s *Server) Set(path string, sseServer *server.SSEServer) { - s.sseServers.Set(path, sseServer) +type Handler struct { + *server.MCPServer + handlers map[string]http.Handler } -func (s *Server) Del(path string) { - s.sseServers.Del(path) +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") + if strings.HasSuffix(r.URL.Path, "/mcp") { + h.handlers["openapi-stream"].ServeHTTP(w, r) + return + } + if strings.HasPrefix(r.URL.Path, "/api") { + h.handlers["api-sse"].ServeHTTP(w, r) + return + } else if strings.HasPrefix(r.URL.Path, "/openapi") { + h.handlers["openapi-sse"].ServeHTTP(w, r) + return + } + http.NotFound(w, r) + return +} + +func (s *Server) Set(id string, ser *server.MCPServer) { + s.locker.Lock() + defer s.locker.Unlock() + tmp := &Handler{ + MCPServer: ser, + handlers: make(map[string]http.Handler), + } + tmp.handlers["api-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, id))) + tmp.handlers["openapi-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, id))) + tmp.handlers["openapi-stream"] = server.NewStreamableHTTPServer(ser, server.WithEndpointPath(fmt.Sprintf("/openapi/v1/%s/%s/mcp", ServiceBasePath, id))) + s.servers[id] = tmp + +} + +func (s *Server) Del(id string) { + s.locker.Lock() + defer s.locker.Unlock() + delete(s.servers, id) +} + +func (s *Server) Get(id string) (*Handler, bool) { + s.locker.RLock() + defer s.locker.RUnlock() + ser, has := s.servers[id] + if !has { + return nil, false + } + m := &Handler{ + MCPServer: ser.MCPServer, + handlers: make(map[string]http.Handler), + } + for k, v := range ser.handlers { + m.handlers[k] = v + } + + return m, true } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - sseServer, has := s.sseServers.Get(trimPath(r.URL.Path)) + sid, err := genPath(r.URL.Path) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + ser, has := s.Get(sid) if has { - sseServer.ServeHTTP(w, r) + ser.ServeHTTP(w, r) return } http.NotFound(w, r) return } -func trimPath(path string) string { +func genPath(path string) (sid string, err error) { path = strings.TrimSuffix(path, "/") - path = strings.TrimSuffix(path, "/message") - path = strings.TrimSuffix(path, "/sse") - return path + ps := strings.Split(path, "/") + if len(ps) < 2 { + err = fmt.Errorf("invalid path: %s", path) + return + } + sid = ps[len(ps)-2] + return } -func SetSSEServer(sid string, name string, version string, tools ...ITool) { - s := server.NewMCPServer(name, version) +func SetServer(sid string, name string, version string, tools ...ITool) { + ser, has := mcpServer.Get(sid) + if !has { + mcpServer.Set(sid, server.NewMCPServer(name, version, server.WithToolCapabilities(true))) + ser, has = mcpServer.Get(sid) + if !has { + return + } + } + ts := make([]server.ServerTool, 0, len(tools)) for _, tool := range tools { - tool.RegisterMCP(s) + ts = append(ts, tool.Tool()) } - apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid) - openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid) - mcpServer.Set(apiPath, server.NewSSEServer(s, server.WithBasePath(apiPath))) - mcpServer.Set(openAPIPath, server.NewSSEServer(s, server.WithBasePath(openAPIPath))) + ser.SetTools(ts...) } -func DelSSEServer(sid string) { - apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid) - openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid) - mcpServer.Del(apiPath) - mcpServer.Del(openAPIPath) +func DelServer(sid string) { + mcpServer.Del(sid) } func ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -76,3 +143,115 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request) { func DefaultMCPServer() *Server { return mcpServer } + +func SetServerByOpenapi(sid, name, version, content string) error { + mcpInfo, err := ConvertMCPFromOpenAPI3Data([]byte(content)) + if err != nil { + return fmt.Errorf("convert mcp from openapi3 data error: %w", err) + } + tools := make([]ITool, 0, len(mcpInfo.Apis)) + for _, a := range mcpInfo.Apis { + toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2) + toolOptions = append(toolOptions, mcp.WithDescription(a.Description)) + params := make(map[string]*Param) + for _, v := range a.Params { + params[v.Name] = NewParam(Position(v.In), v.Required, v.Description) + options := make([]mcp.PropertyOption, 0, 2) + if v.Required { + options = append(options, mcp.Required()) + } + options = append(options, mcp.Description(v.Description)) + toolOptions = append(toolOptions, mcp.WithString(v.Name, options...)) + } + if a.Body != nil { + type Schema struct { + Type string `mapstructure:"type"` + Properties map[string]interface{} `mapstructure:"properties"` + Items interface{} `mapstructure:"items"` + Required interface{} `mapstructure:"required"` + } + var tmp Schema + err = mapstructure.Decode(a.Body, &tmp) + if err != nil { + return err + } + required := map[string]struct{}{} + switch t := tmp.Required.(type) { + case []interface{}: + for _, v := range t { + i, ok := v.(string) + if !ok { + continue + } + required[i] = struct{}{} + } + } + for k, v := range tmp.Properties { + description := "" + typ := "string" + isRequired := false + if _, ok := required[k]; ok { + isRequired = true + } + var props map[string]interface{} + var items interface{} + switch t := v.(type) { + case map[string]interface{}: + if m, ok := t["type"]; ok { + n, ok := m.(string) + if ok { + typ = n + } + } + if m, ok := t["description"]; ok { + n, ok := m.(string) + if ok { + description = n + } + } + switch typ { + case "array": + if m, ok := t["items"]; ok { + items = m + } + case "object": + if m, ok := t["properties"]; ok { + n, ok := m.(map[string]interface{}) + if ok { + props = n + } + } + } + } + + params[k] = NewParam(PositionBody, isRequired, description) + options := make([]mcp.PropertyOption, 0, 3) + options = append(options, mcp.Description(description)) + if props != nil { + options = append(options, mcp.Properties(props)) + } + if items != nil { + options = append(options, mcp.Items(items)) + } + switch typ { + case "string": + toolOptions = append(toolOptions, mcp.WithString(k, options...)) + case "integer", "number", "float": + toolOptions = append(toolOptions, mcp.WithNumber(k, options...)) + case "boolean": + toolOptions = append(toolOptions, mcp.WithBoolean(k, options...)) + case "array": + toolOptions = append(toolOptions, mcp.WithArray(k, options...)) + case "object": + toolOptions = append(toolOptions, mcp.WithObject(k, options...)) + default: + return fmt.Errorf("unsupported type: %s", typ) + } + } + } + + tools = append(tools, NewTool(a.Summary, a.Path, a.Method, a.ContentType, params, toolOptions...)) + } + SetServer(sid, name, version, tools...) + return nil +} diff --git a/mcp-server/tool.go b/mcp-server/tool.go index bc07ff74..b17c3ffa 100644 --- a/mcp-server/tool.go +++ b/mcp-server/tool.go @@ -2,7 +2,6 @@ package mcp_server import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -16,36 +15,38 @@ import ( ) type ITool interface { - RegisterMCP(s *server.MCPServer) + Tool() server.ServerTool } -const ( - MCPBody = "Body" - MCPHeader = "Header" - MCPQuery = "Query" - MCPPath = "Path" -) - type Tool struct { name string url string method string contentType string + params map[string]*Param opts []mcp.ToolOption } -func NewTool(name string, uri string, method string, contentType string, opts ...mcp.ToolOption) ITool { +func (t *Tool) Tool() server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool(t.name, t.opts...), + Handler: generateInvokeTool(t.url, t.method, t.contentType, t.params), + } +} + +func NewTool(name string, uri string, method string, contentType string, params map[string]*Param, opts ...mcp.ToolOption) ITool { return &Tool{ name: name, url: uri, method: method, contentType: contentType, + params: params, opts: opts, } } -func (t *Tool) RegisterMCP(s *server.MCPServer) { - s.AddTool(mcp.NewTool(t.name, t.opts...), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func generateInvokeTool(path string, method string, contentType string, params map[string]*Param) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { invokeAddress := utils.GatewayInvoke(ctx) if invokeAddress == "" { return nil, fmt.Errorf("invoke address is empty") @@ -58,69 +59,54 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) { u.Scheme = "http" } - path := t.url queries := url.Values{} headers := make(map[string]string) - body := "" - for k, v := range request.Params.Arguments { - if k == "Body" { - switch a := v.(type) { - case string: - body = a - case map[string]interface{}: - switch t.contentType { - case "application/json": - tmp, _ := json.Marshal(a) - body = string(tmp) - case "application/x-www-form-urlencoded": - bodyValue := url.Values{} - for kk, vv := range a { - bodyValue.Set(kk, fmt.Sprintf("%v", vv)) - } - body = bodyValue.Encode() + bodyParam := NewBodyParam(contentType) + for k, p := range params { + vv, ok := request.GetArguments()[k] + if !ok && p.required { + return nil, fmt.Errorf("param %s is required", k) + } + if p.position == PositionHeader || p.position == PositionQuery || p.position == PositionPath { + v, ok := vv.(string) + if !ok || v == "" { + if p.required { + return nil, fmt.Errorf("param %s is required", k) } - default: - tmp, _ := json.Marshal(a) - body = string(tmp) + continue } - continue } - tmp, ok := v.(map[string]interface{}) - if !ok { - continue - } - switch k { - case MCPHeader: - for kk, vv := range tmp { - headers[kk] = fmt.Sprintf("%v", vv) - } - case MCPQuery: - for kk, vv := range tmp { - queries.Set(kk, fmt.Sprintf("%v", vv)) - } - case MCPPath: - for kk, vv := range tmp { - p, ok := vv.(string) - if !ok { - return nil, fmt.Errorf("invalid path %s", v) - } - path = strings.Replace(path, fmt.Sprintf("{%s}", kk), p, -1) + switch p.position { + case PositionPath: + path = strings.ReplaceAll(path, "{"+k+"}", fmt.Sprintf("%v", vv)) + case PositionQuery: + queries.Set(k, fmt.Sprintf("%v", vv)) + case PositionHeader: + headers[k] = fmt.Sprintf("%v", vv) + case PositionBody: + if vv == nil { + continue } + bodyParam.Set(k, vv) } } + bodyData, err := bodyParam.Encode() + if err != nil { + return nil, err + } u.Path = path u.RawQuery = queries.Encode() - req, err := http.NewRequest(t.method, u.String(), strings.NewReader(body)) + req, err := http.NewRequest(method, u.String(), strings.NewReader(bodyData)) if err != nil { return nil, err } for k, v := range headers { req.Header.Set(k, v) } - if t.contentType != "" { - req.Header.Set("Content-Type", t.contentType) + if contentType != "" { + req.Header.Set("Content-Type", contentType) } apikey := utils.Label(ctx, "apikey") if apikey != "" { @@ -141,7 +127,5 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) { } return mcp.NewToolResultText(string(d)), nil - }) + } } - -var client = http.Client{} diff --git a/module/ai-api/schema.go b/module/ai-api/schema.go index 083f2836..8bbf4817 100644 --- a/module/ai-api/schema.go +++ b/module/ai-api/schema.go @@ -7,7 +7,7 @@ import ( func genOpenAPI3Template(title string, description string) *openapi3.T { result := new(openapi3.T) - result.OpenAPI = "3.1.0" + result.OpenAPI = "3.0.1" result.Info = &openapi3.Info{ Title: title, Description: description, @@ -37,6 +37,8 @@ func genOperation(summary string, description string, variables []*ai_api_dto.Ai func genRequestBody(variables []*ai_api_dto.AiPromptVariable) *openapi3.RequestBodyRef { requestBody := openapi3.NewRequestBody() + requestBody.Description = "Request body" + requestBody.Required = true requestBody.Content = openapi3.NewContentWithSchema(genRequestBodySchema(variables), []string{"application/json"}) return &openapi3.RequestBodyRef{ Value: requestBody, @@ -55,10 +57,14 @@ func genResponse() *openapi3.ResponseRef { func genRequestBodySchema(variables []*ai_api_dto.AiPromptVariable) *openapi3.Schema { result := openapi3.NewObjectSchema() + required := make([]string, 0, 2) + required = append(required, "messages") if len(variables) > 0 { result.WithProperty("variables", genVariableSchema(variables)) - result.WithRequired([]string{"variables", "messages"}) + required = append(required, "variables") } + + result.WithRequired(required) streamSchema := openapi3.NewBoolSchema() streamSchema.Title = "stream" streamSchema.Description = "Whether to stream the response" @@ -129,6 +135,8 @@ func genMessageSchema() *openapi3.Schema { "role": roleSchema, "content": contentSchema, }) + + result.WithRequired([]string{"role", "content"}) return result } @@ -137,20 +145,21 @@ func genMessagesSchema() *openapi3.Schema { result.Title = "Messages" result.Description = "Chat Messages" result.Items = openapi3.NewSchemaRef("#/components/schemas/Message", messageSchema) + result.Required = []string{"content", "role"} return result } func genResponseSchema() *openapi3.Schema { result := openapi3.NewObjectSchema() result.Description = "Response from the server" - + // 创建 choices 数组 choicesSchema := openapi3.NewArraySchema() choiceItemSchema := openapi3.NewObjectSchema() - + // choice 中的 message 字段 choiceItemSchema.WithPropertyRef("message", messageSchemaRef) - + // finish_reason 字段 finishReasonSchema := openapi3.NewStringSchema().WithEnum( "stop", @@ -160,41 +169,41 @@ func genResponseSchema() *openapi3.Schema { "null", ) choiceItemSchema.WithProperty("finish_reason", finishReasonSchema) - + // index 字段 choiceItemSchema.WithProperty("index", openapi3.NewIntegerSchema()) - + // logprobs 字段,可以为 null choiceItemSchema.WithProperty("logprobs", openapi3.NewSchema().WithNullable()) - + choicesSchema.Items = &openapi3.SchemaRef{Value: choiceItemSchema} result.WithProperty("choices", choicesSchema) - + // object 字段 result.WithProperty("object", openapi3.NewStringSchema().WithEnum("chat.completion")) - + // usage 字段 usageSchema := openapi3.NewObjectSchema() usageSchema.WithProperty("prompt_tokens", openapi3.NewIntegerSchema()) usageSchema.WithProperty("completion_tokens", openapi3.NewIntegerSchema()) usageSchema.WithProperty("total_tokens", openapi3.NewIntegerSchema()) - + // prompt_tokens_details 字段 promptTokensDetailsSchema := openapi3.NewObjectSchema() promptTokensDetailsSchema.WithProperty("cached_tokens", openapi3.NewIntegerSchema()) usageSchema.WithProperty("prompt_tokens_details", promptTokensDetailsSchema) - + result.WithProperty("usage", usageSchema) - + // 其他字段 result.WithProperty("created", openapi3.NewIntegerSchema()) result.WithProperty("system_fingerprint", openapi3.NewStringSchema().WithNullable()) result.WithProperty("model", openapi3.NewStringSchema()) result.WithProperty("id", openapi3.NewStringSchema()) - + // 保留原有的错误字段 result.WithProperty("code", openapi3.NewIntegerSchema()) result.WithProperty("error", openapi3.NewStringSchema()) - + return result } diff --git a/module/catalogue/iml.go b/module/catalogue/iml.go index 39e56a22..ee0aa942 100644 --- a/module/catalogue/iml.go +++ b/module/catalogue/iml.go @@ -78,55 +78,6 @@ type imlCatalogueModule struct { root *Root } -//func (i *imlCatalogueModule) OnInit() { -// register.Handle(func(v server.Server) { -// ctx := context.Background() -// list, err := i.releaseService.GetRunningList(ctx) -// if err != nil { -// log.Errorf("onInit: get running list failed:%s", err.Error()) -// return -// } -// if len(list) < 1 || list[0].APICount > 0 { -// return -// } -// serviceMap := make(map[string]*release.Release) -// serviceIds := make([]string, 0, len(list)) -// for _, v := range list { -// if _, ok := serviceMap[v.Service]; !ok { -// serviceMap[v.Service] = v -// serviceIds = append(serviceIds, v.Service) -// } -// } -// if len(serviceIds) < 1 { -// return -// } -// commitIds, err := i.releaseService.GetRunningApiDocCommits(ctx, serviceIds...) -// if err != nil { -// log.Errorf("onInit: get running api doc commits failed:%s", err.Error()) -// return -// } -// if len(commitIds) < 1 { -// return -// } -// listCommits, err := i.apiDocService.ListDocCommit(ctx, commitIds...) -// if err != nil { -// log.Error("onInit: list doc commit failed:", err.Error()) -// return -// } -// for _, v := range listCommits { -// m, ok := serviceMap[v.Target] -// if !ok { -// continue -// } -// -// i.releaseService.UpdateRelease(ctx, m.UUID, &release.Update{ -// APICount: &v.Data.APICount, -// }) -// } -// }) -// -//} - func (i *imlCatalogueModule) DefaultCatalogue(ctx context.Context) (*catalogue_dto.Catalogue, error) { catalogues, err := i.catalogueService.List(ctx) if err != nil { diff --git a/module/mcp/iml.go b/module/mcp/iml.go index 9e2af616..a344ca33 100644 --- a/module/mcp/iml.go +++ b/module/mcp/iml.go @@ -10,7 +10,6 @@ import ( "net/url" "strconv" "strings" - "time" "github.com/APIParkLab/APIPark/service/subscribe" @@ -48,7 +47,7 @@ type imlMcpModule struct { } func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - keyword, _ := req.Params.Arguments["keyword"].(string) + keyword, _ := req.GetArguments()["keyword"].(string) list, err := i.serviceService.Search(ctx, keyword, map[string]interface{}{ "as_server": true, }, "update_at desc") @@ -116,34 +115,34 @@ func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (* } -func (i *imlMcpModule) Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - keyword := req.Params.Arguments["keyword"].(string) - condition := make(map[string]interface{}) - condition["as_app"] = true - list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc") - if err != nil { - return nil, fmt.Errorf("search service error: %w", err) - } - if len(list) == 0 { - list, err = i.serviceService.Search(ctx, "", condition, "update_at desc") - if err != nil { - return nil, fmt.Errorf("search service error: %w", err) - } - } - data, _ := json.Marshal(utils.SliceToSlice(list, func(s *service.Service) *mcp_dto.App { - return &mcp_dto.App{ - Id: s.Id, - Name: s.Name, - Description: s.Name, - CreateTime: s.CreateTime, - UpdateTime: s.UpdateTime, - } - })) - return mcp.NewToolResultText(string(data)), nil -} +//func (i *imlMcpModule) Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { +// keyword := req.GetArguments()["keyword"].(string) +// condition := make(map[string]interface{}) +// condition["as_app"] = true +// list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc") +// if err != nil { +// return nil, fmt.Errorf("search service error: %w", err) +// } +// if len(list) == 0 { +// list, err = i.serviceService.Search(ctx, "", condition, "update_at desc") +// if err != nil { +// return nil, fmt.Errorf("search service error: %w", err) +// } +// } +// data, _ := json.Marshal(utils.SliceToSlice(list, func(s *service.Service) *mcp_dto.App { +// return &mcp_dto.App{ +// Id: s.Id, +// Name: s.Name, +// Description: s.Name, +// CreateTime: s.CreateTime, +// UpdateTime: s.UpdateTime, +// } +// })) +// return mcp.NewToolResultText(string(data)), nil +//} func (i *imlMcpModule) APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - serviceId, _ := req.Params.Arguments["service"].(string) + serviceId, _ := req.GetArguments()["service"].(string) serviceIds := make([]string, 0, 1) if serviceId == "" { serviceIds = append(serviceIds, serviceId) @@ -190,45 +189,45 @@ func (i *imlMcpModule) APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp. return mcp.NewToolResultText(string(data)), nil } -func (i *imlMcpModule) SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - serviceId, ok := req.Params.Arguments["service"].(string) - if !ok { - return nil, fmt.Errorf("service id is required") - } - subscribes, err := i.subscriberService.Subscribers(ctx, serviceId, subscribe.ApplyStatusSubscribe) - if err != nil { - return nil, fmt.Errorf("get subscriber error: %w,service id is %s", err, serviceId) - } - appIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string { - return s.Application - }) - if len(appIds) == 0 { - return nil, fmt.Errorf("no subscriber found,service id is %s", serviceId) - } - list, err := i.appAuthorizationService.ListByApp(ctx, appIds...) - if err != nil { - return nil, fmt.Errorf("get app authorization error: %w,app ids is %s", err, appIds) - } - result := utils.SliceToSlice(list, func(a *application_authorization.Authorization) *mcp_dto.AppAuthorization { - return &mcp_dto.AppAuthorization{ - Id: a.UUID, - Name: a.Name, - Position: a.Position, - TokenName: a.TokenName, - Config: a.Config, - } - }, func(a *application_authorization.Authorization) bool { - if a.Type != "apikey" { - return false - } - if a.ExpireTime != 0 && a.ExpireTime < time.Now().Unix() { - return false - } - return true - }) - data, _ := json.Marshal(result) - return mcp.NewToolResultText(string(data)), nil -} +//func (i *imlMcpModule) SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { +// serviceId, ok := req.GetArguments()["service"].(string) +// if !ok { +// return nil, fmt.Errorf("service id is required") +// } +// subscribes, err := i.subscriberService.Subscribers(ctx, serviceId, subscribe.ApplyStatusSubscribe) +// if err != nil { +// return nil, fmt.Errorf("get subscriber error: %w,service id is %s", err, serviceId) +// } +// appIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string { +// return s.Application +// }) +// if len(appIds) == 0 { +// return nil, fmt.Errorf("no subscriber found,service id is %s", serviceId) +// } +// list, err := i.appAuthorizationService.ListByApp(ctx, appIds...) +// if err != nil { +// return nil, fmt.Errorf("get app authorization error: %w,app ids is %s", err, appIds) +// } +// result := utils.SliceToSlice(list, func(a *application_authorization.Authorization) *mcp_dto.AppAuthorization { +// return &mcp_dto.AppAuthorization{ +// Id: a.UUID, +// Name: a.Name, +// position: a.position, +// TokenName: a.TokenName, +// Config: a.Config, +// } +// }, func(a *application_authorization.Authorization) bool { +// if a.Type != "apikey" { +// return false +// } +// if a.ExpireTime != 0 && a.ExpireTime < time.Now().Unix() { +// return false +// } +// return true +// }) +// data, _ := json.Marshal(result) +// return mcp.NewToolResultText(string(data)), nil +//} var ( client = &http.Client{} @@ -248,18 +247,18 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc u.Scheme = "http" } - path, ok := req.Params.Arguments["path"].(string) + path, ok := req.GetArguments()["path"].(string) if !ok { return nil, fmt.Errorf("invalid path") } u.Path = fmt.Sprintf("%s/%s", strings.TrimSuffix(u.Path, "/"), strings.TrimPrefix(path, "/")) - method, ok := req.Params.Arguments["method"].(string) + method, ok := req.GetArguments()["method"].(string) if !ok { method = "GET" } queryParam := url.Values{} - query, ok := req.Params.Arguments["query"].(map[string]interface{}) + query, ok := req.GetArguments()["query"].(map[string]interface{}) if ok { for k, v := range query { switch v := v.(type) { @@ -278,7 +277,7 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc } u.RawQuery = queryParam.Encode() headerParam := http.Header{} - header, ok := req.Params.Arguments["header"].(map[string]interface{}) + header, ok := req.GetArguments()["header"].(map[string]interface{}) if ok { for k, v := range header { switch v := v.(type) { @@ -294,12 +293,12 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc } } - body, ok := req.Params.Arguments["body"].(string) + body, ok := req.GetArguments()["body"].(string) if !ok { body = "" } - contentType, ok := req.Params.Arguments["content-type"].(string) + contentType, ok := req.GetArguments()["content-type"].(string) if !ok { contentType = "application/json" } diff --git a/module/mcp/module.go b/module/mcp/module.go index 1549dc11..da538f53 100644 --- a/module/mcp/module.go +++ b/module/mcp/module.go @@ -13,12 +13,12 @@ type IMcpModule interface { // Services 获取服务列表 Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) // Apps 获取应用列表 - Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) + //Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) // APIs 获取API列表 APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) // SubscriberAuthorizations 获取订阅者授权 - SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) + //SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) } diff --git a/module/publish/iml.go b/module/publish/iml.go index 9855d228..8cb46a76 100644 --- a/module/publish/iml.go +++ b/module/publish/iml.go @@ -15,8 +15,6 @@ import ( mcp_server "github.com/APIParkLab/APIPark/mcp-server" api_doc "github.com/APIParkLab/APIPark/service/api-doc" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mitchellh/mapstructure" strategy_driver "github.com/APIParkLab/APIPark/module/strategy/driver" strategy_dto "github.com/APIParkLab/APIPark/module/strategy/dto" @@ -657,68 +655,7 @@ func (i *imlPublishModule) updateMCPServer(ctx context.Context, sid string, name if err != nil { return fmt.Errorf("get api doc commit error: %w", err) } - mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content)) - if err != nil { - return fmt.Errorf("convert mcp from openapi3 data error: %w", err) - } - tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis)) - for _, a := range mcpInfo.Apis { - toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2) - toolOptions = append(toolOptions, mcp.WithDescription(a.Description)) - headers := make(map[string]interface{}) - queries := make(map[string]interface{}) - path := make(map[string]interface{}) - for _, v := range a.Params { - p := map[string]interface{}{ - "type": "string", - "required": v.Required, - "description": v.Description, - } - switch v.In { - case "header": - headers[v.Name] = p - case "query": - queries[v.Name] = p - case "path": - path[v.Name] = p - } - } - if len(headers) > 0 { - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers."))) - } - if len(queries) > 0 { - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries."))) - } - if len(path) > 0 { - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params."))) - } - if a.Body != nil { - type Schema struct { - Type string `mapstructure:"type"` - Properties map[string]interface{} `mapstructure:"properties"` - Items interface{} `mapstructure:"items"` - } - var tmp Schema - err = mapstructure.Decode(a.Body, &tmp) - if err != nil { - return err - } - //switch a.ContentType { - //case "application/json": - switch tmp.Type { - case "object": - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPBody, mcp.Properties(tmp.Properties), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH."))) - case "array": - toolOptions = append(toolOptions, mcp.WithArray(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH."))) - } - //case "application/x-www-form-urlencoded": - // toolOptions = append(toolOptions, mcp.WithString(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH."))) - - } - tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...)) - } - mcp_server.SetSSEServer(sid, name, version, tools...) - return nil + return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content) } func (i *imlPublishModule) Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) { diff --git a/module/service/iml.go b/module/service/iml.go index af76622a..a524ed7a 100644 --- a/module/service/iml.go +++ b/module/service/iml.go @@ -14,12 +14,8 @@ import ( "github.com/APIParkLab/APIPark/common" - "github.com/mitchellh/mapstructure" - "github.com/eolinker/go-common/register" - "github.com/mark3labs/mcp-go/mcp" - mcp_server "github.com/APIParkLab/APIPark/mcp-server" "github.com/APIParkLab/APIPark/service/release" @@ -395,67 +391,11 @@ func (i *imlServiceModule) updateMCPServer(ctx context.Context, sid string, name if err != nil { return fmt.Errorf("get api doc commit error: %w", err) } - mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content)) - if err != nil { - return fmt.Errorf("convert mcp from openapi3 data error: %w", err) - } - tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis)) - for _, a := range mcpInfo.Apis { - toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2) - toolOptions = append(toolOptions, mcp.WithDescription(a.Description)) - headers := make(map[string]interface{}) - queries := make(map[string]interface{}) - path := make(map[string]interface{}) - for _, v := range a.Params { - p := map[string]interface{}{ - "type": "string", - "required": v.Required, - "description": v.Description, - } - switch v.In { - case "header": - headers[v.Name] = p - case "query": - queries[v.Name] = p - case "path": - path[v.Name] = p - } - } - if len(headers) > 0 { - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers."))) - } - if len(queries) > 0 { - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries."))) - } - if len(path) > 0 { - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params."))) - } - if a.Body != nil { - type Schema struct { - Type string `mapstructure:"type"` - Properties map[string]interface{} `mapstructure:"properties"` - Items interface{} `mapstructure:"items"` - } - var tmp Schema - err = mapstructure.Decode(a.Body, &tmp) - if err != nil { - return err - } - switch tmp.Type { - case "object": - toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPBody, mcp.Properties(tmp.Properties), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH."))) - case "array": - toolOptions = append(toolOptions, mcp.WithArray(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH."))) - } - } - tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...)) - } - mcp_server.SetSSEServer(sid, name, version, tools...) - return nil + return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content) } func (i *imlServiceModule) deleteMCPServer(ctx context.Context, sid string) { - mcp_server.DelSSEServer(sid) + mcp_server.DelServer(sid) } func (i *imlServiceModule) ExportAll(ctx context.Context) ([]*service_dto.ExportService, error) { diff --git a/plugins/openapi/mcp.go b/plugins/openapi/mcp.go index 4c9233de..0780d62c 100644 --- a/plugins/openapi/mcp.go +++ b/plugins/openapi/mcp.go @@ -15,15 +15,23 @@ func (p *plugin) mcpAPIs() []pm3.Api { globalMessagePath := fmt.Sprintf("/openapi/v1/%s/message", strings.Trim(mcp_server.GlobalBasePath, "/")) serviceMessagePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/message", strings.Trim(mcp_server.ServiceBasePath, "/")) serviceSSEPath := fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/")) + serviceStreamablePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/mcp", strings.Trim(mcp_server.ServiceBasePath, "/")) ignore.IgnorePath("openapi", http.MethodPost, globalMessagePath) - ignore.IgnorePath("openapi", http.MethodGet, serviceSSEPath) ignore.IgnorePath("openapi", http.MethodPost, serviceMessagePath) + ignore.IgnorePath("openapi", http.MethodGet, serviceStreamablePath) + ignore.IgnorePath("openapi", http.MethodPost, serviceStreamablePath) + ignore.IgnorePath("openapi", http.MethodDelete, serviceStreamablePath) return []pm3.Api{ pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/")), p.mcpController.GlobalHandleSSE), pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalHandleMessage), pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.ServiceHandleSSE), pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.ServiceHandleMessage), + + pm3.CreateApiSimple(http.MethodPost, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP), + pm3.CreateApiSimple(http.MethodDelete, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP), + pm3.CreateApiSimple(http.MethodGet, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP), } + } diff --git a/stores/api/model.go b/stores/api/model.go index b1eb2ded..21446b80 100644 --- a/stores/api/model.go +++ b/stores/api/model.go @@ -59,7 +59,7 @@ type Doc struct { Content string `gorm:"type:longtext;null;column:content;comment:文档内容"` Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` - APICount int64 `gorm:"type:int(11);not null;column:api_count;comment:接口数量"` + APICount int64 `gorm:"type:int(11);not null;column:api_count;default:0;comment:接口数量"` } func (i *Doc) TableName() string {