@uwa4d/openapi-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/dist/spec.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ export type Engine = 'unity' | 'unreal';
2
+ export type HttpMethod = 'GET' | 'POST';
3
+ export interface Param {
4
+ name: string;
5
+ type: string;
6
+ required: boolean;
7
+ example?: string;
8
+ description: string;
9
+ }
10
+ export interface Variant {
11
+ title: string;
12
+ docEngines: Engine[];
13
+ supportedEngines: Engine[];
14
+ module: string;
15
+ description: string;
16
+ apiVersions: string[];
17
+ sdkRequirement?: string;
18
+ prerequisite?: string;
19
+ notes: string[];
20
+ /** 返回结构里的语义规则(如「x_axis 优先级规则」),误读会直接导致数据解释错误。 */
21
+ returnRules: string[];
22
+ query: Param[];
23
+ body: Param[];
24
+ docLine: number;
25
+ }
26
+ export interface Operation {
27
+ id: string;
28
+ name: string;
29
+ method: HttpMethod;
30
+ path: string;
31
+ summary: string;
32
+ engines: Engine[];
33
+ modules: string[];
34
+ apiVersions: string[];
35
+ defaultApiVersion: string;
36
+ query: Param[];
37
+ body: Param[];
38
+ returnsPresignUrl: boolean;
39
+ variants: Variant[];
40
+ }
41
+ export interface ApiSpec {
42
+ generatedAt: string;
43
+ sourceDoc: string;
44
+ operationCount: number;
45
+ operations: Operation[];
46
+ }
47
+ /**
48
+ * spec 在包根的 spec/ 目录下,运行时读取而非编译期 import,
49
+ * 这样 dist 保持纯代码,更新文档只需替换 json。
50
+ */
51
+ export declare function loadSpec(specPath?: string): ApiSpec;
package/dist/spec.js ADDED
@@ -0,0 +1,12 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * spec 在包根的 spec/ 目录下,运行时读取而非编译期 import,
6
+ * 这样 dist 保持纯代码,更新文档只需替换 json。
7
+ */
8
+ export function loadSpec(specPath) {
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const target = specPath ?? resolve(here, '..', 'spec', 'uwa-openapi.json');
11
+ return JSON.parse(readFileSync(target, 'utf8'));
12
+ }
@@ -0,0 +1,36 @@
1
+ import { type ZodTypeAny } from 'zod';
2
+ import type { Operation } from './spec.js';
3
+ import type { UwaClient } from './client.js';
4
+ export type NameCase = 'snake' | 'camel';
5
+ /**
6
+ * 预签名数据默认完整返回,不按条数截断——实测各接口解压后在 10 KB ~ 500 KB 之间,
7
+ * 使用者绝大多数时候要的就是全量。
8
+ *
9
+ * 只保留一道按字符数的兜底闸门,防止个别异常大的报告一次性撑爆模型上下文。
10
+ * 阈值取 50 万字符(约 16 万 token),高于目前观测到的最大值,正常报告不会触发。
11
+ */
12
+ export declare const DEFAULT_MAX_ROWS = 0;
13
+ export declare const DEFAULT_MAX_CHARS = 500000;
14
+ export declare function toolName(id: string, nameCase: NameCase, prefix: string): string;
15
+ export declare function buildInputSchema(op: Operation): Record<string, ZodTypeAny>;
16
+ export interface ToolResult {
17
+ content: {
18
+ type: 'text';
19
+ text: string;
20
+ }[];
21
+ isError?: boolean;
22
+ }
23
+ /**
24
+ * 曲线类接口的返回里有两套横轴:部分曲线自带 y_axis.{指标}.x_axis(步长 30 帧),
25
+ * 其余用外层共享 x_axis(逐帧)。两者点数不同,混用会把数据对错帧。
26
+ *
27
+ * 光在工具描述里写规则不够可靠,这里直接为每条曲线标注它实际该用哪根横轴、
28
+ * 帧号范围和步长,并在点数对不上时给出警告,从结构上消除歧义。
29
+ */
30
+ export declare function annotateCurveAxes(data: unknown): unknown;
31
+ export declare function makeHandler(op: Operation, client: UwaClient, defaultMaxRows: number, maxChars: number): (args: Record<string, unknown>) => Promise<ToolResult>;
32
+ export declare function toolConfig(op: Operation): {
33
+ title: string;
34
+ description: string;
35
+ inputSchema: Record<string, ZodTypeAny>;
36
+ };
package/dist/tools.js ADDED
@@ -0,0 +1,272 @@
1
+ import { z } from 'zod';
2
+ import { UwaApiError } from './client.js';
3
+ const ENGINE_LABEL = { unity: 'Unity', unreal: 'UE' };
4
+ /**
5
+ * 预签名数据默认完整返回,不按条数截断——实测各接口解压后在 10 KB ~ 500 KB 之间,
6
+ * 使用者绝大多数时候要的就是全量。
7
+ *
8
+ * 只保留一道按字符数的兜底闸门,防止个别异常大的报告一次性撑爆模型上下文。
9
+ * 阈值取 50 万字符(约 16 万 token),高于目前观测到的最大值,正常报告不会触发。
10
+ */
11
+ export const DEFAULT_MAX_ROWS = 0; // 0 表示不限条数
12
+ export const DEFAULT_MAX_CHARS = 500_000; // 0 表示不限字符
13
+ export function toolName(id, nameCase, prefix) {
14
+ const base = nameCase === 'camel' ? id.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase()) : id;
15
+ return prefix ? `${prefix}${base}` : base;
16
+ }
17
+ function zodFor(param) {
18
+ switch (param.type) {
19
+ case 'number':
20
+ return z.number();
21
+ case 'boolean':
22
+ return z.boolean();
23
+ case 'string[]':
24
+ return z.array(z.string());
25
+ case 'number[]':
26
+ return z.array(z.number());
27
+ case 'object[]':
28
+ return z.array(z.record(z.unknown()));
29
+ default:
30
+ return z.string();
31
+ }
32
+ }
33
+ /**
34
+ * 工具描述是模型选对接口的唯一依据,所以把引擎适用范围、版本差异、
35
+ * 前置依赖和文档里的坑都带上;这些信息全部来自官方文档,不做推测。
36
+ */
37
+ function describe(op) {
38
+ const lines = [op.summary || op.name];
39
+ const engines = op.engines.map((e) => ENGINE_LABEL[e]).join(' / ');
40
+ lines.push(`适用引擎:${engines}|模块:${op.modules.join(',')}|${op.method} ${op.path}`);
41
+ const sdk = op.variants.find((v) => v.sdkRequirement)?.sdkRequirement;
42
+ if (sdk)
43
+ lines.push(`SDK 要求:${sdk}`);
44
+ // 同一接口 Unity / UE 说明不同时分别标注,避免模型张冠李戴
45
+ const byEngine = new Map();
46
+ for (const v of op.variants) {
47
+ if (!v.description)
48
+ continue;
49
+ const label = v.docEngines.map((e) => ENGINE_LABEL[e]).join('/');
50
+ if (!byEngine.has(v.description))
51
+ byEngine.set(v.description, []);
52
+ byEngine.get(v.description).push(label);
53
+ }
54
+ if (byEngine.size === 1) {
55
+ lines.push([...byEngine.keys()][0]);
56
+ }
57
+ else {
58
+ for (const [desc, labels] of byEngine)
59
+ lines.push(`[${labels.join('/')}] ${desc}`);
60
+ }
61
+ const prereq = [...new Set(op.variants.map((v) => v.prerequisite).filter(Boolean))];
62
+ if (prereq.length)
63
+ lines.push(`前置依赖:${prereq.join(';')}`);
64
+ // 返回结构的语义规则必须完整给出:漏掉不会报错,但会让模型把数据读错
65
+ const rules = [...new Set(op.variants.flatMap((v) => v.returnRules))];
66
+ if (rules.length)
67
+ lines.push(`解读返回值时务必遵守:\n${rules.map((r) => `- ${r}`).join('\n')}`);
68
+ // 不做截断。文档里的注意事项常把最关键的约束放在最后一条
69
+ const notes = [...new Set(op.variants.flatMap((v) => v.notes))];
70
+ if (notes.length)
71
+ lines.push(`注意:\n${notes.map((n) => `- ${n}`).join('\n')}`);
72
+ if (op.returnsPresignUrl) {
73
+ lines.push('本接口返回预签名下载地址。工具已自动下载、解压并返回全部数据,无需再手动下载;' +
74
+ '只在明确只要前 N 条时才传 maxRows。若返回中出现 _truncated 字段,说明数据被截断,' +
75
+ '可用 download=false 取回 dataPresignUrl(有效期 600 秒)自行下载完整文件。');
76
+ }
77
+ return lines.join('\n');
78
+ }
79
+ export function buildInputSchema(op) {
80
+ const shape = {};
81
+ for (const p of [...op.query, ...op.body]) {
82
+ const base = zodFor(p).describe(p.example ? `${p.description}(示例:${p.example})` : p.description);
83
+ shape[p.name] = p.required ? base : base.optional();
84
+ }
85
+ if (op.apiVersions.length > 1) {
86
+ shape['apiVersion'] = z
87
+ .enum(op.apiVersions)
88
+ .optional()
89
+ .describe(`接口版本,默认 ${op.defaultApiVersion}。不同版本的返回字段有差异,详见文档`);
90
+ }
91
+ if (op.returnsPresignUrl) {
92
+ shape['download'] = z
93
+ .boolean()
94
+ .optional()
95
+ .describe('是否自动下载并解析预签名数据,默认 true。设为 false 时只返回 dataPresignUrl');
96
+ shape['maxRows'] = z
97
+ .number()
98
+ .optional()
99
+ .describe('可选的条目数上限。默认返回全部数据,只有在明确只需要前 N 条时才传这个参数');
100
+ }
101
+ return shape;
102
+ }
103
+ /** 找出对象里最长的那个数组字段,它通常就是主数据列表。 */
104
+ function mainArrayKey(obj) {
105
+ let best;
106
+ let bestLen = 0;
107
+ for (const [k, v] of Object.entries(obj)) {
108
+ if (Array.isArray(v) && v.length > bestLen) {
109
+ best = k;
110
+ bestLen = v.length;
111
+ }
112
+ }
113
+ return best;
114
+ }
115
+ /**
116
+ * 默认原样返回下载到的全部数据。
117
+ * maxRows > 0 时才按条数裁剪;无论如何都受 maxChars 兜底约束。
118
+ */
119
+ function summarize(payload, maxRows, maxChars) {
120
+ let body;
121
+ let total;
122
+ let rowTruncated = false;
123
+ if (payload.kind === 'json') {
124
+ const data = payload.json;
125
+ if (Array.isArray(data)) {
126
+ total = data.length;
127
+ rowTruncated = maxRows > 0 && data.length > maxRows;
128
+ body = rowTruncated ? data.slice(0, maxRows) : data;
129
+ }
130
+ else if (data && typeof data === 'object') {
131
+ const obj = data;
132
+ const key = mainArrayKey(obj);
133
+ const arr = key ? obj[key] : undefined;
134
+ total = arr?.length;
135
+ rowTruncated = maxRows > 0 && arr !== undefined && arr.length > maxRows;
136
+ body = rowTruncated && key ? { ...obj, [key]: arr.slice(0, maxRows) } : data;
137
+ }
138
+ else {
139
+ body = data;
140
+ }
141
+ }
142
+ else {
143
+ const lines = (payload.text ?? '').split('\n');
144
+ total = lines.length;
145
+ rowTruncated = maxRows > 0 && lines.length > maxRows;
146
+ body = rowTruncated ? lines.slice(0, maxRows).join('\n') : (payload.text ?? '');
147
+ }
148
+ let text = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
149
+ let charTruncated = false;
150
+ if (maxChars > 0 && text.length > maxChars) {
151
+ charTruncated = true;
152
+ text = text.slice(0, maxChars);
153
+ }
154
+ return { rowTruncated, charTruncated, total, chars: text.length, body: text };
155
+ }
156
+ function isObj(v) {
157
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
158
+ }
159
+ /**
160
+ * 曲线类接口的返回里有两套横轴:部分曲线自带 y_axis.{指标}.x_axis(步长 30 帧),
161
+ * 其余用外层共享 x_axis(逐帧)。两者点数不同,混用会把数据对错帧。
162
+ *
163
+ * 光在工具描述里写规则不够可靠,这里直接为每条曲线标注它实际该用哪根横轴、
164
+ * 帧号范围和步长,并在点数对不上时给出警告,从结构上消除歧义。
165
+ */
166
+ export function annotateCurveAxes(data) {
167
+ if (!isObj(data) || !isObj(data['y_axis']))
168
+ return data;
169
+ const yAxis = data['y_axis'];
170
+ const shared = isObj(data['x_axis']) && Array.isArray(data['x_axis']['data']) ? data['x_axis']['data'] : null;
171
+ const binding = {};
172
+ for (const [name, curve] of Object.entries(yAxis)) {
173
+ if (!isObj(curve))
174
+ continue;
175
+ const points = Array.isArray(curve['data']) ? curve['data'].length : 0;
176
+ const ownAxis = isObj(curve['x_axis']) && Array.isArray(curve['x_axis']['data']) ? curve['x_axis']['data'] : null;
177
+ const axis = ownAxis ?? shared;
178
+ const first = axis?.[0];
179
+ const last = axis?.[axis.length - 1];
180
+ binding[name] = {
181
+ points,
182
+ xAxisSource: ownAxis ? `y_axis.${name}.x_axis(该曲线专属)` : 'x_axis(外层共享)',
183
+ xAxisPoints: axis?.length ?? null,
184
+ xRange: axis?.length ? [first, last] : null,
185
+ step: axis && axis.length > 1 ? Number(axis[1]) - Number(axis[0]) : null,
186
+ ...(axis && axis.length !== points
187
+ ? { warning: `曲线 ${points} 个数据点与横轴 ${axis.length} 个点长度不一致,请勿按下标直接对齐` }
188
+ : {}),
189
+ };
190
+ }
191
+ if (Object.keys(binding).length === 0)
192
+ return data;
193
+ return {
194
+ ...data,
195
+ _axisBinding: binding,
196
+ _axisHint: '每条曲线按 _axisBinding[曲线名].xAxisSource 指定的横轴取值,不要统一使用外层 x_axis',
197
+ };
198
+ }
199
+ /** MCP 的 content.text 必须是字符串,undefined 会让客户端校验失败。 */
200
+ function asText(value) {
201
+ if (typeof value === 'string')
202
+ return value;
203
+ if (value === undefined || value === null)
204
+ return '(接口无返回内容)';
205
+ return JSON.stringify(value, null, 2);
206
+ }
207
+ export function makeHandler(op, client, defaultMaxRows, maxChars) {
208
+ return async (args) => {
209
+ try {
210
+ const apiVersion = args['apiVersion'] ?? op.defaultApiVersion;
211
+ const wantDownload = args['download'] ?? true;
212
+ const maxRows = args['maxRows'] ?? defaultMaxRows;
213
+ const query = {};
214
+ for (const p of op.query)
215
+ if (args[p.name] !== undefined)
216
+ query[p.name] = args[p.name];
217
+ let body;
218
+ if (op.body.length) {
219
+ body = {};
220
+ for (const p of op.body)
221
+ if (args[p.name] !== undefined)
222
+ body[p.name] = args[p.name];
223
+ }
224
+ const data = annotateCurveAxes(await client.call(op.method, op.path, apiVersion, query, body));
225
+ const presignUrl = op.returnsPresignUrl && data && typeof data === 'object'
226
+ ? data['dataPresignUrl']
227
+ : undefined;
228
+ if (!presignUrl || !wantDownload) {
229
+ return { content: [{ type: 'text', text: asText(data) }] };
230
+ }
231
+ const payload = await client.downloadPresign(presignUrl);
232
+ const { rowTruncated, charTruncated, total, chars, body: content } = summarize(payload, maxRows, maxChars);
233
+ const notes = [];
234
+ if (rowTruncated)
235
+ notes.push(`按 maxRows=${maxRows} 截断,去掉 maxRows 参数可取全量`);
236
+ if (charTruncated) {
237
+ notes.push(`内容超过 ${maxChars} 字符上限已截断,尾部数据缺失。` +
238
+ `如需完整数据,用 download=false 拿 dataPresignUrl 自行下载,或启动时调大 --max-chars`);
239
+ }
240
+ const meta = {
241
+ ...data,
242
+ _downloaded: true,
243
+ _format: payload.kind,
244
+ ...(total !== undefined ? { _totalItems: total } : {}),
245
+ _returnedChars: chars,
246
+ ...(notes.length ? { _truncated: notes.join(';') } : { _complete: '已返回全部数据' }),
247
+ };
248
+ delete meta['dataPresignUrl'];
249
+ return {
250
+ content: [
251
+ { type: 'text', text: asText(meta) },
252
+ { type: 'text', text: asText(content) },
253
+ ],
254
+ };
255
+ }
256
+ catch (err) {
257
+ const msg = err instanceof UwaApiError
258
+ ? err.message
259
+ : err instanceof Error
260
+ ? `调用 ${op.method} ${op.path} 失败:${err.message}`
261
+ : String(err);
262
+ return { content: [{ type: 'text', text: msg }], isError: true };
263
+ }
264
+ };
265
+ }
266
+ export function toolConfig(op) {
267
+ return {
268
+ title: op.name,
269
+ description: describe(op),
270
+ inputSchema: buildInputSchema(op),
271
+ };
272
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@uwa4d/openapi-mcp",
3
+ "version": "0.1.0",
4
+ "description": "UWA 开放平台 MCP Server,将 UWA Open API 暴露为 MCP 工具供 AI 助手调用",
5
+ "type": "module",
6
+ "bin": {
7
+ "uwa-openapi-mcp": "dist/cli.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "spec",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "gen": "tsx scripts/parse-docs.ts",
24
+ "build": "tsc -p tsconfig.json",
25
+ "dev": "tsx src/cli.ts",
26
+ "smoke": "tsx scripts/smoke-test.ts",
27
+ "prepublishOnly": "npm run build && node scripts/check-publish-tag.mjs",
28
+ "release": "node scripts/release.mjs",
29
+ "release:dry": "node scripts/release.mjs --dry-run",
30
+ "version:beta": "node scripts/bump.mjs beta",
31
+ "version:release": "node scripts/bump.mjs release"
32
+ },
33
+ "keywords": [
34
+ "uwa",
35
+ "mcp",
36
+ "model-context-protocol",
37
+ "openapi",
38
+ "game-performance"
39
+ ],
40
+ "license": "UNLICENSED",
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.30.0",
43
+ "commander": "^12.1.0",
44
+ "zod": "^3.23.8"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.14.0",
48
+ "tsx": "^4.19.0",
49
+ "typescript": "^5.5.0"
50
+ }
51
+ }