@wllcyg001/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/src/yapi.ts ADDED
@@ -0,0 +1,192 @@
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 ADDED
@@ -0,0 +1,14 @@
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
+ }