@uwa4d/openapi-mcp 0.2.0-beta.9 → 0.2.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/cli.js CHANGED
@@ -6,6 +6,7 @@ import { compositeFilterKeys, selectCompositeTools, COMPOSITE_TOOLS } from './co
6
6
  import { filterKeys, selectOperations } from './presets.js';
7
7
  import { DEFAULT_MAX_CHARS, DEFAULT_MAX_ROWS, toolName } from './tools.js';
8
8
  import { PACKAGE_VERSION, startStdio } from './server.js';
9
+ import { checkForUpdate, describeReleaseChannel, formatUpdateNotice } from './version-check.js';
9
10
  const DEFAULT_BASE_URL = 'https://secure-api.uwa4d.com';
10
11
  const SANDBOX_BASE_URL = 'https://sandbox-api.uwa4d.com';
11
12
  function splitList(value) {
@@ -133,6 +134,16 @@ program
133
134
  console.log('\n凭证有效,接口连通。');
134
135
  if (typeof total === 'number')
135
136
  console.log(`最近 7 天有 ${total} 份报告。`);
137
+ console.log(`\nMCP 包版本:${PACKAGE_VERSION}(${describeReleaseChannel(PACKAGE_VERSION).releaseLabel})`);
138
+ if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] !== '1') {
139
+ const update = await checkForUpdate(PACKAGE_VERSION);
140
+ if (update) {
141
+ console.log(formatUpdateNotice(update));
142
+ }
143
+ else {
144
+ console.log('已是最新同 release 线版本(或暂时无法连接 npm registry)。');
145
+ }
146
+ }
136
147
  console.log('\n可以按 README 配置 MCP 客户端了。');
137
148
  }
138
149
  catch (err) {
@@ -2,8 +2,10 @@ import { toolName } from '../tools.js';
2
2
  import { topResourcesTool } from './top-resources.js';
3
3
  import { topFunctionsTool } from './top-functions.js';
4
4
  import { reportDiagnosisTool } from './report-diagnosis.js';
5
+ import { mcpVersionTool } from './mcp-version.js';
5
6
  /** 全部手写复合工具(不进 uwa-openapi.json)。 */
6
7
  export const COMPOSITE_TOOLS = [
8
+ mcpVersionTool,
7
9
  topResourcesTool,
8
10
  topFunctionsTool,
9
11
  reportDiagnosisTool,
@@ -0,0 +1,2 @@
1
+ import type { CompositeTool } from './types.js';
2
+ export declare const mcpVersionTool: CompositeTool;
@@ -0,0 +1,48 @@
1
+ import { PACKAGE_VERSION } from '../package-version.js';
2
+ import { buildMcpVersionInfo, describeReleaseChannel } from '../version-check.js';
3
+ import { errorResult, jsonToolResult } from './helpers.js';
4
+ const release = describeReleaseChannel(PACKAGE_VERSION);
5
+ const STABLE_DESCRIPTION = [
6
+ '查询当前 UWA OpenAPI MCP 正式版版本,并与 npm 最新稳定版对比(仅正式版↔正式版,不与内部测试版交叉)。',
7
+ '适用:Unity / UE|复合工具|无需 dataKey',
8
+ '触发:本对话第一次使用任意 UWA MCP 工具前应先调一次;用户问版本/升级时也可调。同一对话勿重复。',
9
+ '若 updateAvailable=true / suggestTellUser=true,请用 userNotice 主动转告客户重启 MCP 升级;已是最新勿打扰。',
10
+ '不要臆测版本号。',
11
+ ].join('\n');
12
+ const INTERNAL_DESCRIPTION = [
13
+ '查询当前 UWA OpenAPI MCP 内部测试版版本,并与 npm @beta 最新测试版对比(仅测试↔测试,不与正式版交叉)。',
14
+ '适用:Unity / UE|复合工具|无需 dataKey|仅 UWA 内部 sandbox 联调',
15
+ '触发:本对话第一次使用任意 UWA MCP 工具前应先调一次;用户问版本/升级时也可调。同一对话勿重复。',
16
+ '若 updateAvailable=true / suggestTellUser=true,请用 userNotice 主动转告测试人员重启 MCP 升级;已是最新勿打扰。',
17
+ '不要臆测版本号。',
18
+ ].join('\n');
19
+ export const mcpVersionTool = {
20
+ id: 'uwa_mcp_version',
21
+ title: release.releaseLine === 'stable' ? 'MCP 正式版与升级提示' : 'MCP 内部测试版与升级提示',
22
+ engines: ['unity', 'unreal'],
23
+ filterKeys: ['uwa_mcp_version', 'composite', 'preset.default', 'common'],
24
+ description: release.releaseLine === 'stable' ? STABLE_DESCRIPTION : INTERNAL_DESCRIPTION,
25
+ inputSchema: {},
26
+ async handler(_args, ctx) {
27
+ try {
28
+ if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] === '1') {
29
+ return jsonToolResult({
30
+ package: '@uwa4d/openapi-mcp',
31
+ currentVersion: PACKAGE_VERSION,
32
+ releaseLine: release.releaseLine,
33
+ releaseLabel: release.releaseLabel,
34
+ updateCheckSkipped: true,
35
+ userNotice: `当前 MCP ${release.releaseLabel} ${PACKAGE_VERSION}(已设置 UWA_MCP_SKIP_UPDATE_CHECK,未核对 npm 最新版)。`,
36
+ }, ctx.maxChars);
37
+ }
38
+ const info = await buildMcpVersionInfo(PACKAGE_VERSION);
39
+ return jsonToolResult({
40
+ ...info,
41
+ suggestTellUser: info.updateAvailable,
42
+ }, ctx.maxChars);
43
+ }
44
+ catch (err) {
45
+ return errorResult(err);
46
+ }
47
+ },
48
+ };
@@ -1,3 +1,4 @@
1
+ import { withRateLimitAnnotation } from '../error-hints.js';
1
2
  import { z } from './types.js';
2
3
  import { errorResult, isObj, jsonToolResult, requireReportKey, topNOf, } from './helpers.js';
3
4
  import { fetchOverviewStatistic, fetchReportIdentity, preferAtResourceTable, } from './route-overview.js';
@@ -193,26 +194,24 @@ export async function runTopResources(client, args) {
193
194
  apisUsed.push(...fetched.apisUsed);
194
195
  if (fetched.items.length === 0) {
195
196
  const fallbackItems = typeLevelFromOverview(view);
196
- return packResult(fallbackItems, {
197
+ return packResult(fallbackItems, withRateLimitAnnotation({
197
198
  _sourceKind: fetched.sourceKind,
198
199
  _apisUsed: apisUsed,
199
- ...(fetched.errors.length ? { _partialErrors: fetched.errors } : {}),
200
200
  _note: fallbackItems.length > 0
201
201
  ? '逐资源列表为空,已回退为 Overview 类型级峰值。'
202
202
  : '逐资源列表与类型级峰值均为空(报告可能未采集资源内存)。',
203
203
  _limitations: ['per_asset_empty'],
204
- });
204
+ }, fetched.errors.length ? fetched.errors : undefined));
205
205
  }
206
206
  const note = groupBy === 'assetType'
207
207
  ? `已按类型各取 Top ${topN}(${fetched.sourceKind},共 ${assetTypes.length} 种)。`
208
208
  : `已按内存峰值合并 ${assetTypes.length} 种资源类型(${fetched.sourceKind}),返回全局 Top ${topN}。`;
209
- return packResult(fetched.items, {
209
+ return packResult(fetched.items, withRateLimitAnnotation({
210
210
  totalCandidates: fetched.items.length,
211
211
  _sourceKind: fetched.sourceKind,
212
212
  _apisUsed: apisUsed,
213
- ...(fetched.errors.length ? { _partialErrors: fetched.errors } : {}),
214
213
  _note: note,
215
- });
214
+ }, fetched.errors.length ? fetched.errors : undefined));
216
215
  }
217
216
  function normalizeAssetTypes(raw) {
218
217
  if (Array.isArray(raw) && raw.length)
@@ -236,6 +235,7 @@ export const topResourcesTool = {
236
235
  'Unity / UE 均优先走 AT 资源总览预签名(at/resource/overall/table/presign),按 assetType 并行下载后合并排序。',
237
236
  'groupBy=merged(默认):跨类型合并后取全局 Top N;groupBy=assetType:每种类型各取 Top N,返回 byType。',
238
237
  'Unity 旧报告(解析日 < 2026-06-25)回退 memory/manage;拉不到逐资源时再回退 Overview 类型级峰值。',
238
+ '内部按 assetType 并行请求,易触发 OpenAPI 限流 31004(UWA 服务端保护,预期行为)。遇限流见 _rateLimitHint:可缩小 assetTypes、稍等重试;部分类型限流时已有 Top 仍可用。',
239
239
  '不确定报告版本时直接用本工具;需要原始全量列表时再调对应原子工具。',
240
240
  ].join('\n'),
241
241
  inputSchema: {
@@ -9,5 +9,15 @@ export declare const FILE_META_REASONS: {
9
9
  readonly AT_META_TYPE_INVALID: "AT_META_TYPE_INVALID";
10
10
  readonly AT_META_PATH_MISSING: "AT_META_PATH_MISSING";
11
11
  };
12
+ /** OpenAPI 接口限流(UWA 服务端保护,预期行为)。 */
13
+ export declare const RATE_LIMIT_ERROR_CODE = 31004;
14
+ /** 复合工具 _partialErrors 含限流时附加的说明(与 resolveErrorHint(31004) 语义一致)。 */
15
+ export declare const RATE_LIMIT_HINT: string;
16
+ /** 错误文案是否含 31004 / API_RATE_LIMIT。 */
17
+ export declare function isRateLimitMessage(text: string): boolean;
18
+ /** _partialErrors 中是否有限流项。 */
19
+ export declare function hasRateLimitInPartialErrors(errors: string[]): boolean;
20
+ /** 附带 _partialErrors;若含限流则加 _rateLimitHint。 */
21
+ export declare function withRateLimitAnnotation(extra: Record<string, unknown>, partialErrors?: string[]): Record<string, unknown>;
12
22
  /** 组装给模型看的排查建议;优先 rawMessage 原因码 + OpenAPI message。 */
13
23
  export declare function resolveErrorHint(code: number, rawMessage: string, apiMessage: string): string | undefined;
@@ -9,6 +9,11 @@ export const FILE_META_REASONS = {
9
9
  AT_META_TYPE_INVALID: 'AT_META_TYPE_INVALID',
10
10
  AT_META_PATH_MISSING: 'AT_META_PATH_MISSING',
11
11
  };
12
+ /** OpenAPI 接口限流(UWA 服务端保护,预期行为)。 */
13
+ export const RATE_LIMIT_ERROR_CODE = 31004;
14
+ /** 复合工具 _partialErrors 含限流时附加的说明(与 resolveErrorHint(31004) 语义一致)。 */
15
+ export const RATE_LIMIT_HINT = 'OpenAPI 限流(31004)是 UWA 服务端保护,不是报告无数据或接口故障。可稍等重试、减少并发或缩小 assetTypes;' +
16
+ '若 _partialErrors 仅部分类型限流,已有 Top 仍可用,需说明可能不完整。';
12
17
  const ERROR_HINTS_BY_CODE = {
13
18
  20001: '业务参数不合法,核对参数名、取值范围和必填项(注意批量接口的参数名多为复数,如 dataKeys)',
14
19
  23508: '数据服务错误,常见原因是报告不适用该接口版本(如新报告调用了 1.0 接口,或旧报告调用了 2.0 接口),改用对应版本重试',
@@ -18,6 +23,8 @@ const ERROR_HINTS_BY_CODE = {
18
23
  24056: '签名错误,检查 appSecret 是否正确',
19
24
  24057: '时间戳过期,本机时间与服务端偏差不能超过 20 分钟',
20
25
  30001: '服务端错误,常见原因是用错了引擎对应的接口(如对 UE 报告调用了 Unity 专用接口);勿把瞬时失败说成「一定没有数据」',
26
+ 31004: 'OpenAPI 接口限流(UWA 服务端保护,预期行为)。稍等重试,或减少并行请求/缩小查询范围(如 top_resources 的 assetTypes);' +
27
+ '勿当成报告无数据或接口永久故障。',
21
28
  };
22
29
  /** OpenAPI 已按引擎写好的稳定文案(优先沿用,再补 AI 纪律)。 */
23
30
  function hintForOpenApiRuntimeLogMessage(apiMessage) {
@@ -45,6 +52,24 @@ function hintForFileMetaReason(rawMessage, apiMessage) {
45
52
  }
46
53
  return null;
47
54
  }
55
+ /** 错误文案是否含 31004 / API_RATE_LIMIT。 */
56
+ export function isRateLimitMessage(text) {
57
+ return text.includes(`[${RATE_LIMIT_ERROR_CODE}]`) || text.includes('API_RATE_LIMIT') || text.includes('接口限流');
58
+ }
59
+ /** _partialErrors 中是否有限流项。 */
60
+ export function hasRateLimitInPartialErrors(errors) {
61
+ return errors.some(isRateLimitMessage);
62
+ }
63
+ /** 附带 _partialErrors;若含限流则加 _rateLimitHint。 */
64
+ export function withRateLimitAnnotation(extra, partialErrors) {
65
+ if (!partialErrors?.length)
66
+ return extra;
67
+ const out = { ...extra, _partialErrors: partialErrors };
68
+ if (hasRateLimitInPartialErrors(partialErrors)) {
69
+ out._rateLimitHint = RATE_LIMIT_HINT;
70
+ }
71
+ return out;
72
+ }
48
73
  /** 组装给模型看的排查建议;优先 rawMessage 原因码 + OpenAPI message。 */
49
74
  export function resolveErrorHint(code, rawMessage, apiMessage) {
50
75
  if (code === 80108) {
@@ -0,0 +1,2 @@
1
+ /** 与 package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
2
+ export declare const PACKAGE_VERSION: string;
@@ -0,0 +1,15 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ function readPackageVersion() {
5
+ try {
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
8
+ return pkg.version ?? '0.0.0';
9
+ }
10
+ catch {
11
+ return '0.0.0';
12
+ }
13
+ }
14
+ /** 与 package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
15
+ export const PACKAGE_VERSION = readPackageVersion();
package/dist/presets.js CHANGED
@@ -31,6 +31,7 @@ export const PRESET_DEFAULT = [
31
31
  'gotonline_overview_stack_stutter_scene_tree_presign',
32
32
  'gotonline_overview_stack_tree_frame_presign',
33
33
  // 复合工具(手写,见 src/composite/)
34
+ 'uwa_mcp_version',
34
35
  'top_resources',
35
36
  'top_functions',
36
37
  'report_diagnosis',
@@ -11,6 +11,8 @@ export const SERVER_INSTRUCTIONS = [
11
11
  '4. 卡顿根因:优先 gotonline_overview_stack_stutter_full_tree_presign / stutter_scene_tree;禁止把 WaitForVsync 直接当根因。尖峰再查单帧树。',
12
12
  '5. 非法面板名要纠正(fps_mean→fps_avg;android_pss_max→android_memory_pss@max);数值带单位。PSS 优先读 pss_peak_kb/mb/frame。',
13
13
  '6. 接口失败时说明原因:运行日志 80108 — Unity 无 LG file_meta 表示未解析、无可用日志;UE 无 meta 只能说「暂不支持获取当前报告的运行日志」(方案切换过),勿断定报告本身无日志。其它失败勿把瞬时错误说成「一定没有数据」。',
14
+ '7. 接口限流 31004(API_RATE_LIMIT):UWA 服务端保护,预期行为,不是 Bug。稍等重试或减少并行/批量(如 top_resources 缩小 assetTypes)。若返回含 _partialErrors 且 _rateLimitHint,部分类型限流时已有 Top 仍可用,需说明可能不完整,勿当成报告无资源。',
15
+ '8. MCP 版本(每对话最多一次):本对话第一次使用任意 UWA MCP 工具前,先调用一次 uwa_mcp_version 核对 npm(正式版只对比正式版,内部测试版只对比测试版,互不交叉)。若 updateAvailable=true / suggestTellUser=true,必须用 userNotice 主动转告并说明重启 MCP 升级;已是最新则不要打扰用户。同一对话内勿重复调用。用户显式问版本时也可再调。',
14
16
  '',
15
17
  '体检优先调用 report_diagnosis;细节再按 _apisUsed 深挖。对比多份报告时说明场景/时长/目标帧是否可比(见 comparability_note)。',
16
18
  ].join('\n');
package/dist/server.d.ts CHANGED
@@ -16,9 +16,10 @@ export interface ServerOptions {
16
16
  maxChars: number;
17
17
  timeoutMs: number;
18
18
  specPath?: string;
19
+ /** 覆盖默认 server instructions(用于启动时注入版本升级提醒)。 */
20
+ instructions?: string;
19
21
  }
20
- /** package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
21
- export declare const PACKAGE_VERSION: string;
22
+ export { PACKAGE_VERSION } from './package-version.js';
22
23
  export declare function createServer(opts: ServerOptions): {
23
24
  server: McpServer;
24
25
  toolCount: number;
package/dist/server.js CHANGED
@@ -1,6 +1,3 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
3
  import { UwaClient } from './client.js';
@@ -9,18 +6,9 @@ import { selectOperations } from './presets.js';
9
6
  import { loadSpec } from './spec.js';
10
7
  import { makeHandler, toolConfig, toolName } from './tools.js';
11
8
  import { SERVER_INSTRUCTIONS } from './server-instructions.js';
12
- function readPackageVersion() {
13
- try {
14
- const here = dirname(fileURLToPath(import.meta.url));
15
- const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
16
- return pkg.version ?? '0.0.0';
17
- }
18
- catch {
19
- return '0.0.0';
20
- }
21
- }
22
- /** 与 package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
23
- export const PACKAGE_VERSION = readPackageVersion();
9
+ import { PACKAGE_VERSION } from './package-version.js';
10
+ import { buildMcpVersionInfo, buildUpdateInstructionAppendix, scheduleUpdateNotice, } from './version-check.js';
11
+ export { PACKAGE_VERSION } from './package-version.js';
24
12
  export function createServer(opts) {
25
13
  const spec = loadSpec(opts.specPath);
26
14
  const operations = selectOperations(spec.operations, { tools: opts.tools, engine: opts.engine });
@@ -29,7 +17,7 @@ export function createServer(opts) {
29
17
  credentials: { appId: opts.appId, appSecret: opts.appSecret },
30
18
  timeoutMs: opts.timeoutMs,
31
19
  });
32
- const server = new McpServer({ name: 'uwa-openapi-mcp', version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
20
+ const server = new McpServer({ name: 'uwa-openapi-mcp', version: PACKAGE_VERSION }, { instructions: opts.instructions ?? SERVER_INSTRUCTIONS });
33
21
  for (const op of operations) {
34
22
  server.registerTool(toolName(op.id, opts.nameCase, opts.namePrefix), toolConfig(op), makeHandler(op, client, opts.maxRows, opts.maxChars));
35
23
  }
@@ -45,9 +33,25 @@ export function createServer(opts) {
45
33
  return { server, toolCount, total, atomicCount: operations.length, compositeCount };
46
34
  }
47
35
  export async function startStdio(opts) {
48
- const { server, toolCount, total, atomicCount, compositeCount } = createServer(opts);
36
+ let instructions = SERVER_INSTRUCTIONS;
37
+ if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] !== '1') {
38
+ try {
39
+ const info = await buildMcpVersionInfo(PACKAGE_VERSION);
40
+ const appendix = buildUpdateInstructionAppendix(info);
41
+ if (appendix)
42
+ instructions = `${SERVER_INSTRUCTIONS}\n\n${appendix}`;
43
+ }
44
+ catch {
45
+ /* 版本检查失败不阻塞启动 */
46
+ }
47
+ }
48
+ const { server, toolCount, total, atomicCount, compositeCount } = createServer({
49
+ ...opts,
50
+ instructions,
51
+ });
49
52
  // stdout 是 MCP 协议通道,任何日志都必须走 stderr
50
- console.error(`[uwa-openapi-mcp] 已加载 ${toolCount}/${total} 个工具(原子 ${atomicCount} + 复合 ${compositeCount}),接口地址 ${opts.baseUrl}`);
53
+ console.error(`[uwa-openapi-mcp] v${PACKAGE_VERSION} 已加载 ${toolCount}/${total} 个工具(原子 ${atomicCount} + 复合 ${compositeCount}),接口地址 ${opts.baseUrl}`);
54
+ scheduleUpdateNotice(PACKAGE_VERSION);
51
55
  await server.connect(new StdioServerTransport());
52
56
  }
53
57
  export { selectCompositeTools, COMPOSITE_TOOLS };
@@ -0,0 +1,41 @@
1
+ /** npm 包名(与 package.json 同步)。 */
2
+ export declare const PACKAGE_NAME = "@uwa4d/openapi-mcp";
3
+ export interface UpdateCheckResult {
4
+ current: string;
5
+ latest: string;
6
+ /** npm dist-tag,仅作逻辑区分 */
7
+ registryTag: 'beta' | 'latest';
8
+ }
9
+ /** uwa_mcp_version 工具与启动 instructions 用的结构化版本信息。 */
10
+ export interface McpVersionInfo {
11
+ package: string;
12
+ currentVersion: string;
13
+ /** stable=正式版(客户) / internal=内部测试版;与 npm dist-tag 一一对应,互不交叉比对 */
14
+ releaseLine: 'stable' | 'internal';
15
+ /** 对用户/模型可见的通道名,正式版不说 beta */
16
+ releaseLabel: string;
17
+ latestOnChannel: string | null;
18
+ registryReachable: boolean;
19
+ updateAvailable: boolean;
20
+ userNotice: string;
21
+ upgradeHint: string;
22
+ }
23
+ export interface ReleaseChannelPresentation {
24
+ registryTag: 'beta' | 'latest';
25
+ releaseLine: 'stable' | 'internal';
26
+ releaseLabel: string;
27
+ packageInstallRef: string;
28
+ }
29
+ /** 当前运行版本应跟哪个 npm dist-tag 比对新版(正式↔latest,测试↔beta,不交叉)。 */
30
+ export declare function resolveUpdateChannel(version: string): 'beta' | 'latest';
31
+ /** 正式版与内部测试版的分发呈现(客户侧不出现 beta 字样)。 */
32
+ export declare function describeReleaseChannel(version: string): ReleaseChannelPresentation;
33
+ /** 查询 registry 是否有比 current 更新的同通道版本;失败时返回 null。 */
34
+ export declare function checkForUpdate(current: string): Promise<UpdateCheckResult | null>;
35
+ /** 拉取完整版本信息(工具返回 + 启动 instructions)。 */
36
+ export declare function buildMcpVersionInfo(current: string): Promise<McpVersionInfo>;
37
+ /** 有新版本时追加到 server instructions,让模型在对话里主动提醒用户。 */
38
+ export declare function buildUpdateInstructionAppendix(info: McpVersionInfo): string | null;
39
+ export declare function formatUpdateNotice(result: UpdateCheckResult): string;
40
+ /** 启动时异步检查;仅写 stderr,不阻塞 MCP 握手。 */
41
+ export declare function scheduleUpdateNotice(current: string): void;
@@ -0,0 +1,161 @@
1
+ /** npm 包名(与 package.json 同步)。 */
2
+ export const PACKAGE_NAME = '@uwa4d/openapi-mcp';
3
+ const REGISTRY_URL = 'https://registry.npmjs.org/@uwa4d%2Fopenapi-mcp';
4
+ const CHECK_TIMEOUT_MS = 8_000;
5
+ /** 当前运行版本应跟哪个 npm dist-tag 比对新版(正式↔latest,测试↔beta,不交叉)。 */
6
+ export function resolveUpdateChannel(version) {
7
+ return version.includes('-') ? 'beta' : 'latest';
8
+ }
9
+ /** 正式版与内部测试版的分发呈现(客户侧不出现 beta 字样)。 */
10
+ export function describeReleaseChannel(version) {
11
+ const registryTag = resolveUpdateChannel(version);
12
+ if (registryTag === 'beta') {
13
+ return {
14
+ registryTag: 'beta',
15
+ releaseLine: 'internal',
16
+ releaseLabel: '内部测试版',
17
+ packageInstallRef: `${PACKAGE_NAME}@beta`,
18
+ };
19
+ }
20
+ return {
21
+ registryTag: 'latest',
22
+ releaseLine: 'stable',
23
+ releaseLabel: '正式版',
24
+ packageInstallRef: PACKAGE_NAME,
25
+ };
26
+ }
27
+ /** 简易 semver 比较:a > b 返回 1,相等 0,a < b 返回 -1。 */
28
+ function compareVersions(a, b) {
29
+ if (a === b)
30
+ return 0;
31
+ const parseCore = (v) => v.split('-')[0].split('.').map((n) => Number.parseInt(n, 10) || 0);
32
+ const parsePre = (v) => {
33
+ const idx = v.indexOf('-');
34
+ if (idx < 0)
35
+ return [];
36
+ return v
37
+ .slice(idx + 1)
38
+ .split('.')
39
+ .map((part, i) => (i === 0 ? part : Number.parseInt(part, 10) || part));
40
+ };
41
+ const aCore = parseCore(a);
42
+ const bCore = parseCore(b);
43
+ for (let i = 0; i < Math.max(aCore.length, bCore.length); i++) {
44
+ const av = aCore[i] ?? 0;
45
+ const bv = bCore[i] ?? 0;
46
+ if (av !== bv)
47
+ return av > bv ? 1 : -1;
48
+ }
49
+ const aPre = parsePre(a);
50
+ const bPre = parsePre(b);
51
+ const aHasPre = aPre.length > 0;
52
+ const bHasPre = bPre.length > 0;
53
+ if (!aHasPre && bHasPre)
54
+ return 1;
55
+ if (aHasPre && !bHasPre)
56
+ return -1;
57
+ if (!aHasPre && !bHasPre)
58
+ return 0;
59
+ for (let i = 0; i < Math.max(aPre.length, bPre.length); i++) {
60
+ const av = aPre[i];
61
+ const bv = bPre[i];
62
+ if (av === bv)
63
+ continue;
64
+ if (av === undefined)
65
+ return -1;
66
+ if (bv === undefined)
67
+ return 1;
68
+ if (typeof av === 'number' && typeof bv === 'number')
69
+ return av > bv ? 1 : -1;
70
+ return String(av) > String(bv) ? 1 : -1;
71
+ }
72
+ return 0;
73
+ }
74
+ async function fetchDistTagVersion(channel) {
75
+ try {
76
+ const res = await fetch(REGISTRY_URL, {
77
+ signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
78
+ headers: { Accept: 'application/json' },
79
+ });
80
+ if (!res.ok)
81
+ return null;
82
+ const data = (await res.json());
83
+ const tag = data['dist-tags']?.[channel];
84
+ return typeof tag === 'string' && tag.trim() ? tag.trim() : null;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ /** 查询 registry 是否有比 current 更新的同通道版本;失败时返回 null。 */
91
+ export async function checkForUpdate(current) {
92
+ const info = await buildMcpVersionInfo(current);
93
+ if (!info.updateAvailable || !info.latestOnChannel)
94
+ return null;
95
+ return { current: info.currentVersion, latest: info.latestOnChannel, registryTag: describeReleaseChannel(current).registryTag };
96
+ }
97
+ function buildUpgradeHint(presentation) {
98
+ return (`若 MCP 配置未锁定具体版本号,请在您使用的 AI 客户端中重启 MCP 服务以升级(重载 MCP 配置或重启客户端均可);` +
99
+ `终端核对:npx --yes --prefer-online ${presentation.packageInstallRef} --version`);
100
+ }
101
+ /** 拉取完整版本信息(工具返回 + 启动 instructions)。 */
102
+ export async function buildMcpVersionInfo(current) {
103
+ const presentation = describeReleaseChannel(current);
104
+ const latestOnChannel = await fetchDistTagVersion(presentation.registryTag);
105
+ const registryReachable = latestOnChannel !== null;
106
+ const updateAvailable = registryReachable && latestOnChannel !== null && compareVersions(latestOnChannel, current) > 0;
107
+ const upgradeHint = buildUpgradeHint(presentation);
108
+ if (updateAvailable && latestOnChannel) {
109
+ return {
110
+ package: PACKAGE_NAME,
111
+ currentVersion: current,
112
+ releaseLine: presentation.releaseLine,
113
+ releaseLabel: presentation.releaseLabel,
114
+ latestOnChannel,
115
+ registryReachable,
116
+ updateAvailable: true,
117
+ userNotice: `UWA OpenAPI MCP(${presentation.releaseLabel})有新版本:当前 ${current},${presentation.releaseLabel}最新 ${latestOnChannel}。` +
118
+ upgradeHint,
119
+ upgradeHint,
120
+ };
121
+ }
122
+ const latestLabel = latestOnChannel ?? '(暂时无法连接 npm 核对最新版)';
123
+ return {
124
+ package: PACKAGE_NAME,
125
+ currentVersion: current,
126
+ releaseLine: presentation.releaseLine,
127
+ releaseLabel: presentation.releaseLabel,
128
+ latestOnChannel,
129
+ registryReachable,
130
+ updateAvailable: false,
131
+ userNotice: registryReachable
132
+ ? `UWA OpenAPI MCP(${presentation.releaseLabel})已是最新:当前 ${current},${presentation.releaseLabel}最新 ${latestLabel}。`
133
+ : `UWA OpenAPI MCP(${presentation.releaseLabel})当前版本 ${current};暂时无法连接 npm 核对最新版。`,
134
+ upgradeHint,
135
+ };
136
+ }
137
+ /** 有新版本时追加到 server instructions,让模型在对话里主动提醒用户。 */
138
+ export function buildUpdateInstructionAppendix(info) {
139
+ if (!info.updateAvailable || !info.latestOnChannel)
140
+ return null;
141
+ const headline = info.releaseLine === 'stable'
142
+ ? '【MCP 正式版有新版本 — 请主动告知用户】'
143
+ : '【MCP 内部测试版有新版本 — 请主动告知内部测试人员】';
144
+ return (`${headline}当前运行 ${info.currentVersion},${info.releaseLabel}最新 ${info.latestOnChannel}。` +
145
+ `请用自然语言转告用户重启 MCP 升级(勿只写 stderr 日志)。详情可调工具 uwa_mcp_version。` +
146
+ ` ${info.upgradeHint}`);
147
+ }
148
+ export function formatUpdateNotice(result) {
149
+ const presentation = describeReleaseChannel(result.current);
150
+ return (`[uwa-openapi-mcp] ${presentation.releaseLabel}有新版本 ${result.latest}(当前 ${result.current})。` +
151
+ buildUpgradeHint(presentation));
152
+ }
153
+ /** 启动时异步检查;仅写 stderr,不阻塞 MCP 握手。 */
154
+ export function scheduleUpdateNotice(current) {
155
+ if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] === '1')
156
+ return;
157
+ void checkForUpdate(current).then((result) => {
158
+ if (result)
159
+ console.error(formatUpdateNotice(result));
160
+ });
161
+ }
@@ -155,15 +155,15 @@ export const VERSION_GUIDE = {
155
155
  gotonline_overview_method_curve: [
156
156
  '【选用规则——务必先看】',
157
157
  '- v1.0.1 直出曲线 JSON(按 stackMethodName 或 stackMethodId)。',
158
- '- **MCP 与 Open API 推荐改用 gotonline_overview_method_curve_presign(v1.0.2)**:OSS 预签名,仅传 stackMethodId。',
158
+ '- **MCP 与 Open API 推荐改用 gotonline_overview_method_curve_presign(v1.0.2)**:OSS 预签名,id 或 name 至少其一。',
159
159
  '- stackMethodId 来自 gotonline_overview_method_idmap_statistic 或自定义函数组统计。',
160
160
  ].join('\n'),
161
161
  gotonline_overview_method_curve_presign: [
162
162
  '【选用规则——务必先看】',
163
163
  '- v1.0.2 OSS 预签名下载函数逐帧曲线 JSON(Unity · UE)。',
164
- '- **Open API 入口仅支持 stackMethodId(必填)**;UE 另需 threadName',
164
+ '- stackMethodId stackMethodName **至少传一个**(同时传时优先 id);UE 另需 threadName,Unity 不传。',
165
165
  '- 不要用 v1.0.1 gotonline_overview_method_curve 除非必须服务端直出。',
166
- '- stackMethodId 优先从 gotonline_overview_method_idmap_statistic 获取。',
166
+ '- id 优先从 gotonline_overview_method_idmap_statistic 获取。',
167
167
  ].join('\n'),
168
168
  gotonline_overview_indicator_dashboard_keys: [
169
169
  '【选用规则——务必先看】',
@@ -208,6 +208,7 @@ export const VERSION_GUIDE = {
208
208
  gotonline_overview_stack_tree_frame_presign: [
209
209
  '【选用规则——务必先看】',
210
210
  '- **单帧**完整堆栈树(frameId 必填)。用于已知尖峰帧深挖,不是卡顿分析的第一步。',
211
+ '- Unity:不传 threadId=主线程;传子线程 id/名=该子线程指定帧树(thread idmap)。UE:threadId 必传(含 GameThread)。',
211
212
  '- 卡顿共性根因请先用 stutter_full_tree / stutter_scene_tree(已是多帧合并结果)。',
212
213
  '- ❌ 不要对 frametime valueGt 筛出的每个慢帧循环调用本工具来「聚类」。',
213
214
  STACK_ANALYSIS_GUIDE,
@@ -215,6 +216,7 @@ export const VERSION_GUIDE = {
215
216
  gotonline_overview_stack_sample_frame_presign: [
216
217
  '【选用规则——务必先看】',
217
218
  '- 堆栈树某个结点的逐帧曲线;结点 id 来自树文件。',
219
+ '- Unity 子线程须传 threadId + sampleId;不传 threadId 为主线程(可不传 sampleId 取全帧聚合)。',
218
220
  STACK_ANALYSIS_GUIDE,
219
221
  ].join('\n'),
220
222
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwa4d/openapi-mcp",
3
- "version": "0.2.0-beta.9",
3
+ "version": "0.2.0",
4
4
  "description": "UWA 开放平台 MCP Server,将 UWA Open API 暴露为 MCP 工具供 AI 助手调用",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "prepublishOnly": "npm run build && node scripts/check-publish-tag.mjs",
28
28
  "release": "node scripts/release.mjs",
29
29
  "release:dry": "node scripts/release.mjs --dry-run",
30
+ "release:stable-patch": "node scripts/release-stable-patch.mjs",
30
31
  "version:beta": "node scripts/bump.mjs beta",
31
32
  "version:release": "node scripts/bump.mjs release"
32
33
  },
@@ -21,6 +21,52 @@
21
21
  "to": "单次请求最多查询 30 份报告(文档写 50,实测上限 30,超出会返回 20001)"
22
22
  }
23
23
  ]
24
+ },
25
+ "gotonline_overview_method_idmap_statistic": {
26
+ "reason": "threadName 仅 UE 必填;Unity 主线程不传。MCP 全局 schema 不得 required,否则 Zod 拦 Unity 调用。",
27
+ "verifiedAt": "2026-08-12",
28
+ "evidence": "sandbox:Unity dataKey=20260812163333sdk6b704767 不传 threadName 返回 348 条;UE 同接口须 threadName=GameThread。",
29
+ "setQueryRequired": {
30
+ "threadName": false
31
+ },
32
+ "setQueryDescription": {
33
+ "threadName": "UE 必传(GameThread / RHIThread / RenderThread / FAsyncLoadingThread 等)。Unity 无需此参数(不传=主线程)"
34
+ }
35
+ },
36
+ "gotonline_overview_method_curve_presign": {
37
+ "reason": "threadName 仅 UE 必填;Unity 不传。stackMethodId/Name 至少其一即可(与线上 Open API 一致)。",
38
+ "verifiedAt": "2026-08-12",
39
+ "evidence": "sandbox:Unity 仅 stackMethodId 可取曲线;UE 须 threadName=GameThread。生产 Open API 支持 name+id。",
40
+ "setQueryRequired": {
41
+ "threadName": false,
42
+ "stackMethodId": false,
43
+ "stackMethodName": false
44
+ },
45
+ "setQueryDescription": {
46
+ "threadName": "UE 必传。Unity 无需此参数",
47
+ "stackMethodId": "函数 ID;与 stackMethodName 至少传一个,同时传时优先 id",
48
+ "stackMethodName": "函数名称;与 stackMethodId 至少传一个"
49
+ }
50
+ },
51
+ "gotonline_overview_stack_tree_frame_presign": {
52
+ "reason": "threadId 仅 UE 必填(含 GameThread);Unity 不传=主线程,传子线程 id/名=子线程树。",
53
+ "verifiedAt": "2026-08-12",
54
+ "evidence": "sandbox:Unity 帧树不传 threadId 成功;UE 传 GameThread/RenderThread 成功。旧 MCP schema 把 threadId 标成全局 required,Agent 调 Unity 时被 Zod 卡住。",
55
+ "setQueryRequired": {
56
+ "threadId": false
57
+ },
58
+ "setQueryDescription": {
59
+ "threadId": "UE 必传(线程 id 或线程名,含 GameThread)。Unity:不传=主线程;传子线程 id/名=该子线程指定帧树"
60
+ }
61
+ },
62
+ "gotonline_overview_stack_sample_frame_presign": {
63
+ "reason": "threadId / sampleId 文档写作「UE 是」,不得升成 MCP 全局必填。",
64
+ "verifiedAt": "2026-08-12",
65
+ "evidence": "文档表写作「UE 是」;旧 isRequired 用 /是/ 子串匹配会误判。Unity 主线程可不传 threadId,可不传 sampleId 取全帧聚合。",
66
+ "setQueryRequired": {
67
+ "threadId": false,
68
+ "sampleId": false
69
+ }
24
70
  }
25
71
  }
26
72
  }