@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/README.md +189 -0
- package/dist/auth.d.ts +16 -0
- package/dist/auth.js +27 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.js +103 -0
- package/dist/presets.d.ts +20 -0
- package/dist/presets.js +51 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +27 -0
- package/dist/spec.d.ts +51 -0
- package/dist/spec.js +12 -0
- package/dist/tools.d.ts +36 -0
- package/dist/tools.js +272 -0
- package/package.json +51 -0
- package/spec/uwa-openapi.json +5414 -0
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @uwa4d/openapi-mcp
|
|
2
|
+
|
|
3
|
+
UWA 开放平台 MCP Server。把 UWA Open API 暴露为 MCP 工具,让 Cursor、Claude、Trae 等 AI 助手直接查询 GOT Online 性能报告数据。
|
|
4
|
+
|
|
5
|
+
装好之后,可以直接用自然语言提问,例如:
|
|
6
|
+
|
|
7
|
+
- 「列出我最近 10 份 GOT Online 报告」
|
|
8
|
+
- 「分析 xxx 报告的卡顿帧,找出耗时最高的几个函数」
|
|
9
|
+
- 「对比这两份报告的内存占用差异」
|
|
10
|
+
|
|
11
|
+
## 准备
|
|
12
|
+
|
|
13
|
+
1. **Node.js 18 或更高版本**(`node -v` 确认)。`npx` 随 Node 一起安装,不需要单独装本包。
|
|
14
|
+
2. **AppId / AppSecret**:向 UWA 申请。
|
|
15
|
+
|
|
16
|
+
## 接入
|
|
17
|
+
|
|
18
|
+
在 AI 客户端的 MCP 配置里加上:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"uwa-openapi": {
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": ["-y", "@uwa4d/openapi-mcp", "mcp"],
|
|
26
|
+
"env": {
|
|
27
|
+
"UWA_MCP_APP_ID": "<your_app_id>",
|
|
28
|
+
"UWA_MCP_APP_SECRET": "<your_app_secret>"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
配置文件位置:
|
|
36
|
+
|
|
37
|
+
| 客户端 | 路径 |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| Cursor | `~/.cursor/mcp.json`(全局)或项目下 `.cursor/mcp.json` |
|
|
40
|
+
| Claude Desktop | `claude_desktop_config.json` |
|
|
41
|
+
| Trae | 设置面板中的 MCP 配置 |
|
|
42
|
+
|
|
43
|
+
改完重启客户端即可。
|
|
44
|
+
|
|
45
|
+
凭证也可以走命令行参数,适配不支持 `env` 字段的客户端:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"uwa-openapi": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "@uwa4d/openapi-mcp", "mcp", "-a", "<app_id>", "-s", "<app_secret>"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> 配置文件里含有凭证,注意不要提交到公开代码仓库。
|
|
59
|
+
|
|
60
|
+
### 先验证一下
|
|
61
|
+
|
|
62
|
+
配置前可以在终端确认凭证和网络是否通:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx -y @uwa4d/openapi-mcp check -a <app_id> -s <app_secret>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
它会用你的凭证真实调用一次接口。输出「凭证有效,接口连通」就说明没问题,失败时会给出具体原因和排查方向。
|
|
69
|
+
|
|
70
|
+
## 命令行参数
|
|
71
|
+
|
|
72
|
+
三个子命令:`mcp` 启动服务(默认)、`check` 验证凭证、`list-tools` 查看工具清单。
|
|
73
|
+
|
|
74
|
+
以下参数用于 `mcp`:
|
|
75
|
+
|
|
76
|
+
| 参数 | 简写 | 说明 | 环境变量 |
|
|
77
|
+
|------|------|------|----------|
|
|
78
|
+
| `--app-id` | `-a` | AppId | `UWA_MCP_APP_ID` |
|
|
79
|
+
| `--app-secret` | `-s` | AppSecret | `UWA_MCP_APP_SECRET` |
|
|
80
|
+
| `--base-url` | `-b` | 接口地址,默认 `https://secure-api.uwa4d.com` | `UWA_MCP_API_BASE_URL` |
|
|
81
|
+
| `--sandbox` | | 切到测试环境 `https://sandbox-api.uwa4d.com` | |
|
|
82
|
+
| `--tool` | `-t` | 加载哪些工具,逗号分隔,默认 `preset.default` | `UWA_MCP_TOOL` |
|
|
83
|
+
| `--engine` | `-e` | 只加载适用于 `unity` / `unreal` 的工具,默认 `all` | `UWA_MCP_ENGINE` |
|
|
84
|
+
| `--tool-name-case` | `-c` | 工具命名风格 `snake` / `camel`,默认 `snake` | |
|
|
85
|
+
| `--tool-name-prefix` | | 工具名前缀,便于与其他 MCP 区分 | `UWA_MCP_TOOL_NAME_PREFIX` |
|
|
86
|
+
| `--max-rows` | | 预签名数据的条目数上限,`0` 表示不限(默认返回全部) | |
|
|
87
|
+
| `--max-chars` | | 单次返回的字符数上限,防止超大数据撑爆上下文,`0` 表示不限 | |
|
|
88
|
+
| `--timeout` | | 单次请求超时毫秒数,默认 60000 | |
|
|
89
|
+
|
|
90
|
+
## 工具过滤
|
|
91
|
+
|
|
92
|
+
全部接口一次性注册会让模型难以选择,默认只加载 `preset.default`。用 `--tool` 按需调整:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 默认高频工具集
|
|
96
|
+
-t preset.default
|
|
97
|
+
|
|
98
|
+
# 全部只读接口
|
|
99
|
+
-t preset.readonly
|
|
100
|
+
|
|
101
|
+
# 全部工具
|
|
102
|
+
-t all
|
|
103
|
+
|
|
104
|
+
# 按模块加载:common / gotonline / overview / gpu / mono / lua / localtest
|
|
105
|
+
-t overview,gpu
|
|
106
|
+
|
|
107
|
+
# 组合:默认集 + 所有预签名类接口 + 单个指定工具
|
|
108
|
+
-t preset.default,presign,gotonline_overview_stack_id_map
|
|
109
|
+
|
|
110
|
+
# 只做 Unity 项目,砍掉 UE 专用工具
|
|
111
|
+
-t preset.default -e unity
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
查看全部工具和可用过滤键:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npx -y @uwa4d/openapi-mcp list-tools
|
|
118
|
+
npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 大数据接口的返回方式
|
|
122
|
+
|
|
123
|
+
堆栈树、资源列表、卡顿帧等接口不直接返回数据,而是返回一个有效期 600 秒的预签名下载地址。
|
|
124
|
+
|
|
125
|
+
**这类工具会自动下载、解压并返回全部数据**,不需要你手动下载。返回里带有完整性标记:
|
|
126
|
+
|
|
127
|
+
- `_complete: "已返回全部数据"` — 数据完整
|
|
128
|
+
- `_totalItems` — 主数据列表的条目总数
|
|
129
|
+
- `_returnedChars` — 实际返回的字符数
|
|
130
|
+
|
|
131
|
+
只在明确只要前 N 条时才传 `maxRows`。另有一道字符数兜底闸门(`--max-chars`,默认 50 万字符),正常报告不会触发;一旦触发会在 `_truncated` 里说清楚,此时可以用 `download: false` 取回 `dataPresignUrl` 自行下载完整文件。
|
|
132
|
+
|
|
133
|
+
## 曲线接口的双横轴
|
|
134
|
+
|
|
135
|
+
曲线类接口(如 `gotonline_overview_indicator_curve_dashboard`)的返回里有**两套横轴**:部分曲线自带 `y_axis.{指标名}.x_axis`,其余用外层共享 `x_axis`。两者点数不同,混用会把数据对错帧。
|
|
136
|
+
|
|
137
|
+
工具会在保留原始结构的前提下追加 `_axisBinding`,逐条说明该用哪根横轴:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"y_axis": { "...": "原始数据原样保留" },
|
|
142
|
+
"x_axis": { "...": "原始数据原样保留" },
|
|
143
|
+
"_axisBinding": {
|
|
144
|
+
"fps": {
|
|
145
|
+
"points": 14,
|
|
146
|
+
"xAxisSource": "y_axis.fps.x_axis(该曲线专属)",
|
|
147
|
+
"xAxisPoints": 14,
|
|
148
|
+
"xRange": [0, 390],
|
|
149
|
+
"step": 30
|
|
150
|
+
},
|
|
151
|
+
"Application_TargetFramerate_Count": {
|
|
152
|
+
"points": 407,
|
|
153
|
+
"xAxisSource": "x_axis(外层共享)",
|
|
154
|
+
"xAxisPoints": 407,
|
|
155
|
+
"xRange": [1, 407],
|
|
156
|
+
"step": 1
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
数据点数与横轴点数不一致时会额外给出 `warning`。
|
|
163
|
+
|
|
164
|
+
## 版本
|
|
165
|
+
|
|
166
|
+
`npx -y @uwa4d/openapi-mcp` 会自动拉取最新稳定版。可以按需要锁定版本,写在包名后面:
|
|
167
|
+
|
|
168
|
+
| 写法 | 行为 | 适用 |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| `@uwa4d/openapi-mcp` | 跟随最新稳定版,自动升级 | 默认,大多数用户 |
|
|
171
|
+
| `@uwa4d/openapi-mcp@0.1.0` | 锁死某个版本 | 生产流水线,要求完全可复现 |
|
|
172
|
+
| `@uwa4d/openapi-mcp@beta` | 跟随测试版 | 配合 UWA 验证新接口 |
|
|
173
|
+
|
|
174
|
+
测试版只发布在 `beta` 标签下,默认写法不会拉到测试版。
|
|
175
|
+
|
|
176
|
+
## 常见问题
|
|
177
|
+
|
|
178
|
+
**启动后客户端里看不到工具**
|
|
179
|
+
检查 Node 版本是否 ≥ 18,以及配置文件 JSON 格式是否正确(多一个逗号就会整个失效)。改完配置需要重启客户端。
|
|
180
|
+
|
|
181
|
+
**提示凭证错误**
|
|
182
|
+
用 `check` 命令在终端单独验证一次,排除是客户端配置传参的问题。注意测试环境和生产环境的凭证不通用。
|
|
183
|
+
|
|
184
|
+
**返回「数据服务错误」**
|
|
185
|
+
部分接口区分报告生成时间和引擎类型,例如 Unity 2026-07-09 之前的报告用 `get_overview_statistic_v1`,之后的用 `get_overview_statistic_v2`。工具描述里注明了适用范围。
|
|
186
|
+
|
|
187
|
+
## 许可
|
|
188
|
+
|
|
189
|
+
Copyright © UWA. 保留所有权利。
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
appId: string;
|
|
3
|
+
appSecret: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* UWA 开放平台签名。
|
|
7
|
+
*
|
|
8
|
+
* sign = SHA256(appSecret 字节 ‖ (appId + "/n" + timestamp) 字节)
|
|
9
|
+
*
|
|
10
|
+
* 注意两个容易踩的点:
|
|
11
|
+
* - "/n" 是字面的斜杠加字母 n,不是换行符
|
|
12
|
+
* - appSecret 是作为哈希前缀喂进去的,不是标准 HMAC
|
|
13
|
+
*/
|
|
14
|
+
export declare function sign(creds: Credentials, timestampSec: number): string;
|
|
15
|
+
/** 服务端允许的时间偏差是 20 分钟,这里每次请求都重新生成,不做缓存。 */
|
|
16
|
+
export declare function authHeaders(creds: Credentials, apiVersion: string): Record<string, string>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* UWA 开放平台签名。
|
|
4
|
+
*
|
|
5
|
+
* sign = SHA256(appSecret 字节 ‖ (appId + "/n" + timestamp) 字节)
|
|
6
|
+
*
|
|
7
|
+
* 注意两个容易踩的点:
|
|
8
|
+
* - "/n" 是字面的斜杠加字母 n,不是换行符
|
|
9
|
+
* - appSecret 是作为哈希前缀喂进去的,不是标准 HMAC
|
|
10
|
+
*/
|
|
11
|
+
export function sign(creds, timestampSec) {
|
|
12
|
+
return createHash('sha256')
|
|
13
|
+
.update(creds.appSecret, 'utf8')
|
|
14
|
+
.update(`${creds.appId}/n${timestampSec}`, 'utf8')
|
|
15
|
+
.digest('hex');
|
|
16
|
+
}
|
|
17
|
+
/** 服务端允许的时间偏差是 20 分钟,这里每次请求都重新生成,不做缓存。 */
|
|
18
|
+
export function authHeaders(creds, apiVersion) {
|
|
19
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
20
|
+
return {
|
|
21
|
+
'x-uwa-api-minor-version': apiVersion,
|
|
22
|
+
'x-uwa-open-api-app-id': creds.appId,
|
|
23
|
+
'x-uwa-open-api-sign': sign(creds, ts),
|
|
24
|
+
'x-uwa-open-api-timestamp': String(ts),
|
|
25
|
+
'x-uwa-open-api-sign-version': 'v1',
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
|
+
import { UwaClient } from './client.js';
|
|
4
|
+
import { loadSpec } from './spec.js';
|
|
5
|
+
import { filterKeys, selectOperations } from './presets.js';
|
|
6
|
+
import { DEFAULT_MAX_CHARS, DEFAULT_MAX_ROWS, toolName } from './tools.js';
|
|
7
|
+
import { PACKAGE_VERSION, startStdio } from './server.js';
|
|
8
|
+
const DEFAULT_BASE_URL = 'https://secure-api.uwa4d.com';
|
|
9
|
+
const SANDBOX_BASE_URL = 'https://sandbox-api.uwa4d.com';
|
|
10
|
+
function splitList(value) {
|
|
11
|
+
return value
|
|
12
|
+
.split(',')
|
|
13
|
+
.map((s) => s.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
/** 凭证不落配置文件时可以只给环境变量,命令行参数优先级更高。 */
|
|
17
|
+
function resolveCredentials(opts) {
|
|
18
|
+
const appId = opts.appId ?? process.env['UWA_MCP_APP_ID'];
|
|
19
|
+
const appSecret = opts.appSecret ?? process.env['UWA_MCP_APP_SECRET'];
|
|
20
|
+
if (!appId || !appSecret) {
|
|
21
|
+
console.error('缺少凭证。请通过 -a/--app-id 与 -s/--app-secret 传入,\n' +
|
|
22
|
+
'或设置环境变量 UWA_MCP_APP_ID / UWA_MCP_APP_SECRET。');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
return { appId, appSecret };
|
|
26
|
+
}
|
|
27
|
+
const program = new Command();
|
|
28
|
+
program
|
|
29
|
+
.name('uwa-openapi-mcp')
|
|
30
|
+
.description('UWA 开放平台 MCP Server,把 UWA Open API 暴露为 MCP 工具')
|
|
31
|
+
.version(PACKAGE_VERSION);
|
|
32
|
+
program
|
|
33
|
+
.command('mcp', { isDefault: true })
|
|
34
|
+
.description('启动 MCP Server')
|
|
35
|
+
.option('-a, --app-id <id>', 'UWA 开放平台 AppId(环境变量 UWA_MCP_APP_ID)')
|
|
36
|
+
.option('-s, --app-secret <secret>', 'UWA 开放平台 AppSecret(环境变量 UWA_MCP_APP_SECRET)')
|
|
37
|
+
.option('-b, --base-url <url>', `接口地址(环境变量 UWA_MCP_API_BASE_URL)`, process.env['UWA_MCP_API_BASE_URL'] ?? DEFAULT_BASE_URL)
|
|
38
|
+
.option('--sandbox', `使用测试环境 ${SANDBOX_BASE_URL}`, false)
|
|
39
|
+
.option('-t, --tool <list>', '加载的工具,逗号分隔,可用 preset.default / preset.readonly / all / 模块名 / 工具名', splitList)
|
|
40
|
+
.addOption(new Option('-e, --engine <engine>', '只加载适用于该引擎的工具')
|
|
41
|
+
.choices(['unity', 'unreal', 'all'])
|
|
42
|
+
.default(process.env['UWA_MCP_ENGINE'] ?? 'all'))
|
|
43
|
+
.addOption(new Option('-c, --tool-name-case <case>', '工具命名风格').choices(['snake', 'camel']).default('snake'))
|
|
44
|
+
.option('--tool-name-prefix <prefix>', '工具名前缀,便于与其他 MCP 区分', process.env['UWA_MCP_TOOL_NAME_PREFIX'] ?? '')
|
|
45
|
+
.option('--max-rows <n>', '预签名数据的条目数上限,0 表示不限(默认返回全部)', (v) => Number.parseInt(v, 10), DEFAULT_MAX_ROWS)
|
|
46
|
+
.option('--max-chars <n>', '单次返回的字符数兜底上限,防止异常大的数据撑爆上下文,0 表示不限', (v) => Number.parseInt(v, 10), DEFAULT_MAX_CHARS)
|
|
47
|
+
.option('--timeout <ms>', '单次请求超时(毫秒)', (v) => Number.parseInt(v, 10), 60_000)
|
|
48
|
+
.option('--spec <path>', '自定义 spec 文件路径,默认用内置的')
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
const { appId, appSecret } = resolveCredentials(opts);
|
|
51
|
+
await startStdio({
|
|
52
|
+
appId,
|
|
53
|
+
appSecret,
|
|
54
|
+
baseUrl: opts.sandbox ? SANDBOX_BASE_URL : opts.baseUrl,
|
|
55
|
+
tools: opts.tool ?? splitList(process.env['UWA_MCP_TOOL'] ?? 'preset.default'),
|
|
56
|
+
engine: opts.engine,
|
|
57
|
+
nameCase: opts.toolNameCase,
|
|
58
|
+
namePrefix: opts.toolNamePrefix,
|
|
59
|
+
maxRows: opts.maxRows,
|
|
60
|
+
maxChars: opts.maxChars,
|
|
61
|
+
timeoutMs: opts.timeout,
|
|
62
|
+
specPath: opts.spec,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
program
|
|
66
|
+
.command('list-tools')
|
|
67
|
+
.description('列出可用工具及其过滤键,用于挑选 --tool 的取值')
|
|
68
|
+
.option('-t, --tool <list>', '按过滤键预览选中结果', splitList)
|
|
69
|
+
.addOption(new Option('-e, --engine <engine>', '按引擎过滤').choices(['unity', 'unreal', 'all']).default('all'))
|
|
70
|
+
.option('--spec <path>', '自定义 spec 文件路径')
|
|
71
|
+
.action((opts) => {
|
|
72
|
+
const spec = loadSpec(opts.spec);
|
|
73
|
+
const selected = new Set(selectOperations(spec.operations, {
|
|
74
|
+
tools: opts.tool ?? ['all'],
|
|
75
|
+
engine: opts.engine,
|
|
76
|
+
}).map((o) => o.id));
|
|
77
|
+
const byModule = new Map();
|
|
78
|
+
for (const op of spec.operations) {
|
|
79
|
+
const key = op.modules.join(',');
|
|
80
|
+
if (!byModule.has(key))
|
|
81
|
+
byModule.set(key, []);
|
|
82
|
+
byModule.get(key).push(op);
|
|
83
|
+
}
|
|
84
|
+
for (const [module, ops] of [...byModule].sort()) {
|
|
85
|
+
console.log(`\n## ${module}`);
|
|
86
|
+
for (const op of ops) {
|
|
87
|
+
const mark = selected.has(op.id) ? '*' : ' ';
|
|
88
|
+
const engines = op.engines.map((e) => (e === 'unity' ? 'U3D' : 'UE')).join('+');
|
|
89
|
+
console.log(`${mark} ${toolName(op.id, 'snake', '').padEnd(52)} ${op.method.padEnd(4)} ${engines.padEnd(7)} ${op.name}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log(`\n共 ${spec.operations.length} 个工具,当前条件选中 ${selected.size} 个(* 标记)`);
|
|
93
|
+
console.log(`可用过滤键:${[...new Set(spec.operations.flatMap(filterKeys))].sort().join(', ')}`);
|
|
94
|
+
});
|
|
95
|
+
program
|
|
96
|
+
.command('check')
|
|
97
|
+
.description('验证凭证与网络连通性,用于接入前自查')
|
|
98
|
+
.option('-a, --app-id <id>', 'UWA 开放平台 AppId(环境变量 UWA_MCP_APP_ID)')
|
|
99
|
+
.option('-s, --app-secret <secret>', 'UWA 开放平台 AppSecret(环境变量 UWA_MCP_APP_SECRET)')
|
|
100
|
+
.option('-b, --base-url <url>', '接口地址', process.env['UWA_MCP_API_BASE_URL'] ?? DEFAULT_BASE_URL)
|
|
101
|
+
.option('--sandbox', `使用测试环境 ${SANDBOX_BASE_URL}`, false)
|
|
102
|
+
.option('--timeout <ms>', '单次请求超时(毫秒)', (v) => Number.parseInt(v, 10), 30_000)
|
|
103
|
+
.action(async (opts) => {
|
|
104
|
+
const { appId, appSecret } = resolveCredentials(opts);
|
|
105
|
+
const baseUrl = opts.sandbox ? SANDBOX_BASE_URL : opts.baseUrl;
|
|
106
|
+
console.log(`接口地址:${baseUrl}`);
|
|
107
|
+
console.log(`AppId:${appId}`);
|
|
108
|
+
const client = new UwaClient({
|
|
109
|
+
baseUrl,
|
|
110
|
+
credentials: { appId, appSecret },
|
|
111
|
+
timeoutMs: opts.timeout,
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
// 挑一个最轻的只读接口做真实调用,凭证、签名、网络三者一次性验完
|
|
115
|
+
const data = await client.call('GET', '/openapi/v1/data/report/record/batch', 'v1.0.1', {
|
|
116
|
+
days: 7,
|
|
117
|
+
pageNo: 1,
|
|
118
|
+
pageSize: 10,
|
|
119
|
+
});
|
|
120
|
+
const total = data?.total;
|
|
121
|
+
console.log('\n凭证有效,接口连通。');
|
|
122
|
+
if (typeof total === 'number')
|
|
123
|
+
console.log(`最近 7 天有 ${total} 份报告。`);
|
|
124
|
+
console.log('\n可以按 README 配置 MCP 客户端了。');
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error(`\n调用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
128
|
+
console.error('\n排查方向:\n' +
|
|
129
|
+
' - 凭证是否填错或已停用\n' +
|
|
130
|
+
' - 是否该用测试环境(加 --sandbox)\n' +
|
|
131
|
+
' - 本机到接口地址的网络是否通(代理、防火墙)');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
136
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Credentials } from './auth.js';
|
|
2
|
+
import type { HttpMethod } from './spec.js';
|
|
3
|
+
export interface ClientOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
credentials: Credentials;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class UwaApiError extends Error {
|
|
9
|
+
readonly code: number;
|
|
10
|
+
readonly rawMessage: string;
|
|
11
|
+
constructor(code: number, rawMessage: string, message: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class UwaClient {
|
|
14
|
+
private readonly opts;
|
|
15
|
+
constructor(opts: ClientOptions);
|
|
16
|
+
call(method: HttpMethod, path: string, apiVersion: string, query: Record<string, unknown>, body?: Record<string, unknown>): Promise<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* 下载预签名地址指向的数据。
|
|
19
|
+
*
|
|
20
|
+
* 这些文件动辄几十 MB 且多为 gzip,必须在这里解压、解析、截断,
|
|
21
|
+
* 直接把原始内容交给模型会撑爆上下文。
|
|
22
|
+
*/
|
|
23
|
+
downloadPresign(presignUrl: string): Promise<{
|
|
24
|
+
kind: 'json' | 'text';
|
|
25
|
+
json?: unknown;
|
|
26
|
+
text?: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib';
|
|
2
|
+
import { authHeaders } from './auth.js';
|
|
3
|
+
function isEnvelope(v) {
|
|
4
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
5
|
+
return false;
|
|
6
|
+
const o = v;
|
|
7
|
+
return o['status'] === 'success' || o['status'] === 'failed';
|
|
8
|
+
}
|
|
9
|
+
/** 常见错误码的排查提示,直接给到模型,省掉一轮试错。 */
|
|
10
|
+
const ERROR_HINTS = {
|
|
11
|
+
20001: '业务参数不合法,核对参数名、取值范围和必填项(注意批量接口的参数名多为复数,如 dataKeys)',
|
|
12
|
+
23508: '数据服务错误,常见原因是报告不适用该接口版本(如新报告调用了 1.0 接口,或旧报告调用了 2.0 接口),改用对应版本重试',
|
|
13
|
+
24050: '该账号未开通 Open API 权限,请联系 UWA 工作人员开通',
|
|
14
|
+
24052: '请求参数有误,请对照接口文档检查',
|
|
15
|
+
24054: 'AppId 不存在,检查 appId 是否正确、是否用错了环境(sandbox / 线上凭证不通用)',
|
|
16
|
+
24056: '签名错误,检查 appSecret 是否正确',
|
|
17
|
+
24057: '时间戳过期,本机时间与服务端偏差不能超过 20 分钟',
|
|
18
|
+
30001: '服务端错误,常见原因是用错了引擎对应的接口(如对 UE 报告调用了 Unity 专用接口)',
|
|
19
|
+
};
|
|
20
|
+
export class UwaApiError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
rawMessage;
|
|
23
|
+
constructor(code, rawMessage, message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.rawMessage = rawMessage;
|
|
27
|
+
this.name = 'UwaApiError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class UwaClient {
|
|
31
|
+
opts;
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
this.opts = opts;
|
|
34
|
+
}
|
|
35
|
+
async call(method, path, apiVersion, query, body) {
|
|
36
|
+
const url = new URL(path, this.opts.baseUrl);
|
|
37
|
+
for (const [k, v] of Object.entries(query)) {
|
|
38
|
+
if (v === undefined || v === null || v === '')
|
|
39
|
+
continue;
|
|
40
|
+
// 文档约定:多值参数一律用英文逗号分隔,不用重复 key
|
|
41
|
+
url.searchParams.set(k, Array.isArray(v) ? v.join(',') : String(v));
|
|
42
|
+
}
|
|
43
|
+
const headers = authHeaders(this.opts.credentials, apiVersion);
|
|
44
|
+
if (body)
|
|
45
|
+
headers['Content-Type'] = 'application/json';
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method,
|
|
48
|
+
headers,
|
|
49
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
50
|
+
signal: AbortSignal.timeout(this.opts.timeoutMs),
|
|
51
|
+
});
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
if (!res.ok && !text) {
|
|
54
|
+
throw new UwaApiError(res.status, 'HTTP_ERROR', `HTTP ${res.status} ${res.statusText}`);
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(text);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// idmap 等接口返回 octet-stream,非 JSON 时原样交回
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
// idmap 一类接口直接返回业务数据,没有 {status, data} 信封,
|
|
65
|
+
// 只有识别出信封结构时才拆包,否则原样返回,避免拆出 undefined。
|
|
66
|
+
if (!isEnvelope(parsed))
|
|
67
|
+
return parsed;
|
|
68
|
+
if (parsed.status === 'failed' || parsed.error) {
|
|
69
|
+
const code = parsed.error?.code ?? -1;
|
|
70
|
+
const raw = parsed.error?.data?.rawMessage ?? '';
|
|
71
|
+
const hint = ERROR_HINTS[code];
|
|
72
|
+
throw new UwaApiError(code, raw, `[${code}] ${parsed.error?.message ?? '请求失败'}${raw ? ` (${raw})` : ''}${hint ? `\n排查建议:${hint}` : ''}`);
|
|
73
|
+
}
|
|
74
|
+
return parsed.data ?? {};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 下载预签名地址指向的数据。
|
|
78
|
+
*
|
|
79
|
+
* 这些文件动辄几十 MB 且多为 gzip,必须在这里解压、解析、截断,
|
|
80
|
+
* 直接把原始内容交给模型会撑爆上下文。
|
|
81
|
+
*/
|
|
82
|
+
async downloadPresign(presignUrl) {
|
|
83
|
+
const res = await fetch(presignUrl, { signal: AbortSignal.timeout(this.opts.timeoutMs) });
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw new Error(`下载预签名数据失败:HTTP ${res.status} ${res.statusText}`);
|
|
86
|
+
let buf = Buffer.from(await res.arrayBuffer());
|
|
87
|
+
// fetch 只在响应头声明 Content-Encoding 时自动解压;
|
|
88
|
+
// OSS 上不少文件是内容本身就是 gzip,靠 magic bytes 兜底
|
|
89
|
+
if (buf.length > 2 && buf[0] === 0x1f && buf[1] === 0x8b)
|
|
90
|
+
buf = gunzipSync(buf);
|
|
91
|
+
const text = buf.toString('utf8');
|
|
92
|
+
const head = text.trimStart()[0];
|
|
93
|
+
if (head === '{' || head === '[') {
|
|
94
|
+
try {
|
|
95
|
+
return { kind: 'json', json: JSON.parse(text) };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* 落到文本分支 */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { kind: 'text', text };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Engine, Operation } from './spec.js';
|
|
2
|
+
/**
|
|
3
|
+
* 默认加载的高频工具。
|
|
4
|
+
*
|
|
5
|
+
* 全量 55 个接口一次性注册会让 tools/list 过长、模型选择困难,
|
|
6
|
+
* 所以默认只给覆盖 80% 场景的这一组,其余通过 --tool 按需加载。
|
|
7
|
+
*/
|
|
8
|
+
export declare const PRESET_DEFAULT: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* 每个接口对应的一组过滤键,用户在 --tool 里写任意一个即可命中。
|
|
11
|
+
* 键包含:工具 id、所属模块、preset 名、以及 presign 这一横切分类。
|
|
12
|
+
*/
|
|
13
|
+
export declare function filterKeys(op: Operation): string[];
|
|
14
|
+
export interface SelectOptions {
|
|
15
|
+
/** --tool 的取值,未指定时用 preset.default */
|
|
16
|
+
tools?: string[];
|
|
17
|
+
/** --engine 过滤,'all' 表示不过滤 */
|
|
18
|
+
engine?: Engine | 'all';
|
|
19
|
+
}
|
|
20
|
+
export declare function selectOperations(all: Operation[], opts: SelectOptions): Operation[];
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认加载的高频工具。
|
|
3
|
+
*
|
|
4
|
+
* 全量 55 个接口一次性注册会让 tools/list 过长、模型选择困难,
|
|
5
|
+
* 所以默认只给覆盖 80% 场景的这一组,其余通过 --tool 按需加载。
|
|
6
|
+
*/
|
|
7
|
+
export const PRESET_DEFAULT = [
|
|
8
|
+
'list_report_records',
|
|
9
|
+
'get_report_detail',
|
|
10
|
+
'get_report_url',
|
|
11
|
+
'list_scenes',
|
|
12
|
+
'gotonline_meta_custom',
|
|
13
|
+
'get_overview_statistic_v1',
|
|
14
|
+
'get_overview_statistic_v2',
|
|
15
|
+
'get_ue_overview_statistic_v2',
|
|
16
|
+
'get_scene_statistic_v1',
|
|
17
|
+
'get_scene_statistic_v2',
|
|
18
|
+
'get_gpu_statistic',
|
|
19
|
+
'get_mono_statistic',
|
|
20
|
+
'get_localtest_statistic',
|
|
21
|
+
'get_localtest_failed_assets',
|
|
22
|
+
'list_optimization_tasks',
|
|
23
|
+
'gotonline_overview_indicator_statistic_dashboard',
|
|
24
|
+
'gotonline_overview_indicator_curve_dashboard',
|
|
25
|
+
'gotonline_overview_memory_manage_data_report',
|
|
26
|
+
'gotonline_overview_method_group_statistic',
|
|
27
|
+
'gotonline_overview_stack_stutter_full_presign',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* 每个接口对应的一组过滤键,用户在 --tool 里写任意一个即可命中。
|
|
31
|
+
* 键包含:工具 id、所属模块、preset 名、以及 presign 这一横切分类。
|
|
32
|
+
*/
|
|
33
|
+
export function filterKeys(op) {
|
|
34
|
+
const keys = new Set([op.id, 'preset.all', 'all', ...op.modules]);
|
|
35
|
+
if (op.returnsPresignUrl)
|
|
36
|
+
keys.add('presign');
|
|
37
|
+
if (op.method === 'GET')
|
|
38
|
+
keys.add('preset.readonly');
|
|
39
|
+
if (PRESET_DEFAULT.includes(op.id))
|
|
40
|
+
keys.add('preset.default');
|
|
41
|
+
return [...keys];
|
|
42
|
+
}
|
|
43
|
+
export function selectOperations(all, opts) {
|
|
44
|
+
const wanted = new Set((opts.tools?.length ? opts.tools : ['preset.default']).map((t) => t.trim()).filter(Boolean));
|
|
45
|
+
const engine = opts.engine ?? 'all';
|
|
46
|
+
return all.filter((op) => {
|
|
47
|
+
if (engine !== 'all' && !op.engines.includes(engine))
|
|
48
|
+
return false;
|
|
49
|
+
return filterKeys(op).some((k) => wanted.has(k));
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type Engine } from './spec.js';
|
|
3
|
+
import { type NameCase } from './tools.js';
|
|
4
|
+
export interface ServerOptions {
|
|
5
|
+
appId: string;
|
|
6
|
+
appSecret: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
tools: string[];
|
|
9
|
+
engine: Engine | 'all';
|
|
10
|
+
nameCase: NameCase;
|
|
11
|
+
namePrefix: string;
|
|
12
|
+
/** 0 表示不按条数截断(默认) */
|
|
13
|
+
maxRows: number;
|
|
14
|
+
/** 0 表示不按字符数截断 */
|
|
15
|
+
maxChars: number;
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
specPath?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const PACKAGE_VERSION = "0.1.0";
|
|
20
|
+
export declare function createServer(opts: ServerOptions): {
|
|
21
|
+
server: McpServer;
|
|
22
|
+
toolCount: number;
|
|
23
|
+
total: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function startStdio(opts: ServerOptions): Promise<void>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { UwaClient } from './client.js';
|
|
4
|
+
import { selectOperations } from './presets.js';
|
|
5
|
+
import { loadSpec } from './spec.js';
|
|
6
|
+
import { makeHandler, toolConfig, toolName } from './tools.js';
|
|
7
|
+
export const PACKAGE_VERSION = '0.1.0';
|
|
8
|
+
export function createServer(opts) {
|
|
9
|
+
const spec = loadSpec(opts.specPath);
|
|
10
|
+
const operations = selectOperations(spec.operations, { tools: opts.tools, engine: opts.engine });
|
|
11
|
+
const client = new UwaClient({
|
|
12
|
+
baseUrl: opts.baseUrl,
|
|
13
|
+
credentials: { appId: opts.appId, appSecret: opts.appSecret },
|
|
14
|
+
timeoutMs: opts.timeoutMs,
|
|
15
|
+
});
|
|
16
|
+
const server = new McpServer({ name: 'uwa-openapi-mcp', version: PACKAGE_VERSION });
|
|
17
|
+
for (const op of operations) {
|
|
18
|
+
server.registerTool(toolName(op.id, opts.nameCase, opts.namePrefix), toolConfig(op), makeHandler(op, client, opts.maxRows, opts.maxChars));
|
|
19
|
+
}
|
|
20
|
+
return { server, toolCount: operations.length, total: spec.operations.length };
|
|
21
|
+
}
|
|
22
|
+
export async function startStdio(opts) {
|
|
23
|
+
const { server, toolCount, total } = createServer(opts);
|
|
24
|
+
// stdout 是 MCP 协议通道,任何日志都必须走 stderr
|
|
25
|
+
console.error(`[uwa-openapi-mcp] 已加载 ${toolCount}/${total} 个工具,接口地址 ${opts.baseUrl}`);
|
|
26
|
+
await server.connect(new StdioServerTransport());
|
|
27
|
+
}
|