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 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
@@ -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
+ }