Skip to content

Commit e7372cb

Browse files
authored
Merge pull request #291 from APIParkLab/feature/1.7-liujian
Feature/1.7 liujian
2 parents 37d421a + feac042 commit e7372cb

32 files changed

Lines changed: 465 additions & 84 deletions

File tree

controller/catalogue/iml.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package catalogue
33
import (
44
"github.com/APIParkLab/APIPark/module/catalogue"
55
catalogue_dto "github.com/APIParkLab/APIPark/module/catalogue/dto"
6+
"github.com/APIParkLab/APIPark/module/service"
67
"github.com/APIParkLab/APIPark/module/tag"
78
tag_dto "github.com/APIParkLab/APIPark/module/tag/dto"
89
"github.com/gin-gonic/gin"
@@ -14,6 +15,7 @@ var (
1415

1516
type imlCatalogueController struct {
1617
catalogueModule catalogue.ICatalogueModule `autowired:""`
18+
appModule service.IAppModule `autowired:""`
1719
tagModule tag.ITagModule `autowired:""`
1820
}
1921

@@ -26,7 +28,17 @@ func (i *imlCatalogueController) Subscribe(ctx *gin.Context, subscribeInfo *cata
2628
}
2729

2830
func (i *imlCatalogueController) ServiceDetail(ctx *gin.Context, sid string) (*catalogue_dto.ServiceDetail, error) {
29-
return i.catalogueModule.ServiceDetail(ctx, sid)
31+
detail, err := i.catalogueModule.ServiceDetail(ctx, sid)
32+
if err != nil {
33+
return nil, err
34+
}
35+
_, canSubscribe, err := i.appModule.SearchCanSubscribe(ctx, sid)
36+
if err != nil {
37+
return nil, err
38+
}
39+
detail.CanSubscribe = canSubscribe
40+
return detail, nil
41+
3042
}
3143

3244
func (i *imlCatalogueController) Search(ctx *gin.Context, keyword string) ([]*catalogue_dto.Item, []*tag_dto.Item, error) {

controller/mcp/iml.go

Lines changed: 150 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"strings"
77
"sync"
88

9+
application_authorization "github.com/APIParkLab/APIPark/module/application-authorization"
10+
911
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
1012
"github.com/APIParkLab/APIPark/module/mcp"
1113
"github.com/APIParkLab/APIPark/module/system"
@@ -18,11 +20,12 @@ import (
1820
var _ IMcpController = (*imlMcpController)(nil)
1921

2022
type imlMcpController struct {
21-
settingModule system.ISettingModule `autowired:""`
22-
mcpModule mcp.IMcpModule `autowired:""`
23-
sessionKeys sync.Map
24-
server http.Handler
25-
openServer http.Handler
23+
settingModule system.ISettingModule `autowired:""`
24+
authorizationModule application_authorization.IAuthorizationModule `autowired:""`
25+
mcpModule mcp.IMcpModule `autowired:""`
26+
sessionKeys sync.Map
27+
server map[string]http.Handler
28+
openServer http.Handler
2629
}
2730

2831
var mcpDefaultConfig = `{
@@ -42,7 +45,75 @@ func (i *imlMcpController) GlobalMCPConfig(ctx *gin.Context) (string, error) {
4245
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
4346
}
4447

45-
func (i *imlMcpController) OnComplete() {
48+
func (i *imlMcpController) generateZhCNMCPServer() *server.MCPServer {
49+
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
50+
s.AddTool(
51+
mcp2.NewTool(
52+
"service_list",
53+
mcp2.WithDescription("此工具用于获取 APIPark 中已注册服务的列表。每个服务包含其唯一标识(service ID)、名称、描述及包含的 API 列表等关键信息。支持通过关键词进行模糊搜索,以便快速缩小查找范围。在获得某个服务的 ID 后,可以调用 openapi_document 工具来获取该服务的 OpenAPI 文档,以便后续调用其提供的 API 接口。"),
54+
mcp2.WithString("keyword", mcp2.Description("关键词,用于模糊搜索服务")),
55+
),
56+
i.mcpModule.Services,
57+
)
58+
s.AddTool(
59+
mcp2.NewTool(
60+
"openapi_document",
61+
mcp2.WithDescription("此工具用于获取指定服务的 OpenAPI 接口文档。返回内容支持 OpenAPI v3 与 v2 两种规范格式。通过传入服务 ID,可以查看该服务的所有 API 定义、参数结构、请求方式等详细信息,为后续构造请求做准备。"),
62+
mcp2.WithString("service", mcp2.Description("服务的唯一标识 ID")),
63+
),
64+
i.mcpModule.APIs,
65+
)
66+
s.AddTool(
67+
mcp2.NewTool(
68+
"invoke_api",
69+
mcp2.WithDescription("此工具用于直接调用指定的 API 接口。调用前需根据该接口的 OpenAPI 文档构造必要的请求参数,如请求路径、方法、查询参数、请求头、请求体等。调用过程中无需传递认证信息,例如请求头中的 Authorization 字段不需要提供。"),
70+
mcp2.WithString("path", mcp2.Description("API 请求路径"), mcp2.Required()),
71+
mcp2.WithString("method", mcp2.Description("API 请求方法,例如 GET、POST、PUT"), mcp2.Required()),
72+
mcp2.WithString("content-type", mcp2.Description("请求的 Content-Type 类型。如果方法为 POST、PUT 或 PATCH,则必须指定该字段。")),
73+
mcp2.WithObject("query", mcp2.Description("请求的查询参数,类型为 map[string]string")),
74+
mcp2.WithObject("header", mcp2.Description("请求的头部参数,类型为 map[string]string")),
75+
mcp2.WithString("body", mcp2.Description("请求体内容,通常为 JSON 字符串")),
76+
),
77+
i.mcpModule.Invoke,
78+
)
79+
return s
80+
}
81+
82+
func (i *imlMcpController) generateZhTWMCPServer() *server.MCPServer {
83+
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
84+
s.AddTool(
85+
mcp2.NewTool(
86+
"service_list",
87+
mcp2.WithDescription("此工具用於獲取 APIPark 中已註冊服務的清單。每個服務包含其唯一識別碼(service ID)、名稱、描述以及該服務所包含的 API 列表。支援關鍵字模糊搜尋,可快速縮小查詢範圍。獲取到服務 ID 後,可使用 openapi_document 工具來查詢該服務對應的 OpenAPI 文件,為後續 API 呼叫做準備。"),
88+
mcp2.WithString("keyword", mcp2.Description("關鍵字,用於模糊搜尋服務")),
89+
),
90+
i.mcpModule.Services,
91+
)
92+
s.AddTool(
93+
mcp2.NewTool(
94+
"openapi_document",
95+
mcp2.WithDescription("此工具用於查詢指定服務的 OpenAPI 文件。返回的格式支援 OpenAPI v3 與 v2 標準。透過輸入服務 ID,可查閱該服務所有 API 的定義、參數結構、請求方式等細節,有助於後續構造 API 呼叫請求。"),
96+
mcp2.WithString("service", mcp2.Description("欲查詢的服務唯一識別碼")),
97+
),
98+
i.mcpModule.APIs,
99+
)
100+
s.AddTool(
101+
mcp2.NewTool(
102+
"invoke_api",
103+
mcp2.WithDescription("此工具可直接發送 API 請求。在呼叫此工具之前,需根據該 API 的 OpenAPI 文件構造所需的請求參數,如請求路徑、方法、查詢參數、標頭、主體等。使用此工具時不需傳送任何認證資訊,例如 Authorization 標頭可省略。"),
104+
mcp2.WithString("path", mcp2.Description("API 的請求路徑"), mcp2.Required()),
105+
mcp2.WithString("method", mcp2.Description("API 的請求方法,例如 GET、POST、PUT"), mcp2.Required()),
106+
mcp2.WithString("content-type", mcp2.Description("請求的 Content-Type。若方法為 POST、PUT 或 PATCH,則必須指定")),
107+
mcp2.WithObject("query", mcp2.Description("請求的查詢參數,類型為 map[string]string")),
108+
mcp2.WithObject("header", mcp2.Description("請求的標頭,類型為 map[string]string")),
109+
mcp2.WithString("body", mcp2.Description("請求主體內容,通常為 JSON 字串")),
110+
),
111+
i.mcpModule.Invoke,
112+
)
113+
return s
114+
}
115+
116+
func (i *imlMcpController) generateEnMCPServer() *server.MCPServer {
46117
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
47118
s.AddTool(
48119
mcp2.NewTool(
@@ -73,23 +144,72 @@ func (i *imlMcpController) OnComplete() {
73144
),
74145
i.mcpModule.Invoke,
75146
)
76-
i.server = server.NewSSEServer(s, server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
77-
i.openServer = server.NewSSEServer(s, server.WithBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
147+
return s
148+
}
149+
150+
func (i *imlMcpController) generateJPMCPServer() *server.MCPServer {
151+
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
152+
s.AddTool(
153+
mcp2.NewTool(
154+
"service_list",
155+
mcp2.WithDescription("このツールは、APIPark に登録されているサービスの一覧を取得するためのものです。各サービスには、サービスID、名称、説明、およびそのサービスに含まれるAPI一覧といった重要な情報が含まれます。キーワードによるあいまい検索が可能で、目的のサービスを素早く絞り込むことができます。取得したサービスIDを使用して openapi_document ツールを呼び出すことで、そのサービスの OpenAPI ドキュメントを取得でき、APIの利用準備が整います。"),
156+
mcp2.WithString("keyword", mcp2.Description("キーワード。サービスをあいまい検索するための文字列")),
157+
),
158+
i.mcpModule.Services,
159+
)
160+
s.AddTool(
161+
mcp2.NewTool(
162+
"openapi_document",
163+
mcp2.WithDescription("指定されたサービスの OpenAPI ドキュメントを取得するためのツールです。OpenAPI v3 および v2 のフォーマットに対応しています。このドキュメントを使用することで、APIのエンドポイント、リクエスト方法、パラメータなどの詳細を確認でき、API呼び出しの準備に役立ちます。"),
164+
mcp2.WithString("service", mcp2.Description("対象のサービスID")),
165+
),
166+
i.mcpModule.APIs,
167+
)
168+
s.AddTool(
169+
mcp2.NewTool(
170+
"invoke_api",
171+
mcp2.WithDescription("このツールは、指定された API を直接呼び出すためのものです。呼び出し前に、OpenAPI ドキュメントに基づいて必要なパラメータ(パス、メソッド、クエリ、ヘッダー、ボディなど)を構築する必要があります。呼び出し時に認証情報(例:Authorization ヘッダー)を送信する必要はありません。"),
172+
mcp2.WithString("path", mcp2.Description("API のリクエストパス"), mcp2.Required()),
173+
mcp2.WithString("method", mcp2.Description("HTTPメソッド(GET、POST、PUTなど)。"), mcp2.Required()),
174+
mcp2.WithString("content-type", mcp2.Description("リクエストの Content-Type。メソッドが POST、PUT、PATCH の場合に必須。")),
175+
mcp2.WithObject("query", mcp2.Description("リクエストのクエリパラメータ。型は map[string]string")),
176+
mcp2.WithObject("header", mcp2.Description("リクエストヘッダー。型は map[string]string")),
177+
mcp2.WithString("body", mcp2.Description("リクエストボディ。通常はJSON文字列")),
178+
),
179+
i.mcpModule.Invoke,
180+
)
181+
return s
182+
}
183+
184+
func (i *imlMcpController) OnComplete() {
185+
i.server = make(map[string]http.Handler)
186+
enSer := i.generateEnMCPServer()
187+
i.server["en-US"] = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
188+
i.server["zh-CN"] = server.NewSSEServer(i.generateZhCNMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
189+
i.server["zh-TW"] = server.NewSSEServer(i.generateZhTWMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
190+
i.server["ja-JP"] = server.NewSSEServer(i.generateJPMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
191+
192+
i.openServer = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
78193
}
79194

80195
func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
81196
cfg := i.settingModule.Get(ctx)
82197
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
83-
84-
i.server.ServeHTTP(ctx.Writer, req)
198+
locale := utils.I18n(ctx)
199+
if v, ok := i.server[locale]; ok {
200+
v.ServeHTTP(ctx.Writer, req)
201+
return
202+
}
203+
i.server["en-US"].ServeHTTP(ctx.Writer, req)
85204
}
86205

87206
func (i *imlMcpController) GlobalHandleSSE(ctx *gin.Context) {
88-
i.handleSSE(ctx, i.openServer)
207+
apikey := ctx.Request.URL.Query().Get("apikey")
208+
i.handleSSE(ctx, i.openServer, apikey)
89209
}
90210

91-
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler) {
92-
apikey := ctx.Request.URL.Query().Get("apikey")
211+
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apikey string) {
212+
93213
writer := &ResponseWriter{
94214
Writer: ctx.Writer,
95215
sessionId: make(chan string),
@@ -120,7 +240,23 @@ func (i *imlMcpController) MCPHandle(ctx *gin.Context) {
120240
}
121241

122242
func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) {
123-
i.handleSSE(ctx, mcp_server.DefaultMCPServer())
243+
apikey := ctx.Request.URL.Query().Get("apikey")
244+
serviceId := ctx.Param("serviceId")
245+
if serviceId == "" {
246+
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"})
247+
return
248+
}
249+
ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey)
250+
if err != nil {
251+
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
252+
return
253+
}
254+
if !ok {
255+
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"})
256+
return
257+
}
258+
259+
i.handleSSE(ctx, mcp_server.DefaultMCPServer(), apikey)
124260
}
125261

126262
func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) {

controller/service/iml.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,9 @@ type imlAppController struct {
542542
authModule application_authorization.IAuthorizationModule `autowired:""`
543543
}
544544

545-
func (i *imlAppController) SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SimpleAppItem, error) {
546-
return i.module.SearchCanSubscribe(ctx, serviceId)
545+
func (i *imlAppController) SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SubscribeAppItem, error) {
546+
items, _, err := i.module.SearchCanSubscribe(ctx, serviceId)
547+
return items, err
547548
}
548549

549550
func (i *imlAppController) Search(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) {

controller/service/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type IAppController interface {
4444
// SimpleApps 获取简易项目列表
4545
SimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
4646
MySimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
47-
SearchCanSubscribe(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
47+
SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SubscribeAppItem, error)
4848
GetApp(ctx *gin.Context, appId string) (*service_dto.App, error)
4949
DeleteApp(ctx *gin.Context, appId string) error
5050
}

controller/system-apikey/apikey.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type IAPIKeyController interface {
1616
Search(ctx *gin.Context, keyword string) ([]*system_apikey_dto.Item, error)
1717
SimpleList(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error)
1818
MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error)
19+
MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error)
1920
}
2021

2122
func init() {

controller/system-apikey/iml.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ type imlAPIKeyController struct {
1212
apikeyModule system_apikey.IAPIKeyModule `autowired:""`
1313
}
1414

15+
func (i *imlAPIKeyController) MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) {
16+
return i.apikeyModule.MyAPIKeysByService(ctx, serviceId)
17+
}
18+
1519
func (i *imlAPIKeyController) MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) {
1620
return i.apikeyModule.MyAPIKeys(ctx)
1721
}

mcp-server/tool.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,25 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
6464
body := ""
6565
for k, v := range request.Params.Arguments {
6666
if k == "Body" {
67-
tmp, _ := json.Marshal(v)
68-
body = string(tmp)
67+
switch a := v.(type) {
68+
case string:
69+
body = a
70+
case map[string]interface{}:
71+
switch t.contentType {
72+
case "application/json":
73+
tmp, _ := json.Marshal(a)
74+
body = string(tmp)
75+
case "application/x-www-form-urlencoded":
76+
bodyValue := url.Values{}
77+
for kk, vv := range a {
78+
bodyValue.Set(kk, fmt.Sprintf("%v", vv))
79+
}
80+
body = bodyValue.Encode()
81+
}
82+
default:
83+
tmp, _ := json.Marshal(a)
84+
body = string(tmp)
85+
}
6986
continue
7087
}
7188
tmp, ok := v.(map[string]interface{})

module/application-authorization/auth-driver/auth.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package auth_driver
33
import (
44
"encoding/json"
55
"fmt"
6-
6+
77
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
88
)
99

@@ -83,6 +83,6 @@ func generateStruct[T any](cfg interface{}) (*T, error) {
8383
return nil, err
8484
}
8585
}
86-
86+
8787
return result, nil
8888
}

module/application-authorization/auth-driver/oauth2/oauth2.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package oauth2
33
import (
44
"encoding/json"
55
"strconv"
6-
6+
77
auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver"
8-
8+
99
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
1010
)
1111

@@ -33,7 +33,7 @@ func (cfg *Config) ID() string {
3333
}
3434

3535
func (cfg *Config) Valid() ([]byte, error) {
36-
36+
3737
if cfg.HashSecret && !cfg.Hashed {
3838
// 未加密
3939
secret, err := hashSecret([]byte(cfg.ClientSecret), 0, 0, 0)
@@ -48,9 +48,9 @@ func (cfg *Config) Valid() ([]byte, error) {
4848
}
4949

5050
func (cfg *Config) Detail() []application_authorization_dto.DetailItem {
51-
51+
5252
redirectURLs, _ := json.Marshal(cfg.RedirectUrls)
53-
53+
5454
return []application_authorization_dto.DetailItem{
5555
{Key: "客户端ID", Value: cfg.ClientId},
5656
{Key: "客户端密钥", Value: cfg.ClientSecret},

module/application-authorization/authorization.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package application_authorization
22

33
import (
44
"context"
5-
"github.com/APIParkLab/APIPark/module/system"
65
"reflect"
76

7+
"github.com/APIParkLab/APIPark/module/system"
8+
89
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
910

1011
"github.com/APIParkLab/APIPark/gateway"
@@ -31,6 +32,9 @@ type IAuthorizationModule interface {
3132
Detail(ctx context.Context, appId string, aid string) ([]application_authorization_dto.DetailItem, error)
3233
// Info 获取项目鉴权详情
3334
Info(ctx context.Context, appId string, aid string) (*application_authorization_dto.Authorization, error)
35+
36+
CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error)
37+
3438
//ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error)
3539
}
3640

0 commit comments

Comments
 (0)