transn-yapi-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/tools.js +52 -0
- package/dist/tools.js.map +1 -0
- package/dist/yapi.js +61 -0
- package/dist/yapi.js.map +1 -0
- package/package.json +37 -0
- package/src/index.ts +20 -0
- package/src/tools.ts +56 -0
- package/src/types.d.ts +25 -0
- package/src/yapi.ts +119 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# transn-yapi-mcp
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that connects to a YAPI instance and exposes
|
|
4
|
+
its API definitions as tools for AI agents or MCP-compatible clients.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Query YAPI interface definitions by **interfaceId**.
|
|
9
|
+
- Query YAPI interface definitions by **path + projectId**.
|
|
10
|
+
- Return structured metadata that is easy to consume in tooling:
|
|
11
|
+
- name, path, method, description
|
|
12
|
+
- request query & body schema
|
|
13
|
+
- response schema
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install transn-yapi-mcp
|
|
19
|
+
# or
|
|
20
|
+
pnpm add transn-yapi-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
The package is designed to be run as an MCP server over **stdio**.
|
|
26
|
+
Typical MCP client configuration (example):
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"yapi-mcp": {
|
|
32
|
+
"type": "stdio",
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["transn-yapi-mcp"],
|
|
35
|
+
"env": {
|
|
36
|
+
"YAPI_BASE": "https://your-yapi-domain.com",
|
|
37
|
+
"YAPI_TOKEN": "your_yapi_openapi_token"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Required environment variables
|
|
45
|
+
|
|
46
|
+
- `YAPI_BASE`
|
|
47
|
+
Base URL of your YAPI instance, without a trailing slash.
|
|
48
|
+
Example: `https://yapi.example.com`
|
|
49
|
+
|
|
50
|
+
- `YAPI_TOKEN`
|
|
51
|
+
YAPI openapi token for the target project(s). It is passed as `token` query parameter.
|
|
52
|
+
|
|
53
|
+
## Exposed tools
|
|
54
|
+
|
|
55
|
+
### `yapi.get_api_context`
|
|
56
|
+
|
|
57
|
+
Get structured API context from a YAPI interface.
|
|
58
|
+
|
|
59
|
+
**Input**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
{
|
|
63
|
+
interfaceId: number; // required
|
|
64
|
+
projectId?: number; // optional, for disambiguation
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Output (shape, simplified)**
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
{
|
|
72
|
+
name: string;
|
|
73
|
+
path: string;
|
|
74
|
+
method: string;
|
|
75
|
+
description: string;
|
|
76
|
+
request: {
|
|
77
|
+
query: unknown;
|
|
78
|
+
body: unknown;
|
|
79
|
+
};
|
|
80
|
+
response: unknown;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `yapi.get_api_context_by_path`
|
|
85
|
+
|
|
86
|
+
Get API context by path within a YAPI project.
|
|
87
|
+
|
|
88
|
+
**Input**
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
{
|
|
92
|
+
projectId: number; // required
|
|
93
|
+
path: string; // required, e.g. "/busi/patient/detail_by_openid"
|
|
94
|
+
method?: string; // optional, e.g. "GET"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Output**
|
|
99
|
+
|
|
100
|
+
Same shape as `yapi.get_api_context`.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
105
|
+
|
|
106
|
+
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { tools } from "./tools.js";
|
|
5
|
+
const server = new McpServer({
|
|
6
|
+
name: "@transn/yapi-mcp",
|
|
7
|
+
version: "0.1.0"
|
|
8
|
+
});
|
|
9
|
+
// 注册工具
|
|
10
|
+
for (const tool of tools) {
|
|
11
|
+
server.registerTool(tool.name, {
|
|
12
|
+
description: tool.description,
|
|
13
|
+
inputSchema: tool.inputSchema
|
|
14
|
+
}, tool.handler);
|
|
15
|
+
}
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,kBAAkB;IACxB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,OAAO;AACP,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;IACzB,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;AACnB,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getInterface, getInterfaceByPath } from "./yapi.js";
|
|
3
|
+
export const tools = [
|
|
4
|
+
{
|
|
5
|
+
name: "yapi.get_api_context",
|
|
6
|
+
description: "Get structured API context from YAPI",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
projectId: z.number().optional(),
|
|
9
|
+
interfaceId: z.number()
|
|
10
|
+
}),
|
|
11
|
+
handler: async ({ projectId, interfaceId }) => {
|
|
12
|
+
const api = projectId
|
|
13
|
+
? await getInterface(projectId, interfaceId)
|
|
14
|
+
: await getInterface(interfaceId);
|
|
15
|
+
return {
|
|
16
|
+
name: api.title,
|
|
17
|
+
path: api.path,
|
|
18
|
+
method: api.method,
|
|
19
|
+
description: api.desc,
|
|
20
|
+
request: {
|
|
21
|
+
query: api.req_query,
|
|
22
|
+
body: api.req_body_other
|
|
23
|
+
},
|
|
24
|
+
response: api.res_body
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "yapi.get_api_context_by_path",
|
|
30
|
+
description: "Get API context by path within a YAPI project",
|
|
31
|
+
inputSchema: z.object({
|
|
32
|
+
projectId: z.number(),
|
|
33
|
+
path: z.string(),
|
|
34
|
+
method: z.string().optional()
|
|
35
|
+
}),
|
|
36
|
+
handler: async ({ projectId, path, method }) => {
|
|
37
|
+
const api = await getInterfaceByPath(projectId, path, method);
|
|
38
|
+
return {
|
|
39
|
+
name: api.title,
|
|
40
|
+
path: api.path,
|
|
41
|
+
method: api.method,
|
|
42
|
+
description: api.desc,
|
|
43
|
+
request: {
|
|
44
|
+
query: api.req_query,
|
|
45
|
+
body: api.req_body_other
|
|
46
|
+
},
|
|
47
|
+
response: api.res_body
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE7D,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB;QACE,IAAI,EAAE,sBAAsB;QAC5B,WAAW,EAAE,sCAAsC;QACnD,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YAChC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;SACxB,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,WAAW,EAA+C,EAAE,EAAE;YACzF,MAAM,GAAG,GAAG,SAAS;gBACnB,CAAC,CAAC,MAAM,YAAY,CAAC,SAAS,EAAE,WAAW,CAAC;gBAC5C,CAAC,CAAC,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;YAEpC,OAAO;gBACL,IAAI,EAAE,GAAG,CAAC,KAAK;gBACf,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,WAAW,EAAE,GAAG,CAAC,IAAI;gBACrB,OAAO,EAAE;oBACP,KAAK,EAAE,GAAG,CAAC,SAAS;oBACpB,IAAI,EAAE,GAAG,CAAC,cAAc;iBACzB;gBACD,QAAQ,EAAE,GAAG,CAAC,QAAQ;aACvB,CAAC;QACJ,CAAC;KACF;IACD;QACE,IAAI,EAAE,8BAA8B;QACpC,WAAW,EAAE,+CAA+C;QAC5D,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;YACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;YAChB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SAC9B,CAAC;QACF,OAAO,EAAE,KAAK,EACZ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAwD,EACjF,EAAE;YACF,MAAM,GAAG,GAAG,MAAM,kBAAkB,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;YAE9D,OAAO;gBACL,IAAI,EAAE,GAAG,CAAC,KAAK;gBACf,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,WAAW,EAAE,GAAG,CAAC,IAAI;gBACrB,OAAO,EAAE;oBACP,KAAK,EAAE,GAAG,CAAC,SAAS;oBACpB,IAAI,EAAE,GAAG,CAAC,cAAc;iBACzB;gBACD,QAAQ,EAAE,GAAG,CAAC,QAAQ;aACvB,CAAC;QACJ,CAAC;KACF;CACF,CAAC"}
|
package/dist/yapi.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
// YAPI 基础地址,末尾不要加斜杠,必填通过环境变量提供
|
|
3
|
+
const YAPI_BASE = process.env.YAPI_BASE;
|
|
4
|
+
// YAPI 的 openapi token(从环境变量读取,放在查询参数 token),避免依赖登录态 Cookie
|
|
5
|
+
const YAPI_TOKEN = process.env.YAPI_TOKEN;
|
|
6
|
+
export async function getInterface(projectIdOrInterfaceId, maybeInterfaceId) {
|
|
7
|
+
// 兼容两种调用:仅传接口 ID;或传 projectId + 接口 ID
|
|
8
|
+
const interfaceId = maybeInterfaceId ?? projectIdOrInterfaceId;
|
|
9
|
+
const projectId = maybeInterfaceId ? projectIdOrInterfaceId : undefined;
|
|
10
|
+
const params = new URLSearchParams({
|
|
11
|
+
id: String(interfaceId),
|
|
12
|
+
token: YAPI_TOKEN
|
|
13
|
+
});
|
|
14
|
+
if (projectId !== undefined) {
|
|
15
|
+
params.set("project_id", String(projectId));
|
|
16
|
+
}
|
|
17
|
+
const url = `${YAPI_BASE}/api/interface/get?${params.toString()}`;
|
|
18
|
+
const res = await fetch(url);
|
|
19
|
+
const json = await res.json();
|
|
20
|
+
if (json.errcode !== 0) {
|
|
21
|
+
throw new Error(json.errmsg);
|
|
22
|
+
}
|
|
23
|
+
return json.data;
|
|
24
|
+
}
|
|
25
|
+
async function listInterfaces(projectId) {
|
|
26
|
+
const pageSize = 200;
|
|
27
|
+
const result = [];
|
|
28
|
+
let page = 1;
|
|
29
|
+
// 分页拉取,直到达到总数或无更多数据
|
|
30
|
+
while (true) {
|
|
31
|
+
const url = `${YAPI_BASE}/api/interface/list?project_id=${projectId}&page=${page}&limit=${pageSize}&token=${YAPI_TOKEN}`;
|
|
32
|
+
const res = await fetch(url);
|
|
33
|
+
const json = await res.json();
|
|
34
|
+
if (json.errcode !== 0) {
|
|
35
|
+
throw new Error(json.errmsg);
|
|
36
|
+
}
|
|
37
|
+
result.push(...json.data.list);
|
|
38
|
+
if (result.length >= json.data.count || json.data.list.length === 0) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
page += 1;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
export async function getInterfaceByPath(projectId, path, method) {
|
|
46
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
47
|
+
const interfaces = await listInterfaces(projectId);
|
|
48
|
+
const matches = interfaces.filter((item) => item.path === normalizedPath);
|
|
49
|
+
const filtered = method
|
|
50
|
+
? matches.filter((item) => item.method.toUpperCase() === method.toUpperCase())
|
|
51
|
+
: matches;
|
|
52
|
+
const target = filtered[0];
|
|
53
|
+
if (!target) {
|
|
54
|
+
const reason = matches.length === 0
|
|
55
|
+
? `未找到路径为 ${normalizedPath} 的接口`
|
|
56
|
+
: `路径匹配到 ${matches.length} 条,但没有匹配到方法 ${method ?? "N/A"}`;
|
|
57
|
+
throw new Error(reason);
|
|
58
|
+
}
|
|
59
|
+
return getInterface(projectId, target._id);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=yapi.js.map
|
package/dist/yapi.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yapi.js","sourceRoot":"","sources":["../src/yapi.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "transn-yapi-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An MCP server that fetches structured API metadata from YAPI and exposes it as tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"transn-yapi-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"package.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "merrick",
|
|
24
|
+
"email": "merrick.hu@transn.com"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"node-fetch": "^3.3.2",
|
|
29
|
+
"zod": "^3.23.8"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.0.3",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"typescript": "^5.3.3"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { tools } from "./tools.js";
|
|
5
|
+
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "transn-yapi-mcp",
|
|
8
|
+
version: "0.1.0"
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// 注册工具
|
|
12
|
+
for (const tool of tools) {
|
|
13
|
+
server.registerTool(tool.name, {
|
|
14
|
+
description: tool.description,
|
|
15
|
+
inputSchema: tool.inputSchema
|
|
16
|
+
}, tool.handler);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const transport = new StdioServerTransport();
|
|
20
|
+
await server.connect(transport);
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getInterface, getInterfaceByPath } from "./yapi.js";
|
|
3
|
+
|
|
4
|
+
export const tools = [
|
|
5
|
+
{
|
|
6
|
+
name: "yapi.get_api_context",
|
|
7
|
+
description: "Get structured API context from YAPI",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
projectId: z.number().optional(),
|
|
10
|
+
interfaceId: z.number()
|
|
11
|
+
}),
|
|
12
|
+
handler: async ({ projectId, interfaceId }: { projectId?: number; interfaceId: number }) => {
|
|
13
|
+
const api = projectId
|
|
14
|
+
? await getInterface(projectId, interfaceId)
|
|
15
|
+
: await getInterface(interfaceId);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
name: api.title,
|
|
19
|
+
path: api.path,
|
|
20
|
+
method: api.method,
|
|
21
|
+
description: api.desc,
|
|
22
|
+
request: {
|
|
23
|
+
query: api.req_query,
|
|
24
|
+
body: api.req_body_other
|
|
25
|
+
},
|
|
26
|
+
response: api.res_body
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "yapi.get_api_context_by_path",
|
|
32
|
+
description: "Get API context by path within a YAPI project",
|
|
33
|
+
inputSchema: z.object({
|
|
34
|
+
projectId: z.number(),
|
|
35
|
+
path: z.string(),
|
|
36
|
+
method: z.string().optional()
|
|
37
|
+
}),
|
|
38
|
+
handler: async (
|
|
39
|
+
{ projectId, path, method }: { projectId: number; path: string; method?: string }
|
|
40
|
+
) => {
|
|
41
|
+
const api = await getInterfaceByPath(projectId, path, method);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
name: api.title,
|
|
45
|
+
path: api.path,
|
|
46
|
+
method: api.method,
|
|
47
|
+
description: api.desc,
|
|
48
|
+
request: {
|
|
49
|
+
query: api.req_query,
|
|
50
|
+
body: api.req_body_other
|
|
51
|
+
},
|
|
52
|
+
response: api.res_body
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
];
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 类型声明:为通配符导出路径提供类型支持
|
|
2
|
+
declare module "@modelcontextprotocol/sdk/server/mcp.js" {
|
|
3
|
+
// 使用相对路径从 node_modules 导入类型
|
|
4
|
+
export {
|
|
5
|
+
McpServer,
|
|
6
|
+
ResourceTemplate,
|
|
7
|
+
type RegisteredTool,
|
|
8
|
+
type RegisteredResource,
|
|
9
|
+
type RegisteredPrompt,
|
|
10
|
+
type ToolCallback,
|
|
11
|
+
type PromptCallback,
|
|
12
|
+
type ReadResourceCallback,
|
|
13
|
+
type ReadResourceTemplateCallback,
|
|
14
|
+
type ListResourcesCallback,
|
|
15
|
+
type CompleteResourceTemplateCallback,
|
|
16
|
+
type ResourceMetadata
|
|
17
|
+
} from "../node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare module "@modelcontextprotocol/sdk/server/stdio.js" {
|
|
21
|
+
export {
|
|
22
|
+
StdioServerTransport
|
|
23
|
+
} from "../node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js";
|
|
24
|
+
}
|
|
25
|
+
|
package/src/yapi.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
|
|
3
|
+
// YAPI 基础地址,末尾不要加斜杠,必填通过环境变量提供
|
|
4
|
+
const YAPI_BASE = process.env.YAPI_BASE!;
|
|
5
|
+
// YAPI 的 openapi token(从环境变量读取,放在查询参数 token),避免依赖登录态 Cookie
|
|
6
|
+
const YAPI_TOKEN = process.env.YAPI_TOKEN!;
|
|
7
|
+
|
|
8
|
+
interface YapiResponse<T = unknown> {
|
|
9
|
+
errcode: number;
|
|
10
|
+
errmsg: string;
|
|
11
|
+
data: T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface YapiInterface {
|
|
15
|
+
title: string;
|
|
16
|
+
path: string;
|
|
17
|
+
method: string;
|
|
18
|
+
desc: string;
|
|
19
|
+
req_query: unknown;
|
|
20
|
+
req_body_other: unknown;
|
|
21
|
+
res_body: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface YapiInterfaceListItem {
|
|
25
|
+
_id: number;
|
|
26
|
+
path: string;
|
|
27
|
+
method: string;
|
|
28
|
+
title: string;
|
|
29
|
+
catid: number;
|
|
30
|
+
project_id: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface YapiInterfaceList {
|
|
34
|
+
count: number;
|
|
35
|
+
list: YapiInterfaceListItem[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getInterface(
|
|
39
|
+
projectId: number,
|
|
40
|
+
interfaceId: number
|
|
41
|
+
): Promise<YapiInterface>;
|
|
42
|
+
export async function getInterface(
|
|
43
|
+
interfaceId: number
|
|
44
|
+
): Promise<YapiInterface>;
|
|
45
|
+
export async function getInterface(
|
|
46
|
+
projectIdOrInterfaceId: number,
|
|
47
|
+
maybeInterfaceId?: number
|
|
48
|
+
): Promise<YapiInterface> {
|
|
49
|
+
// 兼容两种调用:仅传接口 ID;或传 projectId + 接口 ID
|
|
50
|
+
const interfaceId = maybeInterfaceId ?? projectIdOrInterfaceId;
|
|
51
|
+
const projectId = maybeInterfaceId ? projectIdOrInterfaceId : undefined;
|
|
52
|
+
|
|
53
|
+
const params = new URLSearchParams({
|
|
54
|
+
id: String(interfaceId),
|
|
55
|
+
token: YAPI_TOKEN
|
|
56
|
+
});
|
|
57
|
+
if (projectId !== undefined) {
|
|
58
|
+
params.set("project_id", String(projectId));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const url = `${YAPI_BASE}/api/interface/get?${params.toString()}`;
|
|
62
|
+
const res = await fetch(url);
|
|
63
|
+
|
|
64
|
+
const json = await res.json() as YapiResponse<YapiInterface>;
|
|
65
|
+
if (json.errcode !== 0) {
|
|
66
|
+
throw new Error(json.errmsg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return json.data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function listInterfaces(projectId: number): Promise<YapiInterfaceListItem[]> {
|
|
73
|
+
const pageSize = 200;
|
|
74
|
+
const result: YapiInterfaceListItem[] = [];
|
|
75
|
+
let page = 1;
|
|
76
|
+
|
|
77
|
+
// 分页拉取,直到达到总数或无更多数据
|
|
78
|
+
while (true) {
|
|
79
|
+
const url = `${YAPI_BASE}/api/interface/list?project_id=${projectId}&page=${page}&limit=${pageSize}&token=${YAPI_TOKEN}`;
|
|
80
|
+
const res = await fetch(url);
|
|
81
|
+
const json = await res.json() as YapiResponse<YapiInterfaceList>;
|
|
82
|
+
if (json.errcode !== 0) {
|
|
83
|
+
throw new Error(json.errmsg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
result.push(...json.data.list);
|
|
87
|
+
if (result.length >= json.data.count || json.data.list.length === 0) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
page += 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function getInterfaceByPath(
|
|
98
|
+
projectId: number,
|
|
99
|
+
path: string,
|
|
100
|
+
method?: string
|
|
101
|
+
): Promise<YapiInterface> {
|
|
102
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
103
|
+
const interfaces = await listInterfaces(projectId);
|
|
104
|
+
|
|
105
|
+
const matches = interfaces.filter((item) => item.path === normalizedPath);
|
|
106
|
+
const filtered = method
|
|
107
|
+
? matches.filter((item) => item.method.toUpperCase() === method.toUpperCase())
|
|
108
|
+
: matches;
|
|
109
|
+
|
|
110
|
+
const target = filtered[0];
|
|
111
|
+
if (!target) {
|
|
112
|
+
const reason = matches.length === 0
|
|
113
|
+
? `未找到路径为 ${normalizedPath} 的接口`
|
|
114
|
+
: `路径匹配到 ${matches.length} 条,但没有匹配到方法 ${method ?? "N/A"}`;
|
|
115
|
+
throw new Error(reason);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return getInterface(projectId, target._id);
|
|
119
|
+
}
|