cocos-web-inspector-mcp 2.0.1 → 2.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/README.md +85 -11
- package/dist/src/bridge.d.ts +40 -0
- package/dist/src/bridge.js +833 -24
- package/dist/src/browser-data.d.ts +23 -0
- package/dist/src/browser-data.js +166 -0
- package/dist/src/browser.d.ts +1 -1
- package/dist/src/index.js +21 -3
- package/dist/src/native.d.ts +26 -0
- package/dist/src/native.js +184 -0
- package/dist/src/server.d.ts +5 -1
- package/dist/src/server.js +205 -97
- package/package.json +1 -1
package/dist/src/server.js
CHANGED
|
@@ -2,7 +2,9 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { InspectorError, sanitizeUrl } from './browser.js';
|
|
5
|
-
import {
|
|
5
|
+
import { nativeClick, nativeConsoleMessages, nativeDrag, nativeTypeText } from './native.js';
|
|
6
|
+
import { consoleMessages, networkRequest, networkRequests, storage } from './browser-data.js';
|
|
7
|
+
import { captureNode, clickNode, devicePresetNames, dragNode, emulateDevice, inspectCocosPage, networkProfileNames, runBridge, typeText } from './bridge.js';
|
|
6
8
|
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
7
9
|
const pageUrl = z.url().optional();
|
|
8
10
|
const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
@@ -21,6 +23,21 @@ const propertyValue = z.union([
|
|
|
21
23
|
z.object({ width: finiteNumber, height: finiteNumber }).strict(),
|
|
22
24
|
z.object({ r: finiteNumber, g: finiteNumber, b: finiteNumber, a: finiteNumber }).strict(),
|
|
23
25
|
]);
|
|
26
|
+
// Every node tool takes uuid or path. Paths survive reloads but are not unique, so a path must match exactly one node.
|
|
27
|
+
const target = { uuid: z.string().min(1).max(200).optional(), path: z.string().min(1).max(10_000).optional() };
|
|
28
|
+
const oneTarget = (value) => (value.uuid === undefined) !== (value.path === undefined);
|
|
29
|
+
const oneTargetMessage = 'Provide uuid or path, not both';
|
|
30
|
+
async function resolveUuid(page, input) {
|
|
31
|
+
if (input.uuid)
|
|
32
|
+
return input.uuid;
|
|
33
|
+
const found = await runBridge(page, { action: 'findNode', path: input.path, limit: 10 });
|
|
34
|
+
const matches = found.matches ?? [];
|
|
35
|
+
if (matches.length === 0)
|
|
36
|
+
throw new Error(found.truncated ? 'Node not found within traversal limit' : 'Node not found');
|
|
37
|
+
if (matches.length > 1)
|
|
38
|
+
throw new InspectorError('AMBIGUOUS_NODE', `Path matches ${found.truncated ? 'more than 10' : matches.length} nodes; pass one uuid: ${matches.map(match => match.uuid).join(', ')}`);
|
|
39
|
+
return matches[0].uuid;
|
|
40
|
+
}
|
|
24
41
|
function response(data) {
|
|
25
42
|
return {
|
|
26
43
|
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
@@ -44,6 +61,10 @@ function failure(error) {
|
|
|
44
61
|
}
|
|
45
62
|
export function createServer(browser, options = {}) {
|
|
46
63
|
const server = new McpServer({ name: 'cocos-web-inspector-mcp', version });
|
|
64
|
+
// A native (JSB) runtime has no page: screenshots, device emulation, and network/cookie readers need a browser,
|
|
65
|
+
// the DOM overlays draw nothing, the 2D batcher runs in C++ where analyze_batches cannot observe it,
|
|
66
|
+
// and game.step renders outside the native frame loop, which crashed a 3.8.8 Android build in the GFX pipeline.
|
|
67
|
+
const web = !options.native;
|
|
47
68
|
const execute = async (request, selectedPage) => {
|
|
48
69
|
try {
|
|
49
70
|
return response(await runBridge(await browser.page(selectedPage), request));
|
|
@@ -52,6 +73,16 @@ export function createServer(browser, options = {}) {
|
|
|
52
73
|
return failure(error);
|
|
53
74
|
}
|
|
54
75
|
};
|
|
76
|
+
const withNode = async (input, work) => {
|
|
77
|
+
try {
|
|
78
|
+
const page = await browser.page(input.pageUrl);
|
|
79
|
+
return response(await work(page, await resolveUuid(page, input)));
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return failure(error);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const executeNode = (input, build) => withNode(input, (page, uuid) => runBridge(page, build(uuid)));
|
|
55
86
|
server.registerTool('cocos_list_pages', {
|
|
56
87
|
description: 'List bounded summaries of eligible localhost Chromium pages.',
|
|
57
88
|
inputSchema: z.object({}).strict(),
|
|
@@ -92,97 +123,97 @@ export function createServer(browser, options = {}) {
|
|
|
92
123
|
}, input => execute({ action: 'findNode', uuid: input.uuid, name: input.name, path: input.path, nameContains: input.nameContains, componentType: input.componentType, active: input.active, pathPrefix: input.pathPrefix, limit: input.limit }, input.pageUrl));
|
|
93
124
|
server.registerTool('cocos_get_node', {
|
|
94
125
|
description: 'Return bounded context for one exact Cocos node UUID.',
|
|
95
|
-
inputSchema: z.object({ pageUrl,
|
|
126
|
+
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
96
127
|
annotations: readOnly,
|
|
97
|
-
}, input =>
|
|
128
|
+
}, input => executeNode(input, uuid => ({ action: 'getNode', uuid })));
|
|
98
129
|
server.registerTool('cocos_snapshot_subtree', {
|
|
99
130
|
description: 'Return a bounded stateless subtree snapshot for one exact Cocos node UUID.',
|
|
100
|
-
inputSchema: z.object({ pageUrl,
|
|
131
|
+
inputSchema: z.object({ pageUrl, ...target, maxDepth: z.number().int().min(0).max(20).optional(), maxNodes: z.number().int().min(1).max(5_000).optional() }).strict().refine(oneTarget, oneTargetMessage),
|
|
101
132
|
annotations: readOnly,
|
|
102
|
-
}, input =>
|
|
133
|
+
}, input => executeNode(input, uuid => ({ action: 'snapshotSubtree', uuid, maxDepth: input.maxDepth, maxNodes: input.maxNodes })));
|
|
103
134
|
server.registerTool('cocos_get_node_bounds', {
|
|
104
135
|
description: 'Return bounded canvas and viewport bounds for one Cocos UI node UUID.',
|
|
105
|
-
inputSchema: z.object({ pageUrl,
|
|
106
|
-
annotations: readOnly,
|
|
107
|
-
}, input => execute({ action: 'getNodeBounds', uuid: input.uuid }, input.pageUrl));
|
|
108
|
-
server.registerTool('cocos_capture_node', {
|
|
109
|
-
description: 'Capture a bounded viewport-clipped PNG for one visible Cocos UI node UUID.',
|
|
110
|
-
inputSchema: z.object({ pageUrl, uuid: z.string().min(1) }).strict(),
|
|
136
|
+
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
111
137
|
annotations: readOnly,
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
});
|
|
138
|
+
}, input => executeNode(input, uuid => ({ action: 'getNodeBounds', uuid })));
|
|
139
|
+
if (web)
|
|
140
|
+
server.registerTool('cocos_capture_node', {
|
|
141
|
+
description: 'Capture a bounded viewport-clipped PNG for one visible Cocos UI node UUID.',
|
|
142
|
+
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
143
|
+
annotations: readOnly,
|
|
144
|
+
}, input => withNode(input, captureNode));
|
|
120
145
|
server.registerTool('cocos_get_components', {
|
|
121
146
|
description: 'List bounded component summaries for one Cocos node UUID.',
|
|
122
|
-
inputSchema: z.object({ pageUrl,
|
|
147
|
+
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
123
148
|
annotations: readOnly,
|
|
124
|
-
}, input =>
|
|
149
|
+
}, input => executeNode(input, uuid => ({ action: 'getComponents', uuid })));
|
|
125
150
|
server.registerTool('cocos_get_properties', {
|
|
126
151
|
description: 'Read bounded, cycle-safe public properties from a Cocos node or one component.',
|
|
127
|
-
inputSchema: z.object({ pageUrl,
|
|
152
|
+
inputSchema: z.object({ pageUrl, ...target, componentType: z.string().min(1).optional(), componentUuid: z.string().min(1).optional(), maxDepth: z.number().int().min(0).max(6).optional() }).strict().refine(oneTarget, oneTargetMessage)
|
|
128
153
|
.refine(value => !(value.componentType && value.componentUuid), 'Provide componentType or componentUuid, not both'),
|
|
129
154
|
annotations: readOnly,
|
|
130
|
-
}, input =>
|
|
155
|
+
}, input => executeNode(input, uuid => ({ action: 'getProperties', uuid, componentType: input.componentType, componentUuid: input.componentUuid, maxDepth: input.maxDepth })));
|
|
131
156
|
server.registerTool('cocos_wait_for_property', {
|
|
132
157
|
description: 'Poll one top-level property of a Cocos node or component until it equals a value or the timeout passes.',
|
|
133
|
-
inputSchema: z.object({ pageUrl,
|
|
158
|
+
inputSchema: z.object({ pageUrl, ...target, componentType: z.string().min(1).optional(), componentUuid: z.string().min(1).optional(), key: z.string().min(1).max(200), equals: z.union([z.boolean(), finiteNumber, z.string().max(2_000), z.null()]), timeoutMs: z.number().int().min(100).max(30_000).optional(), intervalMs: z.number().int().min(50).max(5_000).optional() }).strict().refine(oneTarget, oneTargetMessage)
|
|
134
159
|
.refine(value => !(value.componentType && value.componentUuid), 'Provide componentType or componentUuid, not both'),
|
|
135
160
|
annotations: readOnly,
|
|
136
|
-
}, async (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
break;
|
|
148
|
-
await new Promise(resolve => setTimeout(resolve, input.intervalMs ?? 200));
|
|
149
|
-
}
|
|
150
|
-
return response({ matched: value === input.equals, key: input.key, value: value ?? null, polls });
|
|
161
|
+
}, input => withNode(input, async (page, uuid) => {
|
|
162
|
+
const request = { action: 'getProperties', uuid, componentType: input.componentType, componentUuid: input.componentUuid, maxDepth: 0 };
|
|
163
|
+
const deadline = Date.now() + (input.timeoutMs ?? 5_000);
|
|
164
|
+
let polls = 0;
|
|
165
|
+
let value;
|
|
166
|
+
for (;;) {
|
|
167
|
+
polls++;
|
|
168
|
+
value = (await runBridge(page, request)).properties?.[input.key];
|
|
169
|
+
if (value === input.equals || Date.now() >= deadline)
|
|
170
|
+
break;
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, input.intervalMs ?? 200));
|
|
151
172
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
});
|
|
173
|
+
return { matched: value === input.equals, key: input.key, value: value ?? null, polls };
|
|
174
|
+
}));
|
|
156
175
|
if (options.allowRuntimeMutation) {
|
|
157
176
|
server.registerTool('cocos_set_node_active', {
|
|
158
177
|
description: 'Set the active state of one Cocos node selected by exact UUID.',
|
|
159
|
-
inputSchema: z.object({ pageUrl,
|
|
178
|
+
inputSchema: z.object({ pageUrl, ...target, active: z.boolean() }).strict().refine(oneTarget, oneTargetMessage),
|
|
160
179
|
annotations: runtimeMutation,
|
|
161
|
-
}, input =>
|
|
180
|
+
}, input => executeNode(input, uuid => ({ action: 'setNodeActive', uuid, active: input.active })));
|
|
162
181
|
server.registerTool('cocos_set_transform', {
|
|
163
182
|
description: 'Update selected Cocos node position, rotation, or scale by exact UUID.',
|
|
164
|
-
inputSchema: z.object({ pageUrl,
|
|
183
|
+
inputSchema: z.object({ pageUrl, ...target, position: vector3.optional(), rotation: quaternion.optional(), scale: vector3.optional() }).strict().refine(oneTarget, oneTargetMessage)
|
|
165
184
|
.refine(value => !!value.position || !!value.rotation || !!value.scale, 'Provide position, rotation, or scale'),
|
|
166
185
|
annotations: runtimeMutation,
|
|
167
|
-
}, input =>
|
|
186
|
+
}, input => executeNode(input, uuid => ({ action: 'setTransform', uuid, position: input.position, rotation: input.rotation, scale: input.scale })));
|
|
168
187
|
server.registerTool('cocos_set_property', {
|
|
169
188
|
description: 'Update one bounded public data property selected by node and component UUID.',
|
|
170
|
-
inputSchema: z.object({ pageUrl,
|
|
189
|
+
inputSchema: z.object({ pageUrl, ...target, componentUuid: z.string().min(1), key: z.string().min(1).max(200), value: propertyValue }).strict().refine(oneTarget, oneTargetMessage),
|
|
171
190
|
annotations: runtimeMutation,
|
|
172
|
-
}, input =>
|
|
191
|
+
}, input => executeNode(input, uuid => ({ action: 'setProperty', uuid, componentUuid: input.componentUuid, key: input.key, value: input.value })));
|
|
173
192
|
server.registerTool('cocos_click_node', {
|
|
174
193
|
description: 'Dispatch a real mouse click at the visible center of one Cocos UI node, so Button and touch handlers run. Handlers may call game servers or make irreversible changes.',
|
|
175
|
-
inputSchema: z.object({ pageUrl,
|
|
194
|
+
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
176
195
|
// Game click handlers run arbitrary code: a login button reaches real servers, a buy button spends currency.
|
|
177
196
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
197
|
+
}, input => withNode(input, web ? clickNode : nativeClick));
|
|
198
|
+
// Like click: game drag and input handlers run arbitrary code.
|
|
199
|
+
const realInput = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
|
|
200
|
+
server.registerTool('cocos_drag_node', {
|
|
201
|
+
description: 'Drag from the visible center of one Cocos UI node by dx/dy CSS pixels with real pointer input, to scroll a ScrollView, flip a PageView, or move a Slider. Under touch emulation the drag arrives as touch. Handlers may call game servers.',
|
|
202
|
+
inputSchema: z.object({ pageUrl, ...target, dx: z.number().min(-4_000).max(4_000), dy: z.number().min(-4_000).max(4_000), steps: z.number().int().min(1).max(60).optional(), durationMs: z.number().int().min(0).max(5_000).optional() }).strict().refine(oneTarget, oneTargetMessage)
|
|
203
|
+
.refine(value => value.dx !== 0 || value.dy !== 0, 'Provide a non-zero dx or dy'),
|
|
204
|
+
annotations: realInput,
|
|
205
|
+
}, input => withNode(input, (page, uuid) => web ? dragNode(page, uuid, input.dx, input.dy, input.steps, input.durationMs) : nativeDrag(page, uuid, input.dx, input.dy, input.durationMs)));
|
|
206
|
+
server.registerTool('cocos_type_text', {
|
|
207
|
+
description: 'Tap one Cocos EditBox and type text with real keyboard input, replacing its content, so text-changed and editing events fire; submit presses Enter (editing-return on single-line boxes, a newline in multi-line ones). Password text is never echoed back.',
|
|
208
|
+
inputSchema: z.object({ pageUrl, ...target, text: z.string().max(2_000), submit: z.boolean().optional() }).strict().refine(oneTarget, oneTargetMessage),
|
|
209
|
+
annotations: realInput,
|
|
210
|
+
}, input => withNode(input, (page, uuid) => (web ? typeText : nativeTypeText)(page, uuid, input.text, input.submit ?? false)));
|
|
211
|
+
if (web)
|
|
212
|
+
server.registerTool('cocos_analyze_batches', {
|
|
213
|
+
description: 'Capture the 2D draw batches of the next rendered frame: the node that starts each batch and why the previous batch broke (TEXTURE, MATERIAL, MASK, STENCIL, LAYER, BUFFER, MODEL, MIDDLEWARE, ...). Wraps engine batcher methods for that one frame only; the game must be running. tintMs overlays each batch\'s nodes in its own color for that long.',
|
|
214
|
+
inputSchema: z.object({ pageUrl, limit: z.number().int().min(1).max(500).optional(), tintMs: z.number().int().min(100).max(30_000).optional() }).strict(),
|
|
215
|
+
annotations: { ...runtimeMutation, idempotentHint: false },
|
|
216
|
+
}, input => execute({ action: 'analyzeBatches', limit: input.limit, tintMs: input.tintMs }, input.pageUrl));
|
|
186
217
|
for (const action of ['pause', 'resume']) {
|
|
187
218
|
server.registerTool(`cocos_${action}`, {
|
|
188
219
|
description: `${action === 'pause' ? 'Pause' : 'Resume'} the Cocos director through its public API.`,
|
|
@@ -190,55 +221,132 @@ export function createServer(browser, options = {}) {
|
|
|
190
221
|
annotations: runtimeMutation,
|
|
191
222
|
}, input => execute({ action }, input.pageUrl));
|
|
192
223
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
224
|
+
if (web)
|
|
225
|
+
server.registerTool('cocos_step_frame', {
|
|
226
|
+
description: 'Advance a paused Cocos game by fixed-delta frames through cc.game.step; requires cocos_pause first.',
|
|
227
|
+
inputSchema: z.object({ pageUrl, frames: z.number().int().min(1).max(60).optional() }).strict(),
|
|
228
|
+
annotations: { ...runtimeMutation, idempotentHint: false },
|
|
229
|
+
}, input => execute({ action: 'stepFrame', frames: input.frames }, input.pageUrl));
|
|
230
|
+
server.registerTool('cocos_set_time_scale', {
|
|
231
|
+
description: 'Speed up or slow down the whole game by scaling the delta time of every frame (components, scheduler, tweens, animation, physics). 1 restores normal speed; omit scale to read it.',
|
|
232
|
+
inputSchema: z.object({ pageUrl, scale: z.number().gt(0).max(100).optional() }).strict(),
|
|
233
|
+
annotations: runtimeMutation,
|
|
234
|
+
}, input => execute({ action: 'timeScale', scale: input.scale }, input.pageUrl));
|
|
198
235
|
server.registerTool('cocos_show_stats', {
|
|
199
236
|
description: 'Show or hide the Cocos profiler overlay (FPS, draw calls, triangles) through the public profiler API.',
|
|
200
237
|
inputSchema: z.object({ pageUrl, visible: z.boolean() }).strict(),
|
|
201
238
|
annotations: runtimeMutation,
|
|
202
239
|
}, input => execute({ action: 'showStats', visible: input.visible }, input.pageUrl));
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
240
|
+
if (web)
|
|
241
|
+
server.registerTool('cocos_emulate_device', {
|
|
242
|
+
description: 'Emulate a mobile device (viewport, DPR, touch, user agent, orientation) and optionally slow the CPU or network, like the Chrome device toolbar. Settings merge across calls and last until reset or server disconnect; reload lets the game re-detect touch and user agent.',
|
|
243
|
+
inputSchema: z.object({
|
|
244
|
+
pageUrl,
|
|
245
|
+
reset: z.boolean().optional(),
|
|
246
|
+
preset: z.enum(devicePresetNames).optional(),
|
|
247
|
+
width: z.number().int().min(200).max(4_000).optional(),
|
|
248
|
+
height: z.number().int().min(200).max(4_000).optional(),
|
|
249
|
+
deviceScaleFactor: z.number().min(1).max(4).optional(),
|
|
250
|
+
mobile: z.boolean().optional(),
|
|
251
|
+
orientation: z.enum(['portrait', 'landscape']).optional(),
|
|
252
|
+
cpuSlowdown: z.number().min(1).max(20).optional(),
|
|
253
|
+
network: z.enum(networkProfileNames).optional(),
|
|
254
|
+
reload: z.boolean().optional(),
|
|
255
|
+
}).strict()
|
|
256
|
+
.refine(value => (value.width === undefined) === (value.height === undefined), 'Provide width and height together')
|
|
257
|
+
.refine(value => !(value.preset && value.width !== undefined), 'Provide preset or width and height, not both')
|
|
258
|
+
.refine(value => value.width !== undefined || (value.deviceScaleFactor === undefined && value.mobile === undefined), 'deviceScaleFactor and mobile need width and height')
|
|
259
|
+
.refine(value => !value.reset || Object.keys(value).every(key => ['pageUrl', 'reset', 'reload'].includes(key)), 'reset accepts only pageUrl and reload'),
|
|
260
|
+
annotations: { ...runtimeMutation, idempotentHint: false },
|
|
261
|
+
}, async (input) => {
|
|
262
|
+
try {
|
|
263
|
+
return response(await emulateDevice(await browser.page(input.pageUrl), input));
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
return failure(error);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
server.registerTool('cocos_explain_click', {
|
|
271
|
+
description: 'Explain whether a tap on a node (or at viewport x/y) reaches it, replaying the engine touch dispatch order without dispatching: which node would claim the touch, the hit stack, and reasons such as INACTIVE, BUTTON_NOT_INTERACTABLE, BLOCKED_BY_BLOCK_INPUT_EVENTS, COVERED_BY_OTHER_NODE, NO_TOUCH_LISTENER, or OUTSIDE_VIEWPORT.',
|
|
272
|
+
inputSchema: z.object({ pageUrl, ...target, x: z.number().min(0).max(20_000).optional(), y: z.number().min(0).max(20_000).optional() }).strict()
|
|
273
|
+
.refine(value => (value.x === undefined) === (value.y === undefined), 'Provide x and y together')
|
|
274
|
+
.refine(value => value.uuid !== undefined || value.path !== undefined || value.x !== undefined, 'Provide uuid, path, or x and y')
|
|
275
|
+
.refine(value => !(value.uuid !== undefined && value.path !== undefined), oneTargetMessage),
|
|
276
|
+
annotations: readOnly,
|
|
277
|
+
}, input => input.uuid === undefined && input.path === undefined
|
|
278
|
+
? execute({ action: 'explainClick', x: input.x, y: input.y }, input.pageUrl)
|
|
279
|
+
: executeNode(input, uuid => ({ action: 'explainClick', uuid, x: input.x, y: input.y })));
|
|
280
|
+
server.registerTool('cocos_listener_report', {
|
|
281
|
+
description: 'Report scheduler timers, update callbacks, tweens, and director/game/view listeners that outlive their owner: those on destroyed or detached nodes and components, and unowned ones (arrow functions, bind(this), plain objects) the engine can never purge, grouped by event and name with counts. Compare two calls around opening and closing a popup; a growing count is a leak.',
|
|
282
|
+
inputSchema: z.object({ pageUrl, limit: z.number().int().min(1).max(500).optional() }).strict(),
|
|
283
|
+
annotations: readOnly,
|
|
284
|
+
}, input => execute({ action: 'listenerReport', limit: input.limit }, input.pageUrl));
|
|
285
|
+
server.registerTool('cocos_dynamic_atlas', {
|
|
286
|
+
description: 'Report the Cocos dynamic atlas: config, each atlas page with its packed textures, positions, fill ratio, and GPU bytes, plus why visible sprites stayed out of it (DISABLED, COMPRESSED, TOO_LARGE, FILTER, NOT_PACKABLE, ATLAS_FULL, ...).',
|
|
287
|
+
inputSchema: z.object({ pageUrl, limit: z.number().int().min(1).max(500).optional() }).strict(),
|
|
288
|
+
annotations: readOnly,
|
|
289
|
+
}, input => execute({ action: 'dynamicAtlas', limit: input.limit }, input.pageUrl));
|
|
290
|
+
server.registerTool('cocos_asset_report', {
|
|
291
|
+
description: 'List assets held in the Cocos asset cache with type, refCount, bundle, texture GPU bytes, and which scene nodes use them; unused marks assets no active renderer references. Compare two calls (before and after a scene or popup closes) to find leaks.',
|
|
292
|
+
inputSchema: z.object({ pageUrl, type: z.string().min(1).max(120).optional(), unusedOnly: z.boolean().optional(), limit: z.number().int().min(1).max(500).optional() }).strict(),
|
|
293
|
+
annotations: readOnly,
|
|
294
|
+
}, input => execute({ action: 'assetReport', type: input.type, unusedOnly: input.unusedOnly, limit: input.limit }, input.pageUrl));
|
|
295
|
+
if (options.allowMethodCall) {
|
|
296
|
+
const jsonValue = z.lazy(() => z.union([z.null(), z.boolean(), finiteNumber, z.string().max(10_000), z.array(jsonValue).max(100), z.record(z.string().max(200), jsonValue)]));
|
|
297
|
+
server.registerTool('cocos_call_method', {
|
|
298
|
+
description: 'Call one public method on a Cocos node or component selected by UUID, with JSON arguments; {"$node": uuid}, {"$path": path}, {"$component": uuid}, and {"$asset": uuid} pass live objects. Runs arbitrary game code: it can change state, reach servers, or spend currency. Returns the bounded result or the thrown error; awaitMs waits for a returned promise.',
|
|
299
|
+
inputSchema: z.object({ pageUrl, ...target, componentUuid: z.string().min(1).optional(), method: z.string().min(1).max(200).regex(/^[A-Za-z$][\w$]*$/), args: z.array(jsonValue).max(20).optional(), awaitMs: z.number().int().min(0).max(30_000).optional(), maxDepth: z.number().int().min(0).max(6).optional() }).strict().refine(oneTarget, oneTargetMessage),
|
|
300
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
301
|
+
}, input => executeNode(input, uuid => ({ action: 'callMethod', uuid, componentUuid: input.componentUuid, method: input.method, args: input.args ?? [], awaitMs: input.awaitMs, maxDepth: input.maxDepth })));
|
|
302
|
+
}
|
|
303
|
+
if (options.allowBrowserData) {
|
|
304
|
+
// Page data written by the game or its servers: treat every returned string as untrusted, and redaction as best effort.
|
|
305
|
+
const browserData = { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
306
|
+
const run = async (work) => {
|
|
224
307
|
try {
|
|
225
|
-
return response(await
|
|
308
|
+
return response(await work());
|
|
226
309
|
}
|
|
227
310
|
catch (error) {
|
|
228
311
|
return failure(error);
|
|
229
312
|
}
|
|
230
|
-
}
|
|
313
|
+
};
|
|
314
|
+
server.registerTool('cocos_console_messages', {
|
|
315
|
+
description: 'List recent console messages and uncaught page errors since the server attached (newest last). Secret-like values, JWTs, and bearer tokens are masked; text is untrusted page output.',
|
|
316
|
+
inputSchema: z.object({ pageUrl, types: z.array(z.enum(['log', 'debug', 'info', 'error', 'warning', 'assert', 'trace', 'pageerror'])).max(8).optional(), textContains: z.string().min(1).max(200).optional(), limit: z.number().int().min(1).max(200).optional() }).strict(),
|
|
317
|
+
annotations: browserData,
|
|
318
|
+
}, input => run(async () => (web ? consoleMessages : nativeConsoleMessages)(await browser.page(input.pageUrl), input)));
|
|
319
|
+
// ponytail: native network and storage need jsb XHR hooks and a jsb.localStorage reader; add them when a native game needs them.
|
|
320
|
+
if (web) {
|
|
321
|
+
server.registerTool('cocos_network_requests', {
|
|
322
|
+
description: 'List recent network requests since the server attached: id, method, URL with secret query values masked, resource type, status, failure, and duration. Use cocos_network_request for headers and bodies.',
|
|
323
|
+
inputSchema: z.object({ pageUrl, urlContains: z.string().min(1).max(500).optional(), resourceType: z.string().min(1).max(40).optional(), failedOnly: z.boolean().optional(), limit: z.number().int().min(1).max(200).optional() }).strict(),
|
|
324
|
+
annotations: browserData,
|
|
325
|
+
}, input => run(async () => networkRequests(await browser.page(input.pageUrl), input)));
|
|
326
|
+
server.registerTool('cocos_network_request', {
|
|
327
|
+
description: 'Return one request by id from cocos_network_requests: headers with authorization and cookies masked, status, timing, and optionally text bodies up to 20 KB with secret-like JSON and form fields masked.',
|
|
328
|
+
inputSchema: z.object({ pageUrl, id: z.number().int().min(1), includeBody: z.boolean().optional() }).strict(),
|
|
329
|
+
annotations: browserData,
|
|
330
|
+
}, input => run(async () => networkRequest(await browser.page(input.pageUrl), input.id, input.includeBody ?? false)));
|
|
331
|
+
server.registerTool('cocos_storage', {
|
|
332
|
+
description: 'Read localStorage or sessionStorage entries, or cookie names and attributes for the page. Secret-like keys and every cookie value are masked; JSON values are masked by key.',
|
|
333
|
+
inputSchema: z.object({ pageUrl, area: z.enum(['local', 'session', 'cookies']), keyContains: z.string().min(1).max(200).optional(), limit: z.number().int().min(1).max(500).optional() }).strict(),
|
|
334
|
+
annotations: browserData,
|
|
335
|
+
}, input => run(async () => storage(await browser.page(input.pageUrl), input.area, input.keyContains, input.limit)));
|
|
336
|
+
}
|
|
231
337
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
338
|
+
if (web)
|
|
339
|
+
server.registerTool('cocos_get_selection', {
|
|
340
|
+
description: 'Return the node the user last Alt+clicked on the game canvas. The first call installs the picker; Alt+clicks are kept from the game, other input is untouched. disable removes the picker, overlay, and selection.',
|
|
341
|
+
inputSchema: z.object({ pageUrl, disable: z.boolean().optional() }).strict(),
|
|
342
|
+
// Installs a page-level listener and DOM overlay only; never touches the Cocos graph.
|
|
343
|
+
annotations: { ...readOnly, readOnlyHint: false },
|
|
344
|
+
}, input => execute({ action: 'selection', disable: input.disable }, input.pageUrl));
|
|
345
|
+
if (web)
|
|
346
|
+
server.registerTool('cocos_highlight_node', {
|
|
347
|
+
description: 'Temporarily draw a pointer-transparent DOM overlay around one Cocos UI node.',
|
|
348
|
+
inputSchema: z.object({ pageUrl, ...target, durationMs: z.number().int().min(100).max(10_000).optional() }).strict().refine(oneTarget, oneTargetMessage),
|
|
349
|
+
annotations: temporaryMutation,
|
|
350
|
+
}, input => executeNode(input, uuid => ({ action: 'highlightNode', uuid, durationMs: input.durationMs })));
|
|
243
351
|
return server;
|
|
244
352
|
}
|