From 0525c51a8f8706178d42da657ca38f5c629d3209 Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Mon, 21 Jul 2025 16:44:11 +0800 Subject: [PATCH] Consumer MCP completed --- controller/mcp/iml.go | 254 +++++++----------- controller/mcp/mcp.go | 10 +- controller/mcp/tool.go | 104 +++++++ controller/system-apikey/apikey.go | 2 +- controller/system-apikey/iml.go | 10 +- mcp-server/server.go | 1 + .../authorization.go | 3 +- module/application-authorization/iml.go | 22 +- module/mcp/iml.go | 182 ++++++------- module/mcp/module.go | 6 - module/system-apikey/iml.go | 40 +++ module/system-apikey/module.go | 1 + plugins/core/mcp.go | 7 + plugins/core/system.go | 2 +- plugins/openapi/mcp.go | 20 +- resources/access/access.yaml | 11 + resources/access/role.yaml | 7 +- service/universally/get-softdelete.go | 18 +- service/universally/get.go | 12 +- 19 files changed, 425 insertions(+), 287 deletions(-) create mode 100644 controller/mcp/tool.go diff --git a/controller/mcp/iml.go b/controller/mcp/iml.go index bd1871ad..aa27cfe1 100644 --- a/controller/mcp/iml.go +++ b/controller/mcp/iml.go @@ -6,6 +6,8 @@ import ( "strings" "sync" + "github.com/APIParkLab/APIPark/module/service" + application_authorization "github.com/APIParkLab/APIPark/module/application-authorization" mcp_server "github.com/APIParkLab/APIPark/mcp-server" @@ -13,7 +15,6 @@ import ( "github.com/APIParkLab/APIPark/module/system" "github.com/eolinker/go-common/utils" "github.com/gin-gonic/gin" - mcp2 "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) @@ -22,12 +23,80 @@ var _ IMcpController = (*imlMcpController)(nil) type imlMcpController struct { settingModule system.ISettingModule `autowired:""` authorizationModule application_authorization.IAuthorizationModule `autowired:""` + appModule service.IAppModule `autowired:""` mcpModule mcp.IMcpModule `autowired:""` sessionKeys sync.Map server map[string]http.Handler openServer http.Handler } +func (i *imlMcpController) AppMCPHandle(ctx *gin.Context) { + appId := ctx.Param("app") + if appId == "" { + ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "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(), "app", appId)) + paths := strings.Split(req.URL.Path, "/") + req.URL.Path = fmt.Sprintf("/api/v1/%s/%s", mcp_server.GlobalBasePath, paths[len(paths)-1]) + locale := utils.I18n(ctx) + if v, ok := i.server[locale]; ok { + v.ServeHTTP(ctx.Writer, req) + return + } + + i.server[languageEnUs].ServeHTTP(ctx.Writer, req) +} + +func (i *imlMcpController) AppHandleSSE(ctx *gin.Context) { + apikey := ctx.Request.URL.Query().Get("apikey") + appId := ctx.Param("app") + if appId == "" { + ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"}) + return + } + ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByApp(ctx, appId, 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 + } + + ctx.Request.URL.Path = fmt.Sprintf("/openapi/v1/%s/sse", mcp_server.GlobalBasePath) + i.handleSSE(ctx, i.openServer, SessionInfo{ + Apikey: apikey, + App: appId, + }) +} + +func (i *imlMcpController) AppHandleMessage(ctx *gin.Context) { + appId := ctx.Param("app") + if appId == "" { + ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"}) + return + } + ctx.Request.URL.Path = fmt.Sprintf("/openapi/v1/%s/message", mcp_server.GlobalBasePath) + ctx.Request = ctx.Request.WithContext(utils.SetLabel(ctx.Request.Context(), "app", appId)) + i.handleMessage(ctx, i.openServer) +} + +func (i *imlMcpController) AppMCPConfig(ctx *gin.Context, appId string) (string, error) { + cfg := i.settingModule.Get(ctx) + if cfg.SitePrefix == "" { + return "", fmt.Errorf("site prefix is empty") + } + appInfo, err := i.appModule.GetApp(ctx, appId) + if err != nil { + return "", fmt.Errorf("get app info error: %v", err) + } + return fmt.Sprintf(mcpDefaultConfig, appInfo.Name, fmt.Sprintf("%s/openapi/v1/mcp/app/%s/sse?apikey={your_api_key}", strings.TrimSuffix(cfg.SitePrefix, "/"), appId)), nil +} + var mcpDefaultConfig = `{ "mcpServers": { "%s": { @@ -45,152 +114,18 @@ func (i *imlMcpController) GlobalMCPConfig(ctx *gin.Context) (string, error) { return fmt.Sprintf(mcpDefaultConfig, "APIPark-MCP-Server", fmt.Sprintf("%s/openapi/v1/%s/sse?apikey={your_api_key}", strings.TrimSuffix(cfg.SitePrefix, "/"), mcp_server.GlobalBasePath)), nil } -func (i *imlMcpController) generateZhCNMCPServer() *server.MCPServer { - s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging()) - s.AddTool( - mcp2.NewTool( - "service_list", - mcp2.WithDescription("此工具用于获取 APIPark 中已注册服务的列表。每个服务包含其唯一标识(service ID)、名称、描述及包含的 API 列表等关键信息。支持通过关键词进行模糊搜索,以便快速缩小查找范围。在获得某个服务的 ID 后,可以调用 openapi_document 工具来获取该服务的 OpenAPI 文档,以便后续调用其提供的 API 接口。"), - mcp2.WithString("keyword", mcp2.Description("关键词,用于模糊搜索服务")), - ), - i.mcpModule.Services, - ) - s.AddTool( - mcp2.NewTool( - "openapi_document", - mcp2.WithDescription("此工具用于获取指定服务的 OpenAPI 接口文档。返回内容支持 OpenAPI v3 与 v2 两种规范格式。通过传入服务 ID,可以查看该服务的所有 API 定义、参数结构、请求方式等详细信息,为后续构造请求做准备。"), - mcp2.WithString("service", mcp2.Description("服务的唯一标识 ID")), - ), - i.mcpModule.APIs, - ) - s.AddTool( - mcp2.NewTool( - "invoke_api", - mcp2.WithDescription("此工具用于直接调用指定的 API 接口。调用前需根据该接口的 OpenAPI 文档构造必要的请求参数,如请求路径、方法、查询参数、请求头、请求体等。调用过程中无需传递认证信息,例如请求头中的 Authorization 字段不需要提供。"), - mcp2.WithString("path", mcp2.Description("API 请求路径"), mcp2.Required()), - mcp2.WithString("method", mcp2.Description("API 请求方法,例如 GET、POST、PUT"), mcp2.Required()), - mcp2.WithString("content-type", mcp2.Description("请求的 Content-Type 类型。如果方法为 POST、PUT 或 PATCH,则必须指定该字段。")), - mcp2.WithObject("query", mcp2.Description("请求的查询参数,类型为 map[string]string")), - mcp2.WithObject("header", mcp2.Description("请求的头部参数,类型为 map[string]string")), - mcp2.WithString("body", mcp2.Description("请求体内容,通常为 JSON 字符串")), - ), - i.mcpModule.Invoke, - ) - return s -} - -func (i *imlMcpController) generateZhTWMCPServer() *server.MCPServer { - s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging()) - s.AddTool( - mcp2.NewTool( - "service_list", - mcp2.WithDescription("此工具用於獲取 APIPark 中已註冊服務的清單。每個服務包含其唯一識別碼(service ID)、名稱、描述以及該服務所包含的 API 列表。支援關鍵字模糊搜尋,可快速縮小查詢範圍。獲取到服務 ID 後,可使用 openapi_document 工具來查詢該服務對應的 OpenAPI 文件,為後續 API 呼叫做準備。"), - mcp2.WithString("keyword", mcp2.Description("關鍵字,用於模糊搜尋服務")), - ), - i.mcpModule.Services, - ) - s.AddTool( - mcp2.NewTool( - "openapi_document", - mcp2.WithDescription("此工具用於查詢指定服務的 OpenAPI 文件。返回的格式支援 OpenAPI v3 與 v2 標準。透過輸入服務 ID,可查閱該服務所有 API 的定義、參數結構、請求方式等細節,有助於後續構造 API 呼叫請求。"), - mcp2.WithString("service", mcp2.Description("欲查詢的服務唯一識別碼")), - ), - i.mcpModule.APIs, - ) - s.AddTool( - mcp2.NewTool( - "invoke_api", - mcp2.WithDescription("此工具可直接發送 API 請求。在呼叫此工具之前,需根據該 API 的 OpenAPI 文件構造所需的請求參數,如請求路徑、方法、查詢參數、標頭、主體等。使用此工具時不需傳送任何認證資訊,例如 Authorization 標頭可省略。"), - mcp2.WithString("path", mcp2.Description("API 的請求路徑"), mcp2.Required()), - mcp2.WithString("method", mcp2.Description("API 的請求方法,例如 GET、POST、PUT"), mcp2.Required()), - mcp2.WithString("content-type", mcp2.Description("請求的 Content-Type。若方法為 POST、PUT 或 PATCH,則必須指定")), - mcp2.WithObject("query", mcp2.Description("請求的查詢參數,類型為 map[string]string")), - mcp2.WithObject("header", mcp2.Description("請求的標頭,類型為 map[string]string")), - mcp2.WithString("body", mcp2.Description("請求主體內容,通常為 JSON 字串")), - ), - i.mcpModule.Invoke, - ) - return s -} - -func (i *imlMcpController) generateEnMCPServer() *server.MCPServer { - s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging()) - s.AddTool( - mcp2.NewTool( - "service_list", - mcp2.WithDescription("This tool can retrieve a list of registered services on APIPark, including key information such as service ID, name, description, and API list within the service. Support keyword search to quickly narrow down the search scope. After obtaining the service ID, you can use this ID to call the tool openapi_document to obtain the openapi document of the service for the corresponding service, preparing for subsequent API calls."), - mcp2.WithString("keyword", mcp2.Description("Keyword for fuzzy search")), - ), - i.mcpModule.Services, - ) - s.AddTool( - mcp2.NewTool( - "openapi_document", - mcp2.WithDescription("This tool returns the openAPI documentation for the corresponding service. The format supports the specifications of OpenAPI v3 and OpenAPI v2."), - mcp2.WithString("service", mcp2.Description("Service ID")), - ), - i.mcpModule.APIs, - ) - s.AddTool( - mcp2.NewTool( - "invoke_api", - mcp2.WithDescription("This tool can directly make API calls. Before calling this tool, it is necessary to construct relevant parameters based on the corresponding API's openAPI documentation, including query, header, body, method, path, and other parameters. By using this tool, no authentication related information needs to be transmitted, that is, no request header Authorization needs to be transmitted."), - mcp2.WithString("path", mcp2.Description("API path"), mcp2.Required()), - mcp2.WithString("method", mcp2.Description("API method"), mcp2.Required()), - mcp2.WithString("content-type", mcp2.Description("API Request Content-Type. If method is POST,PUT,PATCH, it must be set. If not set, it will be ignored.")), - mcp2.WithObject("query", mcp2.Description("API Request query,param type is map[string]string")), - mcp2.WithObject("header", mcp2.Description("API Request header,param type is map[string]string")), - mcp2.WithString("body", mcp2.Description("API Request body")), - ), - i.mcpModule.Invoke, - ) - return s -} - -func (i *imlMcpController) generateJPMCPServer() *server.MCPServer { - s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging()) - s.AddTool( - mcp2.NewTool( - "service_list", - mcp2.WithDescription("このツールは、APIPark に登録されているサービスの一覧を取得するためのものです。各サービスには、サービスID、名称、説明、およびそのサービスに含まれるAPI一覧といった重要な情報が含まれます。キーワードによるあいまい検索が可能で、目的のサービスを素早く絞り込むことができます。取得したサービスIDを使用して openapi_document ツールを呼び出すことで、そのサービスの OpenAPI ドキュメントを取得でき、APIの利用準備が整います。"), - mcp2.WithString("keyword", mcp2.Description("キーワード。サービスをあいまい検索するための文字列")), - ), - i.mcpModule.Services, - ) - s.AddTool( - mcp2.NewTool( - "openapi_document", - mcp2.WithDescription("指定されたサービスの OpenAPI ドキュメントを取得するためのツールです。OpenAPI v3 および v2 のフォーマットに対応しています。このドキュメントを使用することで、APIのエンドポイント、リクエスト方法、パラメータなどの詳細を確認でき、API呼び出しの準備に役立ちます。"), - mcp2.WithString("service", mcp2.Description("対象のサービスID")), - ), - i.mcpModule.APIs, - ) - s.AddTool( - mcp2.NewTool( - "invoke_api", - mcp2.WithDescription("このツールは、指定された API を直接呼び出すためのものです。呼び出し前に、OpenAPI ドキュメントに基づいて必要なパラメータ(パス、メソッド、クエリ、ヘッダー、ボディなど)を構築する必要があります。呼び出し時に認証情報(例:Authorization ヘッダー)を送信する必要はありません。"), - mcp2.WithString("path", mcp2.Description("API のリクエストパス"), mcp2.Required()), - mcp2.WithString("method", mcp2.Description("HTTPメソッド(GET、POST、PUTなど)。"), mcp2.Required()), - mcp2.WithString("content-type", mcp2.Description("リクエストの Content-Type。メソッドが POST、PUT、PATCH の場合に必須。")), - mcp2.WithObject("query", mcp2.Description("リクエストのクエリパラメータ。型は map[string]string")), - mcp2.WithObject("header", mcp2.Description("リクエストヘッダー。型は map[string]string")), - mcp2.WithString("body", mcp2.Description("リクエストボディ。通常はJSON文字列")), - ), - i.mcpModule.Invoke, - ) - return s -} - func (i *imlMcpController) OnComplete() { i.server = make(map[string]http.Handler) - enSer := i.generateEnMCPServer() - i.server["en-US"] = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath))) - i.server["zh-CN"] = server.NewSSEServer(i.generateZhCNMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath))) - i.server["zh-TW"] = server.NewSSEServer(i.generateZhTWMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath))) - 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, "/")))) - + for language, tools := range mcpToolsByLanguage { + s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging()) + s.AddTool(tools[ToolServiceList], i.mcpModule.Services) + s.AddTool(tools[ToolOpenAPIDocument], i.mcpModule.APIs) + s.AddTool(tools[ToolInvokeAPI], i.mcpModule.Invoke) + i.server[language] = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath))) + if language == languageEnUs { + i.openServer = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/")))) + } + } } func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) { @@ -201,15 +136,17 @@ func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) { v.ServeHTTP(ctx.Writer, req) return } - i.server["en-US"].ServeHTTP(ctx.Writer, req) + i.server[languageEnUs].ServeHTTP(ctx.Writer, req) } func (i *imlMcpController) GlobalHandleSSE(ctx *gin.Context) { apikey := ctx.Request.URL.Query().Get("apikey") - i.handleSSE(ctx, i.openServer, apikey) + i.handleSSE(ctx, i.openServer, SessionInfo{ + Apikey: apikey, + }) } -func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apikey string) { +func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, sIn SessionInfo) { writer := &ResponseWriter{ Writer: ctx.Writer, @@ -223,7 +160,7 @@ func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apik if !ok { return } - i.sessionKeys.Store(sessionId, apikey) + i.sessionKeys.Store(sessionId, sIn) }() server.ServeHTTP(writer, ctx.Request) i.sessionKeys.Delete(sessionId) @@ -247,7 +184,7 @@ func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) { ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"}) return } - ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey) + ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByService(ctx, serviceId, apikey) if err != nil { ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"}) return @@ -257,7 +194,9 @@ func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) { return } - i.handleSSE(ctx, mcp_server.DefaultMCPServer(), apikey) + i.handleSSE(ctx, mcp_server.DefaultMCPServer(), SessionInfo{ + Apikey: apikey, + }) } func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) { @@ -271,7 +210,7 @@ func (i *imlMcpController) ServiceHandleStreamHTTP(ctx *gin.Context) { ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"}) return } - ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey) + ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByService(ctx, serviceId, apikey) if err != nil { ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"}) return @@ -288,13 +227,20 @@ func (i *imlMcpController) ServiceHandleStreamHTTP(ctx *gin.Context) { func (i *imlMcpController) handleMessage(ctx *gin.Context, server http.Handler) { sessionId := ctx.Request.URL.Query().Get("sessionId") - apikey, ok := i.sessionKeys.Load(sessionId) + params, ok := i.sessionKeys.Load(sessionId) if !ok { ctx.String(403, "sessionId not found") return } + ps, ok := params.(SessionInfo) 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.(string))) + req = req.WithContext(utils.SetLabel(req.Context(), "apikey", ps.Apikey)) + req = req.WithContext(utils.SetLabel(req.Context(), "app", ps.App)) server.ServeHTTP(ctx.Writer, req) } + +type SessionInfo struct { + Apikey string + App string +} diff --git a/controller/mcp/mcp.go b/controller/mcp/mcp.go index fd66e263..a2064ae7 100644 --- a/controller/mcp/mcp.go +++ b/controller/mcp/mcp.go @@ -9,12 +9,20 @@ import ( type IMcpController interface { MCPHandle(ctx *gin.Context) + GlobalMCPHandle(ctx *gin.Context) GlobalHandleSSE(ctx *gin.Context) GlobalHandleMessage(ctx *gin.Context) + GlobalMCPConfig(ctx *gin.Context) (string, error) + + AppMCPHandle(ctx *gin.Context) + AppHandleSSE(ctx *gin.Context) + AppHandleMessage(ctx *gin.Context) + AppMCPConfig(ctx *gin.Context, appId string) (string, error) + ServiceHandleSSE(ctx *gin.Context) ServiceHandleMessage(ctx *gin.Context) - GlobalMCPConfig(ctx *gin.Context) (string, error) + ServiceHandleStreamHTTP(ctx *gin.Context) } diff --git a/controller/mcp/tool.go b/controller/mcp/tool.go new file mode 100644 index 00000000..7f729e82 --- /dev/null +++ b/controller/mcp/tool.go @@ -0,0 +1,104 @@ +package mcp + +import "github.com/mark3labs/mcp-go/mcp" + +const ( + ToolServiceList = "service_list" + ToolOpenAPIDocument = "openapi_document" + ToolInvokeAPI = "invoke_api" + + languageZhCN = "zh-CN" + languageZhTW = "zh-TW" + languageEnUs = "en-US" + languageJaJp = "ja-JP" +) + +var mcpToolsByLanguage = map[string]map[string]mcp.Tool{ + languageZhCN: toolsZhCN, + languageZhTW: toolsZhTW, + languageEnUs: toolsEnUs, + languageJaJp: toolsJaJp, +} + +var toolsZhTW = map[string]mcp.Tool{ + ToolServiceList: mcp.NewTool( + ToolServiceList, + mcp.WithDescription("此工具用於獲取 APIPark 中已註冊服務的列表。每個服務包含其唯一標識(service ID)、名稱、描述及包含的 API 列表等關鍵信息。支持通過關鍵詞進行模糊搜索,以便快速縮小查找範圍。在獲得某個服務的 ID 後,可以調用 openapi_document 工具來獲取該服務的 OpenAPI 文檔,以便後續調用其提供的 API 接口。"), + mcp.WithString("keyword", mcp.Description("關鍵詞,用於模糊搜索服務"))), + ToolOpenAPIDocument: mcp.NewTool( + ToolOpenAPIDocument, + mcp.WithDescription("此工具用於獲取 APIPark 中服務的 OpenAPI 文檔。"), + mcp.WithString("service_id", mcp.Description("服務 ID"))), + ToolInvokeAPI: mcp.NewTool( + ToolInvokeAPI, + mcp.WithDescription("此工具可直接發送 API 請求。在調用此工具之前,需要根據該 API 的 OpenAPI 文檔構造所需的請求參數,如請求路徑、方法、查詢參數、頭部信息、主體內容等。使用此工具時不需要傳遞任何認證信息,例如 Authorization 頭部可以省略。"), + mcp.WithString("path", mcp.Description("API 的請求路徑"), mcp.Required()), + mcp.WithString("method", mcp.Description("API 的請求方法,例如 GET、POST、PUT"), mcp.Required()), + mcp.WithString("content-type", mcp.Description("請求的 Content-Type。若方法為 POST、PUT 或 PATCH,則必須指定")), + mcp.WithObject("query", mcp.Description("請求的查詢參數,類型為 map[string]string")), + mcp.WithObject("header", mcp.Description("請求的頭部,類型為 map[string]string")), + mcp.WithString("body", mcp.Description("請求主體內容,通常為 JSON 字符串")), + ), +} + +var toolsZhCN = map[string]mcp.Tool{ + ToolServiceList: mcp.NewTool( + ToolServiceList, + mcp.WithDescription("此工具用于获取 APIPark 中已注册服务的列表。每个服务包含其唯一标识(service ID)、名称、描述及包含的 API 列表等关键信息。支持通过关键词进行模糊搜索,以便快速缩小查找范围。在获得某个服务的 ID 后,可以调用 openapi_document 工具来获取该服务的 OpenAPI 文档,以便后续调用其提供的 API 接口。"), + mcp.WithString("keyword", mcp.Description("关键词,用于模糊搜索服务"))), + ToolOpenAPIDocument: mcp.NewTool( + ToolOpenAPIDocument, mcp.WithDescription("此工具用于查询指定服务的 OpenAPI 文档。返回的格式支持 OpenAPI v3 和 v2 标准。通过输入服务 ID,可以查看该服务所有 API 的定义、参数结构、请求方式等详细信息,为后续构造 API 调用请求做准备。"), + mcp.WithString("service", mcp.Description("欲查询的服务唯一标识")), + ), + ToolInvokeAPI: mcp.NewTool(ToolInvokeAPI, + mcp.WithDescription("此工具可直接发送 API 请求。在调用此工具之前,需要根据该 API 的 OpenAPI 文档构造所需的请求参数,如请求路径、方法、查询参数、头部信息、主体内容等。使用此工具时不需要传递任何认证信息,例如 Authorization 头部可以省略。"), + mcp.WithString("path", mcp.Description("API 的请求路径"), mcp.Required()), + mcp.WithString("method", mcp.Description("API 的请求方法,例如 GET、POST、PUT"), mcp.Required()), + mcp.WithString("content-type", mcp.Description("请求的 Content-Type。若方法为 POST、PUT 或 PATCH,则必须指定")), + mcp.WithObject("query", mcp.Description("请求的查询参数,类型为 map[string]string")), + mcp.WithObject("header", mcp.Description("请求的头部,类型为 map[string]string")), + mcp.WithString("body", mcp.Description("请求主体内容,通常为 JSON 字符串")), + ), +} + +var toolsEnUs = map[string]mcp.Tool{ + ToolServiceList: mcp.NewTool( + ToolServiceList, + mcp.WithDescription("This tool is used to retrieve a list of registered services in APIPark. Each service includes its unique identifier (service ID), name, description, and a list of APIs it contains. It supports fuzzy searching by keyword for quick narrowing down of results. After obtaining a service ID, you can use the openapi_document tool to get the OpenAPI documentation for that service, which is necessary for invoking its APIs."), + mcp.WithString("keyword", mcp.Description("Keyword for fuzzy search of services"))), + ToolOpenAPIDocument: mcp.NewTool( + ToolOpenAPIDocument, + mcp.WithDescription("This tool is used to query the OpenAPI documentation of a specified service. The returned format supports both OpenAPI v3 and v2 standards. By entering the service ID, you can view detailed information about all APIs of that service, including definitions, parameter structures, request methods, etc., which prepares you for subsequent API calls."), + mcp.WithString("service", mcp.Description("Unique identifier of the service to query"))), + ToolInvokeAPI: mcp.NewTool( + ToolInvokeAPI, + mcp.WithDescription("This tool can directly send API requests. Before using this tool, you need to construct the required request parameters based on the OpenAPI documentation of the API, such as request path, method, query parameters, header information, body content, etc. No authentication information like Authorization header is required when using this tool."), + mcp.WithString("path", mcp.Description("API request path"), mcp.Required()), + mcp.WithString("method", mcp.Description("API request method, e.g., GET, POST, PUT"), mcp.Required()), + mcp.WithString("content-type", mcp.Description("Content-Type of the request. Must be specified if method is POST, PUT, or PATCH")), + mcp.WithObject("query", mcp.Description("Query parameters of the request, type map[string]string")), + mcp.WithObject("header", mcp.Description("Header information of the request, type map[string]string")), + mcp.WithString("body", mcp.Description("Body content of the request, usually in JSON string")), + ), +} + +var toolsJaJp = map[string]mcp.Tool{ + ToolServiceList: mcp.NewTool( + ToolServiceList, + mcp.WithDescription("このツールは、APIParkに登録されているサービスのリストを取得するために使用されます。各サービスには、ユニークな識別子(サービスID)、名前、説明、および含まれるAPIのリストが含まれています。キーワードによるあいまい検索をサポートしており、結果を迅速に絞り込むことができます。サービスIDを取得した後は、openapi_documentツールを使用してそのサービスのOpenAPIドキュメントを取得し、そのAPIを呼び出す準備をします。"), + mcp.WithString("keyword", mcp.Description("サービスをあいまい検索するためのキーワード"))), + ToolOpenAPIDocument: mcp.NewTool( + ToolOpenAPIDocument, + mcp.WithDescription("このツールは、指定されたサービスのOpenAPIドキュメントを照会するために使用されます。返される形式は、OpenAPI v3およびv2標準の両方をサポートしています。サービスIDを入力することで、そのサービスのすべてのAPIに関する詳細情報(定義、パラメータ構造、リクエスト方法など)を表示し、後続のAPI呼び出しの準備をします。"), + mcp.WithString("service", mcp.Description("照会するサービスのユニークな識別子"))), + ToolInvokeAPI: mcp.NewTool( + ToolInvokeAPI, + mcp.WithDescription("このツールは、APIリクエストを直接送信できます。このツールを使用する前に、APIのOpenAPIドキュメントに基づいて、必要なリクエストパラメータ(リクエストパス、メソッド、クエリパラメータ、ヘッダー情報、ボディコンテンツなど)を構築する必要があります。このツールを使用する際には、Authorizationヘッダーなどの認証情報は必要ありません。"), + mcp.WithString("path", mcp.Description("APIのリクエストパス"), mcp.Required()), + mcp.WithString("method", mcp.Description("APIのリクエストメソッド(例:GET、POST、PUT)"), mcp.Required()), + mcp.WithString("content-type", mcp.Description("リクエストのContent-Type。メソッドがPOST、PUT、またはPATCHの場合は必須です")), + mcp.WithObject("query", mcp.Description("リクエストのクエリパラメータ、タイプはmap[string]string")), + mcp.WithObject("header", mcp.Description("リクエストのヘッダー、タイプはmap[string]string")), + mcp.WithString("body", mcp.Description("リクエストのボディコンテンツ、通常はJSON文字列")), + ), +} diff --git a/controller/system-apikey/apikey.go b/controller/system-apikey/apikey.go index e026f398..ea374fe6 100644 --- a/controller/system-apikey/apikey.go +++ b/controller/system-apikey/apikey.go @@ -16,7 +16,7 @@ type IAPIKeyController interface { Search(ctx *gin.Context, keyword string) ([]*system_apikey_dto.Item, error) SimpleList(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) - MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) + MyAPIKeysByService(ctx *gin.Context, serviceId string, appId string) ([]*system_apikey_dto.AuthorizationItem, error) } func init() { diff --git a/controller/system-apikey/iml.go b/controller/system-apikey/iml.go index 9a80907e..65e9aab7 100644 --- a/controller/system-apikey/iml.go +++ b/controller/system-apikey/iml.go @@ -12,8 +12,14 @@ type imlAPIKeyController struct { apikeyModule system_apikey.IAPIKeyModule `autowired:""` } -func (i *imlAPIKeyController) MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) { - return i.apikeyModule.MyAPIKeysByService(ctx, serviceId) +func (i *imlAPIKeyController) MyAPIKeysByService(ctx *gin.Context, serviceId string, appId string) ([]*system_apikey_dto.AuthorizationItem, error) { + if serviceId != "" { + return i.apikeyModule.MyAPIKeysByService(ctx, serviceId) + } + if appId != "" { + return i.apikeyModule.MyAPIKeysByApp(ctx, appId) + } + return nil, nil } func (i *imlAPIKeyController) MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) { diff --git a/mcp-server/server.go b/mcp-server/server.go index dd6c8a80..90859022 100644 --- a/mcp-server/server.go +++ b/mcp-server/server.go @@ -16,6 +16,7 @@ var ( mcpServer = NewServer() ServiceBasePath = "mcp/service" GlobalBasePath = "mcp/global" + AppBasePath = "mcp/app" ) func NewServer() *Server { diff --git a/module/application-authorization/authorization.go b/module/application-authorization/authorization.go index 315a5149..a9d5e131 100644 --- a/module/application-authorization/authorization.go +++ b/module/application-authorization/authorization.go @@ -33,7 +33,8 @@ type IAuthorizationModule interface { // Info 获取项目鉴权详情 Info(ctx context.Context, appId string, aid string) (*application_authorization_dto.Authorization, error) - CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error) + CheckAPIKeyAuthorizationByService(ctx context.Context, serviceId string, apikey string) (bool, error) + CheckAPIKeyAuthorizationByApp(ctx context.Context, appId string, apikey string) (bool, error) //ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error) } diff --git a/module/application-authorization/iml.go b/module/application-authorization/iml.go index 583b17ab..543d1d04 100644 --- a/module/application-authorization/iml.go +++ b/module/application-authorization/iml.go @@ -44,7 +44,27 @@ type imlAuthorizationModule struct { transaction store.ITransaction `autowired:""` } -func (i *imlAuthorizationModule) CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error) { +func (i *imlAuthorizationModule) CheckAPIKeyAuthorizationByApp(ctx context.Context, appId string, apikey string) (bool, error) { + authorizations, err := i.authorizationService.ListByApp(ctx, appId) + if err != nil { + return false, err + } + for _, a := range authorizations { + if a.Type != "apikey" { + continue + } + cfg := make(map[string]interface{}) + if a.Config != "" { + json.Unmarshal([]byte(a.Config), &cfg) + } + if cfg["apikey"] == apikey { + return true, nil + } + } + return false, nil +} + +func (i *imlAuthorizationModule) CheckAPIKeyAuthorizationByService(ctx context.Context, serviceId string, apikey string) (bool, error) { list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe) if err != nil { return false, err diff --git a/module/mcp/iml.go b/module/mcp/iml.go index a344ca33..23ad2a6e 100644 --- a/module/mcp/iml.go +++ b/module/mcp/iml.go @@ -46,18 +46,43 @@ type imlMcpModule struct { releaseService release.IReleaseService `autowired:""` } +func (i *imlMcpModule) subscribeServiceIds(ctx context.Context, appId string) ([]string, error) { + subscribes, err := i.subscriberService.SubscriptionsByApplication(ctx, appId) + if err != nil { + return nil, fmt.Errorf("get subscriber error: %w,app id is %s", err, appId) + } + serviceIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string { + return s.Service + }, func(s *subscribe.Subscribe) bool { + return s.ApplyStatus == subscribe.ApplyStatusSubscribe + }) + if len(serviceIds) == 0 { + return nil, fmt.Errorf("no subscriber found,app id is %s", appId) + } + return serviceIds, nil +} + func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + keyword, _ := req.GetArguments()["keyword"].(string) - list, err := i.serviceService.Search(ctx, keyword, map[string]interface{}{ + appId := utils.Label(ctx, "app") + condition := map[string]interface{}{ "as_server": true, - }, "update_at desc") + } + if appId != "" { + serviceIds, err := i.subscribeServiceIds(ctx, appId) + if err != nil { + return nil, fmt.Errorf("get subscriber service ids error: %w,app id is %s", err, appId) + } + condition["uuid"] = serviceIds + } + + 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, "", map[string]interface{}{ - "as_server": true, - }, "update_at desc") + list, err = i.serviceService.Search(ctx, "", condition, "update_at desc") if err != nil { return nil, fmt.Errorf("search service error: %w", err) } @@ -115,120 +140,61 @@ 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.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.GetArguments()["service"].(string) - serviceIds := make([]string, 0, 1) if serviceId == "" { - serviceIds = append(serviceIds, serviceId) + return nil, fmt.Errorf("service id is empty") } - serviceList, err := i.serviceService.ServiceList(ctx) + s, err := i.serviceService.Get(ctx, serviceId) if err != nil { - return nil, fmt.Errorf("get service list error: %w", err) + return nil, fmt.Errorf("get service error: %w,service id is %s", err, serviceId) + } + appId := utils.Label(ctx, "app") + if appId != "" { + subscribers, err := i.subscriberService.ListByApplication(ctx, serviceId, appId) + if err != nil { + return nil, fmt.Errorf("get subscriber error: %w,app id is %s", err, appId) + } + if len(subscribers) < 1 || subscribers[0].ApplyStatus != subscribe.ApplyStatusSubscribe { + return nil, fmt.Errorf("no subscriber found,app id is %s", appId) + } } - result := make([]*mcp_dto.ServiceAPI, 0, len(serviceList)) - for _, s := range serviceList { - serviceRelease, err := i.releaseService.GetRunning(ctx, s.Id) - if err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("get service release error: %w,service id is %s", err, s.Id) - } - continue + serviceRelease, err := i.releaseService.GetRunning(ctx, serviceId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("get service release error: %w,service id is %s", err, s.Id) } - _, _, apiDocRelease, _, _, err := i.releaseService.GetReleaseInfos(ctx, serviceRelease.UUID) - if err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("get service release info error: %w,service id is %s", err, s.Id) - } - continue + return nil, fmt.Errorf("no service found,service id is %s", serviceId) + } + _, _, apiDocRelease, _, _, err := i.releaseService.GetReleaseInfos(ctx, serviceRelease.UUID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("get service release info error: %w,service id is %s", err, s.Id) } - commit, err := i.apiDocService.GetDocCommit(ctx, apiDocRelease.Commit) - if err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("get api doc release error: %w,service id is %s", err, s.Id) - } - continue + return nil, fmt.Errorf("no service found,service id is %s", serviceId) + } + commit, err := i.apiDocService.GetDocCommit(ctx, apiDocRelease.Commit) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("get api doc release error: %w,service id is %s", err, s.Id) } - T, err := openapi3Loader.LoadFromData([]byte(commit.Data.Content)) - if err != nil { - return nil, fmt.Errorf("load openapi3 error: %w,service id is %s", err, s.Id) - } - result = append(result, &mcp_dto.ServiceAPI{ - ServiceID: s.Id, - ServiceName: s.Name, - APIDoc: T, - }) + return nil, fmt.Errorf("no service found,service id is %s", serviceId) + } + T, err := openapi3Loader.LoadFromData([]byte(commit.Data.Content)) + if err != nil { + return nil, fmt.Errorf("load openapi3 error: %w,service id is %s", err, s.Id) + } + + result := &mcp_dto.ServiceAPI{ + ServiceID: serviceId, + ServiceName: s.Name, + APIDoc: T, } 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{} ) @@ -309,8 +275,14 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc request.Header = headerParam request.Header.Set("Content-Type", contentType) apikey := utils.Label(ctx, "apikey") + if apikey != "" { - request.Header.Set("Authorization", utils.Md5(apikey)) + appId := utils.Label(ctx, "app") + if appId == "" { + request.Header.Set("Authorization", utils.Md5(apikey)) + } else { + request.Header.Set("Authorization", apikey) + } } resp, err := client.Do(request) diff --git a/module/mcp/module.go b/module/mcp/module.go index da538f53..698082e3 100644 --- a/module/mcp/module.go +++ b/module/mcp/module.go @@ -12,13 +12,7 @@ import ( 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) - // APIs 获取API列表 APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) - - // SubscriberAuthorizations 获取订阅者授权 - //SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) } diff --git a/module/system-apikey/iml.go b/module/system-apikey/iml.go index a96e30fb..826b13be 100644 --- a/module/system-apikey/iml.go +++ b/module/system-apikey/iml.go @@ -41,6 +41,46 @@ type imlAPIKeyModule struct { transaction store.ITransaction `autowired:""` } +func (i *imlAPIKeyModule) MyAPIKeysByApp(ctx context.Context, appId string) ([]*system_apikey_dto.AuthorizationItem, error) { + appInfo, err := i.serviceService.Get(ctx, appId) + if err != nil { + return nil, err + } + auths, err := i.applicationAuthorizationService.ListByApp(ctx, appId) + if err != nil { + return nil, err + } + result := make(map[string]*system_apikey_dto.AuthorizationItem) + for _, a := range auths { + if a.Type != "apikey" { + continue + } + + m := make(map[string]string) + json.Unmarshal([]byte(a.Config), &m) + if m["apikey"] == "" { + continue + } + if _, ok := result[appInfo.Id]; !ok { + result[appInfo.Id] = &system_apikey_dto.AuthorizationItem{ + Id: appInfo.Id, + Name: appInfo.Name, + Apikeys: []system_apikey_dto.SimpleItem{}, + } + } + result[appInfo.Id].Apikeys = append(result[appInfo.Id].Apikeys, system_apikey_dto.SimpleItem{ + Id: a.UUID, + Name: a.Name, + Value: m["apikey"], + Expired: a.ExpireTime, + }) + + } + return utils.MapToSlice(result, func(k string, t *system_apikey_dto.AuthorizationItem) *system_apikey_dto.AuthorizationItem { + return t + }), nil +} + func (i *imlAPIKeyModule) MyAPIKeysByService(ctx context.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) { list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe) if err != nil { diff --git a/module/system-apikey/module.go b/module/system-apikey/module.go index 4b76278e..b3626266 100644 --- a/module/system-apikey/module.go +++ b/module/system-apikey/module.go @@ -19,6 +19,7 @@ type IAPIKeyModule interface { SimpleList(ctx context.Context) ([]*system_apikey_dto.SimpleItem, error) MyAPIKeys(ctx context.Context) ([]*system_apikey_dto.SimpleItem, error) MyAPIKeysByService(ctx context.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) + MyAPIKeysByApp(ctx context.Context, appId string) ([]*system_apikey_dto.AuthorizationItem, error) } func init() { diff --git a/plugins/core/mcp.go b/plugins/core/mcp.go index 4e29c42e..e1837ade 100644 --- a/plugins/core/mcp.go +++ b/plugins/core/mcp.go @@ -15,15 +15,22 @@ func (p *plugin) mcpAPIs() []pm3.Api { serviceMessagePath := fmt.Sprintf("/api/v1/%s/:serviceId/message", mcp_server.ServiceBasePath) globalSSEPath := fmt.Sprintf("/api/v1/%s/sse", mcp_server.GlobalBasePath) globalMessagePath := fmt.Sprintf("/api/v1/%s/message", mcp_server.GlobalBasePath) + appSSEPath := fmt.Sprintf("/api/v1/%s/sse", mcp_server.AppBasePath) + appMessagePath := fmt.Sprintf("/api/v1/%s/message", mcp_server.AppBasePath) ignore.IgnorePath("login", http.MethodGet, serviceSSEPath) ignore.IgnorePath("login", http.MethodPost, serviceMessagePath) ignore.IgnorePath("login", http.MethodGet, globalSSEPath) ignore.IgnorePath("login", http.MethodPost, globalMessagePath) + ignore.IgnorePath("login", http.MethodGet, appSSEPath) + ignore.IgnorePath("login", http.MethodPost, appMessagePath) return []pm3.Api{ pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.MCPHandle), pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.MCPHandle), pm3.CreateApiSimple(http.MethodGet, globalSSEPath, p.mcpController.GlobalMCPHandle), pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalMCPHandle), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/global/mcp/config", []string{"context"}, []string{"config"}, p.mcpController.GlobalMCPConfig), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/mcp/config", []string{"context", "query:app"}, []string{"config"}, p.mcpController.AppMCPConfig), + pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/api/v1/%s/:app/sse", mcp_server.AppBasePath), p.mcpController.AppMCPHandle), + pm3.CreateApiSimple(http.MethodPost, fmt.Sprintf("/api/v1/%s/:app/message", mcp_server.AppBasePath), p.mcpController.AppMCPHandle), } } diff --git a/plugins/core/system.go b/plugins/core/system.go index dfef3aa1..3921d190 100644 --- a/plugins/core/system.go +++ b/plugins/core/system.go @@ -24,7 +24,7 @@ func (p *plugin) systemApikeyApis() []pm3.Api { pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/system/apikey", []string{"context", "query:apikey"}, nil, p.systemAPIKeyController.Delete), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/system/apikeys", []string{"context", "query:keyword"}, []string{"apikeys"}, p.systemAPIKeyController.Search), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/apikeys", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.MyAPIKeys), - pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/app/apikeys", []string{"context", "query:service"}, []string{"apps"}, p.systemAPIKeyController.MyAPIKeysByService), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/app/apikeys", []string{"context", "query:service", "query:app"}, []string{"apps"}, p.systemAPIKeyController.MyAPIKeysByService), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/system/apikeys", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.SimpleList), } } diff --git a/plugins/openapi/mcp.go b/plugins/openapi/mcp.go index 0780d62c..3c167efc 100644 --- a/plugins/openapi/mcp.go +++ b/plugins/openapi/mcp.go @@ -5,18 +5,26 @@ import ( "net/http" "strings" - "github.com/eolinker/go-common/ignore" - mcp_server "github.com/APIParkLab/APIPark/mcp-server" + "github.com/eolinker/go-common/ignore" "github.com/eolinker/go-common/pm3" ) func (p *plugin) mcpAPIs() []pm3.Api { + globalSSEPath := fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/")) 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, "/")) + + appSSEPath := fmt.Sprintf("/openapi/v1/%s/:app/sse", strings.Trim(mcp_server.AppBasePath, "/")) + appMessagePath := fmt.Sprintf("/openapi/v1/%s/:app/message", strings.Trim(mcp_server.AppBasePath, "/")) + serviceSSEPath := fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/")) + serviceMessagePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/message", 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, globalSSEPath) + ignore.IgnorePath("openapi", http.MethodPost, appMessagePath) + ignore.IgnorePath("openapi", http.MethodGet, appSSEPath) ignore.IgnorePath("openapi", http.MethodGet, serviceSSEPath) ignore.IgnorePath("openapi", http.MethodPost, serviceMessagePath) ignore.IgnorePath("openapi", http.MethodGet, serviceStreamablePath) @@ -24,8 +32,12 @@ func (p *plugin) mcpAPIs() []pm3.Api { 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.MethodGet, globalSSEPath, p.mcpController.GlobalHandleSSE), pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalHandleMessage), + + pm3.CreateApiSimple(http.MethodGet, appSSEPath, p.mcpController.AppHandleSSE), + pm3.CreateApiSimple(http.MethodPost, appMessagePath, p.mcpController.AppHandleMessage), + pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.ServiceHandleSSE), pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.ServiceHandleMessage), diff --git a/resources/access/access.yaml b/resources/access/access.yaml index 4beef30e..8cde8892 100644 --- a/resources/access/access.yaml +++ b/resources/access/access.yaml @@ -304,6 +304,17 @@ team: value: 'manager_subscribed_services' dependents: - team.consumer.subscription.manager_subscribed_services + - name: mcp + cname: MCP + value: 'mcp' + children: + - name: view + value: 'view' + guest_allow: true + - name: manager + value: 'manager' + dependents: + - team.consumer.mcp.view - name: authorization value: 'authorization' children: diff --git a/resources/access/role.yaml b/resources/access/role.yaml index 30714d52..64db7dcc 100644 --- a/resources/access/role.yaml +++ b/resources/access/role.yaml @@ -92,6 +92,8 @@ team: permits: - team.consumer.authorization.manager - team.consumer.authorization.view + - team.consumer.mcp.manager + - team.consumer.mcp.view - team.consumer.subscription.manager_subscribed_services - team.consumer.subscription.subscribe - team.consumer.subscription.view_subscribed_service @@ -165,6 +167,8 @@ team: permits: - team.consumer.authorization.manager - team.consumer.authorization.view + - team.consumer.mcp.manager + - team.consumer.mcp.view - team.consumer.subscription.manager_subscribed_services - team.consumer.subscription.subscribe - team.consumer.subscription.view_subscribed_service @@ -176,9 +180,10 @@ team: - name: consumer developer value: consumer_developer permits: - - team.consumer.application.manager - team.consumer.authorization.manager - team.consumer.authorization.view + - team.consumer.mcp.manager + - team.consumer.mcp.view - team.consumer.subscription.subscribe - team.consumer.subscription.view_subscribed_service - team.team.consumer.view diff --git a/service/universally/get-softdelete.go b/service/universally/get-softdelete.go index 2f83ab2b..35ebbf08 100644 --- a/service/universally/get-softdelete.go +++ b/service/universally/get-softdelete.go @@ -58,11 +58,12 @@ func (s *imlServiceGetSoftDelete[T, E]) List(ctx context.Context, uuids ...strin return utils.SliceToSlice(list, s.toModelHandler), nil } func (s *imlServiceGetSoftDelete[T, E]) Search(ctx context.Context, keyword string, condition map[string]interface{}, sortRule ...string) ([]*T, error) { - if condition == nil { - condition = make(map[string]interface{}) + w := make(map[string]interface{}) + for k, v := range condition { + w[k] = v } - condition[SoftDeleteField] = false - ps, err := s.store.Search(ctx, keyword, condition, sortRule...) + w[SoftDeleteField] = false + ps, err := s.store.Search(ctx, keyword, w, sortRule...) if err != nil { return nil, err } @@ -70,11 +71,12 @@ func (s *imlServiceGetSoftDelete[T, E]) Search(ctx context.Context, keyword stri } func (s *imlServiceGetSoftDelete[T, E]) SearchByPage(ctx context.Context, keyword string, condition map[string]interface{}, page int, pageSize int, sortRule ...string) ([]*T, int64, error) { - if condition == nil { - condition = make(map[string]interface{}) + w := make(map[string]interface{}) + for k, v := range condition { + w[k] = v } - condition[SoftDeleteField] = false - ps, total, err := s.store.SearchByPage(ctx, keyword, condition, page, pageSize, sortRule...) + w[SoftDeleteField] = false + ps, total, err := s.store.SearchByPage(ctx, keyword, w, page, pageSize, sortRule...) if err != nil { return nil, 0, err } diff --git a/service/universally/get.go b/service/universally/get.go index db417f1c..c732aa47 100644 --- a/service/universally/get.go +++ b/service/universally/get.go @@ -71,7 +71,11 @@ func (s *imlServiceGet[T, E]) List(ctx context.Context, uuids ...string) ([]*T, return utils.SliceToSlice(list, s.toModelHandler), nil } func (s *imlServiceGet[T, E]) Search(ctx context.Context, keyword string, condition map[string]interface{}, sortRule ...string) ([]*T, error) { - ps, err := s.store.Search(ctx, keyword, condition, sortRule...) + w := make(map[string]interface{}) + for k, v := range condition { + w[k] = v + } + ps, err := s.store.Search(ctx, keyword, w, sortRule...) if err != nil { return nil, err } @@ -79,7 +83,11 @@ func (s *imlServiceGet[T, E]) Search(ctx context.Context, keyword string, condit } func (s *imlServiceGet[T, E]) SearchByPage(ctx context.Context, keyword string, condition map[string]interface{}, page int, pageSize int, sortRule ...string) ([]*T, int64, error) { - ps, total, err := s.store.SearchByPage(ctx, keyword, condition, page, pageSize, sortRule...) + w := make(map[string]interface{}) + for k, v := range condition { + w[k] = v + } + ps, total, err := s.store.SearchByPage(ctx, keyword, w, page, pageSize, sortRule...) if err != nil { return nil, 0, err }