@wllcyg001/yapi-mcp 0.1.0 → 0.1.8
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 +116 -116
- package/package.json +39 -28
- package/.github/workflows/ci.yml +0 -26
- package/.github/workflows/release.yml +0 -32
- package/YApi-MCP-Server-PRD.md +0 -111
- package/src/config.ts +0 -115
- package/src/errors.ts +0 -29
- package/src/index.ts +0 -254
- package/src/types.ts +0 -42
- package/src/yapi.ts +0 -192
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
# @wllcyg001/yapi-mcp
|
|
2
|
-
|
|
3
|
-
YApi MCP Server(Node.js + MCP SDK),提供按需查询的两个核心工具:
|
|
4
|
-
|
|
5
|
-
- `search_yapi_interfaces`
|
|
6
|
-
- `get_yapi_interface_detail`
|
|
7
|
-
|
|
8
|
-
> Token 仅支持 **project 级**。`group` 级 URL 不支持鉴权。
|
|
9
|
-
|
|
10
|
-
## 1. 安装与构建
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm install
|
|
14
|
-
npm run build
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
本地开发:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm run dev
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## 2. 配置
|
|
24
|
-
|
|
25
|
-
### 环境变量
|
|
26
|
-
|
|
27
|
-
- `YAPI_BASE_URL`(必填)
|
|
28
|
-
- `YAPI_TOKEN_FILE`(可选,默认 `~/.yapi-mcp-tokens.json`)
|
|
29
|
-
- `YAPI_TIMEOUT_MS`(可选,默认 `8000`)
|
|
30
|
-
- `YAPI_RETRY_COUNT`(可选,默认 `1`)
|
|
31
|
-
- `YAPI_TOKEN`(可选,单项目临时兜底)
|
|
32
|
-
|
|
33
|
-
### Token 文件
|
|
34
|
-
|
|
35
|
-
路径:`~/.yapi-mcp-tokens.json`
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"695": "token_for_project_695",
|
|
40
|
-
"703": "token_for_project_703"
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## 3. MCP 客户端配置示例(Cursor)
|
|
45
|
-
|
|
46
|
-
```json
|
|
47
|
-
{
|
|
48
|
-
"mcpServers": {
|
|
49
|
-
"yapi": {
|
|
50
|
-
"command": "node",
|
|
51
|
-
"args": ["D:/self/yapi-mcp/dist/index.js"],
|
|
52
|
-
"env": {
|
|
53
|
-
"YAPI_BASE_URL": "http://10.255.30.245:3000"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
如果你后续发布 npm 包,也可改为:
|
|
61
|
-
|
|
62
|
-
```json
|
|
63
|
-
{
|
|
64
|
-
"mcpServers": {
|
|
65
|
-
"yapi": {
|
|
66
|
-
"command": "npx",
|
|
67
|
-
"args": ["-y", "@wllcyg001/yapi-mcp"],
|
|
68
|
-
"env": {
|
|
69
|
-
"YAPI_BASE_URL": "http://10.255.30.245:3000"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 4. 工具定义
|
|
77
|
-
|
|
78
|
-
### `search_yapi_interfaces`
|
|
79
|
-
|
|
80
|
-
输入:
|
|
81
|
-
|
|
82
|
-
- `keyword: string`(必填)
|
|
83
|
-
- `projectId?: number`
|
|
84
|
-
- `projectUrl?: string`(如 `.../project/695/interface/api`)
|
|
85
|
-
- `method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"`
|
|
86
|
-
- `pathHint?: string`
|
|
87
|
-
- `limit?: number`(默认 10,最大 50)
|
|
88
|
-
|
|
89
|
-
输出:
|
|
90
|
-
|
|
91
|
-
- 匹配接口列表:`interfaceId / name / method / path / projectId / score`
|
|
92
|
-
|
|
93
|
-
### `get_yapi_interface_detail`
|
|
94
|
-
|
|
95
|
-
输入:
|
|
96
|
-
|
|
97
|
-
- `interfaceId: number`(必填)
|
|
98
|
-
- `projectId?: number`
|
|
99
|
-
- `projectUrl?: string`
|
|
100
|
-
- `includeMock?: boolean`(默认 false)
|
|
101
|
-
|
|
102
|
-
输出:
|
|
103
|
-
|
|
104
|
-
- 接口详情(request/response schema,默认不返回 mock)
|
|
105
|
-
|
|
106
|
-
## 5. 错误码
|
|
107
|
-
|
|
108
|
-
- `INVALID_ARGUMENT`
|
|
109
|
-
- `PROJECT_ID_REQUIRED`
|
|
110
|
-
- `PROJECT_TOKEN_REQUIRED`
|
|
111
|
-
- `TOKEN_SCOPE_UNSUPPORTED`
|
|
112
|
-
- `PROJECT_NOT_ACCESSIBLE`
|
|
113
|
-
- `INTERFACE_NOT_FOUND`
|
|
114
|
-
- `UPSTREAM_TIMEOUT`
|
|
115
|
-
- `UPSTREAM_ERROR`
|
|
116
|
-
- `RATE_LIMITED`
|
|
1
|
+
# @wllcyg001/yapi-mcp
|
|
2
|
+
|
|
3
|
+
YApi MCP Server(Node.js + MCP SDK),提供按需查询的两个核心工具:
|
|
4
|
+
|
|
5
|
+
- `search_yapi_interfaces`
|
|
6
|
+
- `get_yapi_interface_detail`
|
|
7
|
+
|
|
8
|
+
> Token 仅支持 **project 级**。`group` 级 URL 不支持鉴权。
|
|
9
|
+
|
|
10
|
+
## 1. 安装与构建
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install
|
|
14
|
+
npm run build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
本地开发:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 2. 配置
|
|
24
|
+
|
|
25
|
+
### 环境变量
|
|
26
|
+
|
|
27
|
+
- `YAPI_BASE_URL`(必填)
|
|
28
|
+
- `YAPI_TOKEN_FILE`(可选,默认 `~/.yapi-mcp-tokens.json`)
|
|
29
|
+
- `YAPI_TIMEOUT_MS`(可选,默认 `8000`)
|
|
30
|
+
- `YAPI_RETRY_COUNT`(可选,默认 `1`)
|
|
31
|
+
- `YAPI_TOKEN`(可选,单项目临时兜底)
|
|
32
|
+
|
|
33
|
+
### Token 文件
|
|
34
|
+
|
|
35
|
+
路径:`~/.yapi-mcp-tokens.json`
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"695": "token_for_project_695",
|
|
40
|
+
"703": "token_for_project_703"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 3. MCP 客户端配置示例(Cursor)
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"yapi": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["D:/self/yapi-mcp/dist/index.js"],
|
|
52
|
+
"env": {
|
|
53
|
+
"YAPI_BASE_URL": "http://10.255.30.245:3000"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
如果你后续发布 npm 包,也可改为:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"yapi": {
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["-y", "@wllcyg001/yapi-mcp"],
|
|
68
|
+
"env": {
|
|
69
|
+
"YAPI_BASE_URL": "http://10.255.30.245:3000"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 4. 工具定义
|
|
77
|
+
|
|
78
|
+
### `search_yapi_interfaces`
|
|
79
|
+
|
|
80
|
+
输入:
|
|
81
|
+
|
|
82
|
+
- `keyword: string`(必填)
|
|
83
|
+
- `projectId?: number`
|
|
84
|
+
- `projectUrl?: string`(如 `.../project/695/interface/api`)
|
|
85
|
+
- `method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"`
|
|
86
|
+
- `pathHint?: string`
|
|
87
|
+
- `limit?: number`(默认 10,最大 50)
|
|
88
|
+
|
|
89
|
+
输出:
|
|
90
|
+
|
|
91
|
+
- 匹配接口列表:`interfaceId / name / method / path / projectId / score`
|
|
92
|
+
|
|
93
|
+
### `get_yapi_interface_detail`
|
|
94
|
+
|
|
95
|
+
输入:
|
|
96
|
+
|
|
97
|
+
- `interfaceId: number`(必填)
|
|
98
|
+
- `projectId?: number`
|
|
99
|
+
- `projectUrl?: string`
|
|
100
|
+
- `includeMock?: boolean`(默认 false)
|
|
101
|
+
|
|
102
|
+
输出:
|
|
103
|
+
|
|
104
|
+
- 接口详情(request/response schema,默认不返回 mock)
|
|
105
|
+
|
|
106
|
+
## 5. 错误码
|
|
107
|
+
|
|
108
|
+
- `INVALID_ARGUMENT`
|
|
109
|
+
- `PROJECT_ID_REQUIRED`
|
|
110
|
+
- `PROJECT_TOKEN_REQUIRED`
|
|
111
|
+
- `TOKEN_SCOPE_UNSUPPORTED`
|
|
112
|
+
- `PROJECT_NOT_ACCESSIBLE`
|
|
113
|
+
- `INTERFACE_NOT_FOUND`
|
|
114
|
+
- `UPSTREAM_TIMEOUT`
|
|
115
|
+
- `UPSTREAM_ERROR`
|
|
116
|
+
- `RATE_LIMITED`
|
package/package.json
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@wllcyg001/yapi-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"private": false,
|
|
5
|
-
"description": "MCP server for querying YApi interfaces by project-level token map",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "dist/index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"yapi-mcp-server": "dist/index.js"
|
|
10
|
-
},
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@wllcyg001/yapi-mcp",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "MCP server for querying YApi interfaces by project-level token map",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"yapi-mcp-server": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/wllcyg/yapi-mcp.git"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"dev": "tsx src/index.ts",
|
|
25
|
+
"start": "node dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
32
|
+
"axios": "^1.9.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.13.9",
|
|
36
|
+
"tsx": "^4.19.3",
|
|
37
|
+
"typescript": "^5.8.2"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- name: Checkout
|
|
14
|
-
uses: actions/checkout@v4
|
|
15
|
-
|
|
16
|
-
- name: Setup Node
|
|
17
|
-
uses: actions/setup-node@v4
|
|
18
|
-
with:
|
|
19
|
-
node-version: 20
|
|
20
|
-
cache: npm
|
|
21
|
-
|
|
22
|
-
- name: Install dependencies
|
|
23
|
-
run: npm ci
|
|
24
|
-
|
|
25
|
-
- name: Build
|
|
26
|
-
run: npm run build
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*"
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
publish:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
id-token: write
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Setup Node
|
|
19
|
-
uses: actions/setup-node@v4
|
|
20
|
-
with:
|
|
21
|
-
node-version: 20
|
|
22
|
-
registry-url: "https://registry.npmjs.org"
|
|
23
|
-
cache: npm
|
|
24
|
-
|
|
25
|
-
- name: Install dependencies
|
|
26
|
-
run: npm ci
|
|
27
|
-
|
|
28
|
-
- name: Build
|
|
29
|
-
run: npm run build
|
|
30
|
-
|
|
31
|
-
- name: Publish to npm (Trusted Publishing)
|
|
32
|
-
run: npm publish --access public --provenance
|
package/YApi-MCP-Server-PRD.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# 产品需求文档 (PRD):YApi MCP Server
|
|
2
|
-
|
|
3
|
-
## 1. 背景与痛点 (Background & Pain points)
|
|
4
|
-
|
|
5
|
-
在日常前端开发中,团队通常依赖 YApi 作为接口管理工具。当前的工作流存在以下痛点:
|
|
6
|
-
1. **冗余的批量生成**:现有的 `yapi-codegen` 脚本会一次性拉取整个项目的所有接口,生成大量可能永远不会用到的本地 `.js` 文件,导致项目体积膨胀。
|
|
7
|
-
2. **人工上下文割裂**:开发者需要离开 IDE,去 YApi 网页端搜索接口,然后再回到代码中引入对应的方法,来回切换打断心流。
|
|
8
|
-
3. **接口同步滞后**:后端修改接口后,前端往往不知道,或者忘记重新跑脚本,导致联调时参数报错。
|
|
9
|
-
|
|
10
|
-
## 2. 解决方案 (Solution Overview)
|
|
11
|
-
|
|
12
|
-
利用 **MCP (Model Context Protocol)** 技术,开发一个独立的 `yapi-mcp-server`。
|
|
13
|
-
该方案的核心在于**颠覆传统的“提前全量生成”模式,走向“AI 按需动态获取”模式**。
|
|
14
|
-
|
|
15
|
-
把 YApi 变成 AI 助手(Cursor / Claude Desktop)的外挂大脑。开发者只需用自然语言描述需求(如“帮我接一下新增工单的接口”),AI 即可在后台自动调用 MCP Server 实时查阅 YApi 文档,并在当前组件中精确生成包含正确类型定义的代码。
|
|
16
|
-
|
|
17
|
-
## 3. 核心功能特性 (Core Features)
|
|
18
|
-
|
|
19
|
-
### 3.1 基于自然语言的接口搜索
|
|
20
|
-
- **描述**:AI 可根据开发者口语化的需求,在指定的 YApi 项目中搜索匹配的接口。
|
|
21
|
-
- **MCP 工具定义**:`search_yapi_interfaces`
|
|
22
|
-
- **入参(建议 Schema)**:
|
|
23
|
-
- `keyword: string`(必填,如“工单详情”)
|
|
24
|
-
- `projectId?: number`(可选,优先显式传入)
|
|
25
|
-
- `projectUrl?: string`(可选,如 `.../project/695/interface/api`,可自动解析 projectId)
|
|
26
|
-
- `method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"`(可选)
|
|
27
|
-
- `pathHint?: string`(可选,如 `/api/work-order`)
|
|
28
|
-
- `limit?: number`(可选,默认 `10`,最大 `50`)
|
|
29
|
-
- **出参**:匹配的接口列表(包含接口 ID、名称、Method、Path、所属项目 ID、匹配分数摘要)。
|
|
30
|
-
|
|
31
|
-
### 3.2 接口详情与契约精准获取
|
|
32
|
-
- **描述**:获取具体某个接口的详细定义,包括 Request Query、Request Body 结构,以及完整的 Response JSON Schema。
|
|
33
|
-
- **MCP 工具定义**:`get_yapi_interface_detail`
|
|
34
|
-
- **入参(建议 Schema)**:
|
|
35
|
-
- `interfaceId: number`(必填,从搜索结果中获得)
|
|
36
|
-
- `projectId?: number`(可选,建议传入以避免跨项目歧义)
|
|
37
|
-
- `projectUrl?: string`(可选,可自动解析 projectId)
|
|
38
|
-
- `includeMock?: boolean`(可选,默认 `false`,为 Phase 2 预留)
|
|
39
|
-
- **出参**:精确到每个字段是否必填、数据类型、默认值/示例值(若有)的完整接口定义。
|
|
40
|
-
|
|
41
|
-
### 3.3 多项目环境下的 Token 管理(仅 Project 级)
|
|
42
|
-
- **关键事实**:YApi 的 Token 是基于「项目级别 (Project)」的,不是 Group 级别。
|
|
43
|
-
- 示例:`/group/896` 没有可用 token;`/project/695/...` 才有对应 token。
|
|
44
|
-
- **解决方案 (Token Map)**:不支持单一全局 Token,而是采用 **Token 映射表**。
|
|
45
|
-
- **本地配置文件**:MCP Server 启动时,读取本机 `~/.yapi-mcp-tokens.json`,格式:`{ "695": "token_A", "703": "token_B" }`。
|
|
46
|
-
- **动态调用**:优先从 `projectId` / `projectUrl` 解析出项目 ID,再去映射表读取对应 token。
|
|
47
|
-
- **不支持 group 级鉴权**:传入 `groupUrl` 时直接返回结构化错误,提示补充 `projectId` 或 `projectUrl`。
|
|
48
|
-
|
|
49
|
-
## 4. 典型用户故事 (User Stories)
|
|
50
|
-
|
|
51
|
-
- **场景 A(新功能开发)**:前端接手一个新页面,在 Cursor 里输入:“帮我写一下用户配置表单,对接保存配置的接口”。Cursor 自动通过 MCP 查到接口参数,并把完整的 `axios.post` 及 React/Vue 表单组件全部写好。
|
|
52
|
-
- **场景 B(接口变更修复)**:联调时后端提示增加了一个必填字段。前端在代码中对 AI 说:“查一下目前获取详情接口最新的返回体,同步更新一下本地的 TypeScript Interface”。
|
|
53
|
-
|
|
54
|
-
## 5. 技术架构与选型 (Architecture)
|
|
55
|
-
|
|
56
|
-
- **运行时环境**:Node.js (v18+)
|
|
57
|
-
- **核心依赖**:
|
|
58
|
-
- `@modelcontextprotocol/sdk`:实现标准 MCP 协议
|
|
59
|
-
- `axios`:用于与公司内部的 YApi 服务器通信
|
|
60
|
-
- **通信方式**:Stdio Transport (标准输入输出,适配绝大部分本地 IDE)
|
|
61
|
-
- **配置管理(统一口径)**:
|
|
62
|
-
- `process.env.YAPI_BASE_URL`:必填
|
|
63
|
-
- `~/.yapi-mcp-tokens.json`:必填(projectId -> token)
|
|
64
|
-
- `process.env.YAPI_TOKEN`:仅作为单项目/临时兜底,不作为主流程依赖
|
|
65
|
-
- **安全与稳定性要求**:
|
|
66
|
-
- Token 不写入日志;日志中对凭证字段统一脱敏
|
|
67
|
-
- `~/.yapi-mcp-tokens.json` 建议仅当前用户可读写(如 `600` 权限)
|
|
68
|
-
- 请求超时(建议 5~10s)+ 有限重试(建议 1~2 次,仅幂等查询)
|
|
69
|
-
- 仅允许访问 `YAPI_BASE_URL` 所属域名,避免 SSRF 风险
|
|
70
|
-
|
|
71
|
-
## 6. 错误模型与返回规范 (Error Model)
|
|
72
|
-
|
|
73
|
-
为保证 AI 可恢复调用,所有失败返回统一结构:
|
|
74
|
-
- `code: string`
|
|
75
|
-
- `message: string`
|
|
76
|
-
- `suggestion?: string`
|
|
77
|
-
- `context?: object`
|
|
78
|
-
|
|
79
|
-
建议错误码:
|
|
80
|
-
- `INVALID_ARGUMENT`:参数非法或缺失
|
|
81
|
-
- `PROJECT_ID_REQUIRED`:无法从参数中确定 projectId
|
|
82
|
-
- `PROJECT_TOKEN_REQUIRED`:缺少指定项目 token
|
|
83
|
-
- `TOKEN_SCOPE_UNSUPPORTED`:仅提供了 group 信息,当前仅支持 project 级 token
|
|
84
|
-
- `PROJECT_NOT_ACCESSIBLE`:项目不存在或无权限
|
|
85
|
-
- `INTERFACE_NOT_FOUND`:接口不存在
|
|
86
|
-
- `UPSTREAM_TIMEOUT`:YApi 请求超时
|
|
87
|
-
- `UPSTREAM_ERROR`:YApi 返回非预期错误
|
|
88
|
-
- `RATE_LIMITED`:触发限流
|
|
89
|
-
|
|
90
|
-
## 7. 演进路线 (Roadmap)
|
|
91
|
-
|
|
92
|
-
- **Phase 1: 核心查询能力(MVP)**
|
|
93
|
-
- 完成 MCP Server 基础骨架
|
|
94
|
-
- 实现 `search_yapi_interfaces` 和 `get_yapi_interface_detail`
|
|
95
|
-
- 打通 `projectId/projectUrl -> token map -> YApi` 主链路
|
|
96
|
-
- 实现统一错误码与结构化返回
|
|
97
|
-
- 在 Cursor 或 Claude Desktop 本地配置跑通
|
|
98
|
-
- **Phase 2: 上下文感知增强**
|
|
99
|
-
- 支持直接提供当前打开文件的代码片段,让 AI 判断是否需要根据 YApi 更新代码
|
|
100
|
-
- 解析 YApi 中的 Mock 数据并直接返回,供 AI 自动生成 Mock Server 脚本
|
|
101
|
-
- **Phase 3: 团队工程化发布**
|
|
102
|
-
- 封装为 NPM 私有包,提供 `npx yapi-mcp-server` 的一键启动能力
|
|
103
|
-
- 编写团队内部操作手册,全员推广“无代码生成”开发模式
|
|
104
|
-
|
|
105
|
-
## 8. Phase 1 验收标准 (Acceptance Criteria)
|
|
106
|
-
|
|
107
|
-
- 在至少 2 个不同项目(如 `695`、`703`)下可正确读取各自 token 并查询成功
|
|
108
|
-
- 对仅提供 `groupUrl` 的请求,返回 `TOKEN_SCOPE_UNSUPPORTED`,且提示可执行的下一步操作
|
|
109
|
-
- 搜索接口结果默认返回前 10 条,且包含 `interfaceId/name/method/path/projectId`
|
|
110
|
-
- 详情接口返回请求参数、响应 Schema、必填信息,不丢关键字段
|
|
111
|
-
- 网络异常和上游超时场景下,服务不崩溃,返回统一错误结构
|
package/src/config.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { AppError } from "./errors.js";
|
|
5
|
-
|
|
6
|
-
export interface AppConfig {
|
|
7
|
-
baseUrl: string;
|
|
8
|
-
tokenFilePath: string;
|
|
9
|
-
timeoutMs: number;
|
|
10
|
-
retryCount: number;
|
|
11
|
-
fallbackToken?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getConfig(): AppConfig {
|
|
15
|
-
const baseUrl = process.env.YAPI_BASE_URL?.trim();
|
|
16
|
-
if (!baseUrl) {
|
|
17
|
-
throw new AppError({
|
|
18
|
-
code: "INVALID_ARGUMENT",
|
|
19
|
-
message: "Missing required env: YAPI_BASE_URL",
|
|
20
|
-
suggestion: "Set YAPI_BASE_URL, e.g. http://10.255.30.245:3000",
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
baseUrl,
|
|
26
|
-
tokenFilePath: process.env.YAPI_TOKEN_FILE?.trim() || path.join(os.homedir(), ".yapi-mcp-tokens.json"),
|
|
27
|
-
timeoutMs: Number(process.env.YAPI_TIMEOUT_MS || 8000),
|
|
28
|
-
retryCount: Number(process.env.YAPI_RETRY_COUNT || 1),
|
|
29
|
-
fallbackToken: process.env.YAPI_TOKEN?.trim() || undefined,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function loadTokenMap(tokenFilePath: string): Record<string, string> {
|
|
34
|
-
if (!fs.existsSync(tokenFilePath)) {
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const raw = fs.readFileSync(tokenFilePath, "utf8");
|
|
39
|
-
if (!raw.trim()) return {};
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
43
|
-
if (!parsed || typeof parsed !== "object") {
|
|
44
|
-
throw new Error("token file is not an object");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const result: Record<string, string> = {};
|
|
48
|
-
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
49
|
-
if (typeof v === "string" && v.trim()) {
|
|
50
|
-
result[k] = v.trim();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
} catch {
|
|
55
|
-
throw new AppError({
|
|
56
|
-
code: "INVALID_ARGUMENT",
|
|
57
|
-
message: `Invalid token file JSON: ${tokenFilePath}`,
|
|
58
|
-
suggestion: "Ensure token file format is like: { \"695\": \"token_xxx\" }",
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function resolveProjectId(input: {
|
|
64
|
-
projectId?: number;
|
|
65
|
-
projectUrl?: string;
|
|
66
|
-
groupUrl?: string;
|
|
67
|
-
}): number {
|
|
68
|
-
if (typeof input.projectId === "number" && Number.isFinite(input.projectId)) {
|
|
69
|
-
return input.projectId;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (input.projectUrl) {
|
|
73
|
-
const matched = input.projectUrl.match(/\/project\/(\d+)/i);
|
|
74
|
-
if (matched) return Number(matched[1]);
|
|
75
|
-
|
|
76
|
-
if (/\/group\/\d+/i.test(input.projectUrl)) {
|
|
77
|
-
throw new AppError({
|
|
78
|
-
code: "TOKEN_SCOPE_UNSUPPORTED",
|
|
79
|
-
message: "Group URL is not supported for token resolution",
|
|
80
|
-
suggestion: "Provide projectId or projectUrl like /project/695/interface/api",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (input.groupUrl) {
|
|
86
|
-
throw new AppError({
|
|
87
|
-
code: "TOKEN_SCOPE_UNSUPPORTED",
|
|
88
|
-
message: "Group URL is not supported for token resolution",
|
|
89
|
-
suggestion: "Provide projectId or projectUrl like /project/695/interface/api",
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
throw new AppError({
|
|
94
|
-
code: "PROJECT_ID_REQUIRED",
|
|
95
|
-
message: "Cannot determine projectId from input",
|
|
96
|
-
suggestion: "Provide projectId or projectUrl",
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function resolveToken(args: {
|
|
101
|
-
projectId: number;
|
|
102
|
-
tokenMap: Record<string, string>;
|
|
103
|
-
fallbackToken?: string;
|
|
104
|
-
}): string {
|
|
105
|
-
const token = args.tokenMap[String(args.projectId)] || args.fallbackToken;
|
|
106
|
-
if (!token) {
|
|
107
|
-
throw new AppError({
|
|
108
|
-
code: "PROJECT_TOKEN_REQUIRED",
|
|
109
|
-
message: `Missing token for projectId=${args.projectId}`,
|
|
110
|
-
suggestion: `Add \"${args.projectId}\": \"<token>\" to ~/.yapi-mcp-tokens.json`,
|
|
111
|
-
context: { projectId: args.projectId },
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
return token;
|
|
115
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { ApiErrorShape, ErrorCode } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export class AppError extends Error {
|
|
4
|
-
readonly code: ErrorCode;
|
|
5
|
-
readonly suggestion?: string;
|
|
6
|
-
readonly context?: Record<string, unknown>;
|
|
7
|
-
|
|
8
|
-
constructor(shape: ApiErrorShape) {
|
|
9
|
-
super(shape.message);
|
|
10
|
-
this.code = shape.code;
|
|
11
|
-
this.suggestion = shape.suggestion;
|
|
12
|
-
this.context = shape.context;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
toShape(): ApiErrorShape {
|
|
16
|
-
return {
|
|
17
|
-
code: this.code,
|
|
18
|
-
message: this.message,
|
|
19
|
-
suggestion: this.suggestion,
|
|
20
|
-
context: this.context,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function ensure(condition: unknown, shape: ApiErrorShape): asserts condition {
|
|
26
|
-
if (!condition) {
|
|
27
|
-
throw new AppError(shape);
|
|
28
|
-
}
|
|
29
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
CallToolRequestSchema,
|
|
6
|
-
ListToolsRequestSchema,
|
|
7
|
-
type CallToolRequest,
|
|
8
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import {
|
|
10
|
-
getConfig,
|
|
11
|
-
loadTokenMap,
|
|
12
|
-
resolveProjectId,
|
|
13
|
-
resolveToken,
|
|
14
|
-
} from "./config.js";
|
|
15
|
-
import { AppError } from "./errors.js";
|
|
16
|
-
import type { DetailInput, SearchInput } from "./types.js";
|
|
17
|
-
import { YApiClient } from "./yapi.js";
|
|
18
|
-
|
|
19
|
-
const config = getConfig();
|
|
20
|
-
const tokenMap = loadTokenMap(config.tokenFilePath);
|
|
21
|
-
const yapi = new YApiClient({
|
|
22
|
-
baseUrl: config.baseUrl,
|
|
23
|
-
timeoutMs: config.timeoutMs,
|
|
24
|
-
retryCount: config.retryCount,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const server = new Server(
|
|
28
|
-
{
|
|
29
|
-
name: "yapi-mcp-server",
|
|
30
|
-
version: "0.1.0",
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
capabilities: {
|
|
34
|
-
tools: {},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
40
|
-
return {
|
|
41
|
-
tools: [
|
|
42
|
-
{
|
|
43
|
-
name: "search_yapi_interfaces",
|
|
44
|
-
description:
|
|
45
|
-
"Search YApi interfaces by natural language keyword within a project. Token scope is project-level only.",
|
|
46
|
-
inputSchema: {
|
|
47
|
-
type: "object",
|
|
48
|
-
properties: {
|
|
49
|
-
keyword: { type: "string", minLength: 1 },
|
|
50
|
-
projectId: { type: "number" },
|
|
51
|
-
projectUrl: { type: "string" },
|
|
52
|
-
method: {
|
|
53
|
-
type: "string",
|
|
54
|
-
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
55
|
-
},
|
|
56
|
-
pathHint: { type: "string" },
|
|
57
|
-
limit: { type: "number", minimum: 1, maximum: 50 },
|
|
58
|
-
},
|
|
59
|
-
required: ["keyword"],
|
|
60
|
-
additionalProperties: false,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: "get_yapi_interface_detail",
|
|
65
|
-
description:
|
|
66
|
-
"Get YApi interface detail including request/response schema. Token scope is project-level only.",
|
|
67
|
-
inputSchema: {
|
|
68
|
-
type: "object",
|
|
69
|
-
properties: {
|
|
70
|
-
interfaceId: { type: "number" },
|
|
71
|
-
projectId: { type: "number" },
|
|
72
|
-
projectUrl: { type: "string" },
|
|
73
|
-
includeMock: { type: "boolean" },
|
|
74
|
-
},
|
|
75
|
-
required: ["interfaceId"],
|
|
76
|
-
additionalProperties: false,
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
server.setRequestHandler(
|
|
84
|
-
CallToolRequestSchema,
|
|
85
|
-
async (request: CallToolRequest) => {
|
|
86
|
-
try {
|
|
87
|
-
const { name, arguments: args } = request.params;
|
|
88
|
-
|
|
89
|
-
if (name === "search_yapi_interfaces") {
|
|
90
|
-
const input = (args ?? {}) as unknown as SearchInput;
|
|
91
|
-
if (!input.keyword?.trim()) {
|
|
92
|
-
throw new AppError({
|
|
93
|
-
code: "INVALID_ARGUMENT",
|
|
94
|
-
message: "keyword is required",
|
|
95
|
-
suggestion: "Pass keyword like '工单详情'",
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const projectId = resolveProjectId({
|
|
100
|
-
projectId: input.projectId,
|
|
101
|
-
projectUrl: input.projectUrl,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const token = resolveToken({
|
|
105
|
-
projectId,
|
|
106
|
-
tokenMap,
|
|
107
|
-
fallbackToken: config.fallbackToken,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const limit = clampLimit(input.limit);
|
|
111
|
-
const result = await yapi.searchInterfaces({
|
|
112
|
-
projectId,
|
|
113
|
-
token,
|
|
114
|
-
keyword: input.keyword,
|
|
115
|
-
method: input.method,
|
|
116
|
-
pathHint: input.pathHint,
|
|
117
|
-
limit,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
return ok({
|
|
121
|
-
projectId,
|
|
122
|
-
count: result.length,
|
|
123
|
-
items: result,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (name === "get_yapi_interface_detail") {
|
|
128
|
-
const input = (args ?? {}) as unknown as DetailInput;
|
|
129
|
-
if (!Number.isFinite(input.interfaceId)) {
|
|
130
|
-
throw new AppError({
|
|
131
|
-
code: "INVALID_ARGUMENT",
|
|
132
|
-
message: "interfaceId is required and must be a number",
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const projectId = resolveProjectId({
|
|
137
|
-
projectId: input.projectId,
|
|
138
|
-
projectUrl: input.projectUrl,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const token = resolveToken({
|
|
142
|
-
projectId,
|
|
143
|
-
tokenMap,
|
|
144
|
-
fallbackToken: config.fallbackToken,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const detail = await yapi.getInterfaceDetail({
|
|
148
|
-
interfaceId: input.interfaceId,
|
|
149
|
-
token,
|
|
150
|
-
includeMock: input.includeMock,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return ok({
|
|
154
|
-
projectId,
|
|
155
|
-
interfaceId: input.interfaceId,
|
|
156
|
-
detail,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
throw new AppError({
|
|
161
|
-
code: "INVALID_ARGUMENT",
|
|
162
|
-
message: `Unknown tool: ${name}`,
|
|
163
|
-
});
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return fail(error);
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
async function main() {
|
|
171
|
-
const transport = new StdioServerTransport();
|
|
172
|
-
await server.connect(transport);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function ok(data: unknown) {
|
|
176
|
-
return {
|
|
177
|
-
content: [
|
|
178
|
-
{
|
|
179
|
-
type: "text" as const,
|
|
180
|
-
text: JSON.stringify(
|
|
181
|
-
{
|
|
182
|
-
ok: true,
|
|
183
|
-
data,
|
|
184
|
-
},
|
|
185
|
-
null,
|
|
186
|
-
2,
|
|
187
|
-
),
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function fail(error: unknown) {
|
|
194
|
-
if (error instanceof AppError) {
|
|
195
|
-
return {
|
|
196
|
-
isError: true,
|
|
197
|
-
content: [
|
|
198
|
-
{
|
|
199
|
-
type: "text" as const,
|
|
200
|
-
text: JSON.stringify(
|
|
201
|
-
{
|
|
202
|
-
ok: false,
|
|
203
|
-
error: error.toShape(),
|
|
204
|
-
},
|
|
205
|
-
null,
|
|
206
|
-
2,
|
|
207
|
-
),
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
isError: true,
|
|
215
|
-
content: [
|
|
216
|
-
{
|
|
217
|
-
type: "text" as const,
|
|
218
|
-
text: JSON.stringify(
|
|
219
|
-
{
|
|
220
|
-
ok: false,
|
|
221
|
-
error: {
|
|
222
|
-
code: "UPSTREAM_ERROR",
|
|
223
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
null,
|
|
227
|
-
2,
|
|
228
|
-
),
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function clampLimit(limit?: number): number {
|
|
235
|
-
if (!Number.isFinite(limit)) return 10;
|
|
236
|
-
return Math.min(50, Math.max(1, Number(limit)));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
main().catch((error) => {
|
|
240
|
-
const payload =
|
|
241
|
-
error instanceof AppError
|
|
242
|
-
? { ok: false, error: error.toShape() }
|
|
243
|
-
: {
|
|
244
|
-
ok: false,
|
|
245
|
-
error: {
|
|
246
|
-
code: "UPSTREAM_ERROR",
|
|
247
|
-
message:
|
|
248
|
-
error instanceof Error ? error.message : "Unknown startup error",
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
253
|
-
process.exit(1);
|
|
254
|
-
});
|
package/src/types.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export type ErrorCode =
|
|
2
|
-
| "INVALID_ARGUMENT"
|
|
3
|
-
| "PROJECT_ID_REQUIRED"
|
|
4
|
-
| "PROJECT_TOKEN_REQUIRED"
|
|
5
|
-
| "TOKEN_SCOPE_UNSUPPORTED"
|
|
6
|
-
| "PROJECT_NOT_ACCESSIBLE"
|
|
7
|
-
| "INTERFACE_NOT_FOUND"
|
|
8
|
-
| "UPSTREAM_TIMEOUT"
|
|
9
|
-
| "UPSTREAM_ERROR"
|
|
10
|
-
| "RATE_LIMITED";
|
|
11
|
-
|
|
12
|
-
export interface ApiErrorShape {
|
|
13
|
-
code: ErrorCode;
|
|
14
|
-
message: string;
|
|
15
|
-
suggestion?: string;
|
|
16
|
-
context?: Record<string, unknown>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface SearchInput {
|
|
20
|
-
keyword: string;
|
|
21
|
-
projectId?: number;
|
|
22
|
-
projectUrl?: string;
|
|
23
|
-
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
24
|
-
pathHint?: string;
|
|
25
|
-
limit?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface DetailInput {
|
|
29
|
-
interfaceId: number;
|
|
30
|
-
projectId?: number;
|
|
31
|
-
projectUrl?: string;
|
|
32
|
-
includeMock?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface YApiInterfaceSummary {
|
|
36
|
-
interfaceId: number;
|
|
37
|
-
name: string;
|
|
38
|
-
method?: string;
|
|
39
|
-
path?: string;
|
|
40
|
-
projectId: number;
|
|
41
|
-
score: number;
|
|
42
|
-
}
|
package/src/yapi.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import axios, { type AxiosInstance } from "axios";
|
|
2
|
-
import { AppError } from "./errors.js";
|
|
3
|
-
import type { YApiInterfaceSummary } from "./types.js";
|
|
4
|
-
|
|
5
|
-
interface YApiClientOptions {
|
|
6
|
-
baseUrl: string;
|
|
7
|
-
timeoutMs: number;
|
|
8
|
-
retryCount: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class YApiClient {
|
|
12
|
-
private readonly client: AxiosInstance;
|
|
13
|
-
private readonly retryCount: number;
|
|
14
|
-
|
|
15
|
-
constructor(options: YApiClientOptions) {
|
|
16
|
-
this.client = axios.create({
|
|
17
|
-
baseURL: options.baseUrl,
|
|
18
|
-
timeout: options.timeoutMs,
|
|
19
|
-
});
|
|
20
|
-
this.retryCount = options.retryCount;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async searchInterfaces(args: {
|
|
24
|
-
projectId: number;
|
|
25
|
-
token: string;
|
|
26
|
-
keyword: string;
|
|
27
|
-
method?: string;
|
|
28
|
-
pathHint?: string;
|
|
29
|
-
limit: number;
|
|
30
|
-
}): Promise<YApiInterfaceSummary[]> {
|
|
31
|
-
const menuData = await this.requestWithRetry("/api/interface/list_menu", {
|
|
32
|
-
project_id: args.projectId,
|
|
33
|
-
token: args.token,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const flat = flattenMenuInterfaces(menuData, args.projectId);
|
|
37
|
-
const keyword = args.keyword.trim().toLowerCase();
|
|
38
|
-
const method = args.method?.toUpperCase();
|
|
39
|
-
const pathHint = args.pathHint?.toLowerCase();
|
|
40
|
-
|
|
41
|
-
const scored = flat
|
|
42
|
-
.filter((item) => {
|
|
43
|
-
if (method && (item.method || "").toUpperCase() !== method) return false;
|
|
44
|
-
return true;
|
|
45
|
-
})
|
|
46
|
-
.map((item) => {
|
|
47
|
-
const title = (item.name || "").toLowerCase();
|
|
48
|
-
const path = (item.path || "").toLowerCase();
|
|
49
|
-
|
|
50
|
-
let score = 0;
|
|
51
|
-
if (title.includes(keyword)) score += 50;
|
|
52
|
-
if (path.includes(keyword)) score += 40;
|
|
53
|
-
if (pathHint && path.includes(pathHint)) score += 30;
|
|
54
|
-
if (title.startsWith(keyword)) score += 20;
|
|
55
|
-
if ((item.method || "").toUpperCase() === method) score += 10;
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
...item,
|
|
59
|
-
score,
|
|
60
|
-
};
|
|
61
|
-
})
|
|
62
|
-
.filter((item) => item.score > 0)
|
|
63
|
-
.sort((a, b) => b.score - a.score)
|
|
64
|
-
.slice(0, args.limit);
|
|
65
|
-
|
|
66
|
-
return scored;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async getInterfaceDetail(args: {
|
|
70
|
-
interfaceId: number;
|
|
71
|
-
token: string;
|
|
72
|
-
includeMock?: boolean;
|
|
73
|
-
}): Promise<Record<string, unknown>> {
|
|
74
|
-
const detail = await this.requestWithRetry("/api/interface/get", {
|
|
75
|
-
id: args.interfaceId,
|
|
76
|
-
token: args.token,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!detail || typeof detail !== "object") {
|
|
80
|
-
throw new AppError({
|
|
81
|
-
code: "INTERFACE_NOT_FOUND",
|
|
82
|
-
message: `Interface ${args.interfaceId} not found`,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!args.includeMock && "mock" in detail) {
|
|
87
|
-
const copy = { ...(detail as Record<string, unknown>) };
|
|
88
|
-
delete copy.mock;
|
|
89
|
-
return copy;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return detail as Record<string, unknown>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private async requestWithRetry(url: string, params: Record<string, unknown>): Promise<unknown> {
|
|
96
|
-
let attempt = 0;
|
|
97
|
-
const maxAttempts = Math.max(1, this.retryCount + 1);
|
|
98
|
-
|
|
99
|
-
while (attempt < maxAttempts) {
|
|
100
|
-
try {
|
|
101
|
-
const res = await this.client.get(url, { params });
|
|
102
|
-
const payload = res.data as any;
|
|
103
|
-
|
|
104
|
-
if (payload?.errcode === 0) {
|
|
105
|
-
return payload.data;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const errcode = payload?.errcode;
|
|
109
|
-
const errmsg = payload?.errmsg || "Unknown YApi error";
|
|
110
|
-
|
|
111
|
-
if (errcode === 40011 || errcode === 40012) {
|
|
112
|
-
throw new AppError({
|
|
113
|
-
code: "PROJECT_NOT_ACCESSIBLE",
|
|
114
|
-
message: errmsg,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new AppError({
|
|
119
|
-
code: "UPSTREAM_ERROR",
|
|
120
|
-
message: errmsg,
|
|
121
|
-
context: { errcode },
|
|
122
|
-
});
|
|
123
|
-
} catch (error: any) {
|
|
124
|
-
const status = error?.response?.status as number | undefined;
|
|
125
|
-
|
|
126
|
-
if (status === 429) {
|
|
127
|
-
throw new AppError({
|
|
128
|
-
code: "RATE_LIMITED",
|
|
129
|
-
message: "YApi rate limited",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (error instanceof AppError) {
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (error?.code === "ECONNABORTED") {
|
|
138
|
-
if (attempt + 1 < maxAttempts) {
|
|
139
|
-
attempt += 1;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
throw new AppError({
|
|
143
|
-
code: "UPSTREAM_TIMEOUT",
|
|
144
|
-
message: "YApi request timed out",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (attempt + 1 < maxAttempts) {
|
|
149
|
-
attempt += 1;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
throw new AppError({
|
|
154
|
-
code: "UPSTREAM_ERROR",
|
|
155
|
-
message: error?.message || "Failed to request YApi",
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
throw new AppError({
|
|
161
|
-
code: "UPSTREAM_ERROR",
|
|
162
|
-
message: "Unexpected request state",
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function flattenMenuInterfaces(menuData: unknown, projectId: number): YApiInterfaceSummary[] {
|
|
168
|
-
if (!Array.isArray(menuData)) return [];
|
|
169
|
-
|
|
170
|
-
const out: YApiInterfaceSummary[] = [];
|
|
171
|
-
|
|
172
|
-
for (const category of menuData as any[]) {
|
|
173
|
-
const list = category?.list;
|
|
174
|
-
if (!Array.isArray(list)) continue;
|
|
175
|
-
|
|
176
|
-
for (const item of list) {
|
|
177
|
-
const interfaceId = Number(item?._id ?? item?.id);
|
|
178
|
-
if (!Number.isFinite(interfaceId)) continue;
|
|
179
|
-
|
|
180
|
-
out.push({
|
|
181
|
-
interfaceId,
|
|
182
|
-
name: String(item?.title ?? item?.name ?? ""),
|
|
183
|
-
method: item?.method ? String(item.method) : undefined,
|
|
184
|
-
path: item?.path ? String(item.path) : undefined,
|
|
185
|
-
projectId,
|
|
186
|
-
score: 0,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return out;
|
|
192
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": "src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*.ts"]
|
|
14
|
-
}
|