cocos-web-inspector-mcp 2.3.0 → 2.4.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 +24 -4
- package/dist/src/bridge.d.ts +27 -2
- package/dist/src/bridge.js +507 -15
- package/dist/src/browser.js +6 -3
- package/dist/src/native.d.ts +3 -2
- package/dist/src/native.js +15 -4
- package/dist/src/server.js +194 -60
- package/package.json +1 -1
package/dist/src/browser.js
CHANGED
|
@@ -82,8 +82,10 @@ export class BrowserConnection {
|
|
|
82
82
|
if (pageUrl) {
|
|
83
83
|
const wanted = validateLocalUrl(pageUrl, true).href;
|
|
84
84
|
const selected = pages.filter(candidate => new URL(candidate.url()).href === wanted);
|
|
85
|
-
if (selected.length === 0)
|
|
86
|
-
|
|
85
|
+
if (selected.length === 0) {
|
|
86
|
+
const open = pages.slice(0, 10).map(candidate => sanitizeUrl(candidate.url())).join(', ');
|
|
87
|
+
throw new InspectorError('PAGE_NOT_FOUND', `Local page not found: ${sanitizeUrl(pageUrl)} (pageUrl must equal the full open page URL including query and fragment; open pages, shown without them: ${open || 'none'})`);
|
|
88
|
+
}
|
|
87
89
|
if (selected.length > 1)
|
|
88
90
|
throw new InspectorError('MULTIPLE_PAGES', `Multiple localhost pages match pageUrl: ${sanitizeUrl(pageUrl)}; close duplicate tabs or use a separate Chromium per project`);
|
|
89
91
|
return selected[0];
|
|
@@ -139,7 +141,8 @@ export class BrowserConnection {
|
|
|
139
141
|
const hint = /\b404\b/.test(message)
|
|
140
142
|
? '; port is likely held by Chrome built-in remote debugging (chrome://inspect/#remote-debugging), which has no /json/version: turn it off or use another port, check with lsof -nP -iTCP:<port> -sTCP:LISTEN'
|
|
141
143
|
: '';
|
|
142
|
-
|
|
144
|
+
const launch = /ECONNREFUSED/.test(message) ? '; no Chromium is listening there: start one with --remote-debugging-port=<port> --user-data-dir=<dir>, or point the server at the right port with --cdp-endpoint / COCOS_CDP_ENDPOINT' : '';
|
|
145
|
+
throw new InspectorError('CDP_UNAVAILABLE', `Unable to connect to Chromium CDP at ${sanitizeUrl(this.endpoint)}: ${message}${hint}${launch}`);
|
|
143
146
|
});
|
|
144
147
|
this.#connecting = connecting;
|
|
145
148
|
try {
|
package/dist/src/native.d.ts
CHANGED
|
@@ -13,9 +13,9 @@ export declare class NativeConnection {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function nativeClick(page: Page, uuid: string): Promise<unknown>;
|
|
15
15
|
export declare function nativeDrag(page: Page, uuid: string, dx: number, dy: number, durationMs?: number): Promise<unknown>;
|
|
16
|
-
export declare function nativeCaptureNode(page: Page, uuid: string): Promise<unknown>;
|
|
16
|
+
export declare function nativeCaptureNode(page: Page, uuid: string, maxBytes?: number): Promise<unknown>;
|
|
17
17
|
/** Crops a raw screencap to the clip and encodes an RGB PNG, sampling every n-th pixel until it fits the response. */
|
|
18
|
-
export declare function cropPng(raw: Buffer, clip: Clip): unknown;
|
|
18
|
+
export declare function cropPng(raw: Buffer, clip: Clip, maxBytes?: number): unknown;
|
|
19
19
|
/**
|
|
20
20
|
* The inspector port from the engine's own "Debugger listening" logcat lines: the latest one, or `expected` only when
|
|
21
21
|
* some line names it. Any app can log under the Cocos tag, so reconnects pass the port they already used.
|
|
@@ -34,6 +34,7 @@ export declare function nativeConsoleMessages(page: Page, options: {
|
|
|
34
34
|
textContains?: string | undefined;
|
|
35
35
|
limit?: number | undefined;
|
|
36
36
|
}): Promise<unknown>;
|
|
37
|
+
export declare function nativePressKey(_page: Page, key: string): Promise<unknown>;
|
|
37
38
|
export declare function nativeTypeText(page: Page, uuid: string, text: string, submit: boolean): Promise<unknown>;
|
|
38
39
|
export declare function nativeNetworkRequests(page: Page, options: Parameters<typeof selectRequests>[1]): Promise<unknown>;
|
|
39
40
|
export declare function nativeNetworkRequest(page: Page, id: number, includeBody: boolean): Promise<unknown>;
|
package/dist/src/native.js
CHANGED
|
@@ -122,15 +122,15 @@ export async function nativeDrag(page, uuid, dx, dy, durationMs = 300) {
|
|
|
122
122
|
// Raw `screencap` output: width, height, and pixel format as uint32 LE, a colorspace word on Android 12+, then RGBA rows
|
|
123
123
|
// in the current display rotation. The device gzips it (18 MB raw takes ~20 s over Wi-Fi adb, ~1.5 s gzipped), and cropping
|
|
124
124
|
// raw pixels needs only zlib here, where decoding `screencap -p` would need a PNG decoder.
|
|
125
|
-
export async function nativeCaptureNode(page, uuid) {
|
|
125
|
+
export async function nativeCaptureNode(page, uuid, maxBytes) {
|
|
126
126
|
const clip = await visibleClip(page, uuid);
|
|
127
127
|
if ('reason' in clip)
|
|
128
128
|
return { captured: false, reason: clip.reason };
|
|
129
129
|
const raw = gunzipSync(await adbBuffer(['exec-out', 'screencap | gzip -1']), { maxOutputLength: 256 * 1024 * 1024 });
|
|
130
|
-
return cropPng(raw, clip);
|
|
130
|
+
return cropPng(raw, clip, maxBytes);
|
|
131
131
|
}
|
|
132
132
|
/** Crops a raw screencap to the clip and encodes an RGB PNG, sampling every n-th pixel until it fits the response. */
|
|
133
|
-
export function cropPng(raw, clip) {
|
|
133
|
+
export function cropPng(raw, clip, maxBytes) {
|
|
134
134
|
if (raw.length < 12)
|
|
135
135
|
return { captured: false, reason: 'SCREENCAP_FAILED' };
|
|
136
136
|
const screenWidth = raw.readUInt32LE(0);
|
|
@@ -158,7 +158,7 @@ export function cropPng(raw, clip) {
|
|
|
158
158
|
raw.copy(rows, out + x * 3, source + x * step * 4, source + x * step * 4 + 3);
|
|
159
159
|
}
|
|
160
160
|
const data = png(width, height, rows).toString('base64');
|
|
161
|
-
if (fitsImage(data))
|
|
161
|
+
if (fitsImage(data, maxBytes))
|
|
162
162
|
return { captured: true, mimeType: 'image/png', data, width: right - left, height: bottom - top, ...(step > 1 ? { scale: 1 / step } : {}) };
|
|
163
163
|
}
|
|
164
164
|
return { captured: false, reason: 'RESPONSE_LIMIT' };
|
|
@@ -229,6 +229,17 @@ export async function nativeConsoleMessages(page, options) {
|
|
|
229
229
|
note: 'Cocos-tagged logcat lines of the app process since it started, bounded by the device log buffer. Engine lines start with D/ or E/; an uncaught JS error spans several error lines (message, then stack).',
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
|
+
const androidKeys = { Enter: 'KEYCODE_ENTER', Escape: 'KEYCODE_ESCAPE', Tab: 'KEYCODE_TAB', Delete: 'KEYCODE_DEL', Space: 'KEYCODE_SPACE', ArrowUp: 'KEYCODE_DPAD_UP', ArrowDown: 'KEYCODE_DPAD_DOWN', ArrowLeft: 'KEYCODE_DPAD_LEFT', ArrowRight: 'KEYCODE_DPAD_RIGHT', Home: 'KEYCODE_MOVE_HOME', End: 'KEYCODE_MOVE_END', PageUp: 'KEYCODE_PAGE_UP', PageDown: 'KEYCODE_PAGE_DOWN' };
|
|
233
|
+
// The native engine reports AKEYCODE_DEL (the physical Backspace key) as Delete, has no entry for FORWARD_DEL, and reports Backspace only for
|
|
234
|
+
// AKEYCODE_BACK, which leaves the app. So Delete maps to DEL and Backspace is refused rather than sent as a key the game would misread.
|
|
235
|
+
export async function nativePressKey(_page, key) {
|
|
236
|
+
if (key === 'Backspace')
|
|
237
|
+
throw new Error('Invalid mutation: native builds deliver Backspace only for the system Back key, which leaves the app; use Delete, which the engine reports for the device Backspace key');
|
|
238
|
+
// Only validated key names reach adb, never free text.
|
|
239
|
+
await adb(['shell', 'input', 'keyevent', androidKeys[key] ?? `KEYCODE_${key.toUpperCase()}`]);
|
|
240
|
+
await settle();
|
|
241
|
+
return { pressed: true, key, input: 'adb', runtimeOnly: true };
|
|
242
|
+
}
|
|
232
243
|
// Tapping an EditBox on Android opens CocosEditBoxActivity, a native EditText whose every change fires text-changed in the game.
|
|
233
244
|
export async function nativeTypeText(page, uuid, text, submit) {
|
|
234
245
|
// input text only types printable ASCII and turns "%s" into a space, so refuse what it would silently mangle.
|
package/dist/src/server.js
CHANGED
|
@@ -2,11 +2,15 @@ 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 { nativeCaptureNode, nativeClick, nativeConsoleMessages, nativeDrag, nativeNetworkRequest, nativeNetworkRequests, nativeTypeText } from './native.js';
|
|
5
|
+
import { nativeCaptureNode, nativeClick, nativeConsoleMessages, nativeDrag, nativeNetworkRequest, nativeNetworkRequests, nativePressKey, nativeTypeText } from './native.js';
|
|
6
6
|
import { consoleMessages, networkRequest, networkRequests, storage } from './browser-data.js';
|
|
7
|
-
import { captureNode, clickNode, devicePresetNames, dragNode, emulateDevice, inspectCocosPage, networkProfileNames, runBridge, typeText } from './bridge.js';
|
|
7
|
+
import { captureNode, clickNode, devicePresetNames, dragNode, emulateDevice, fitsResponse, inspectCocosPage, networkProfileNames, keyPattern, pressKey, runBridge, typeText } from './bridge.js';
|
|
8
8
|
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
9
|
-
|
|
9
|
+
// Agents send "" for unused optional fields; treat it as omitted.
|
|
10
|
+
const blank = (schema) => z.preprocess(value => value === '' ? undefined : value, schema.optional());
|
|
11
|
+
// Strict object whose unknown-key error also lists the accepted keys.
|
|
12
|
+
const obj = (shape) => z.object(shape, { error: issue => issue.code === 'unrecognized_keys' ? `Unrecognized key(s): ${issue.keys.join(', ')}; accepted: ${Object.keys(shape).join(', ')}` : undefined }).strict();
|
|
13
|
+
const pageUrl = blank(z.url());
|
|
10
14
|
const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11
15
|
const temporaryMutation = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
12
16
|
const runtimeMutation = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
@@ -24,9 +28,9 @@ const propertyValue = z.union([
|
|
|
24
28
|
z.object({ r: finiteNumber, g: finiteNumber, b: finiteNumber, a: finiteNumber }).strict(),
|
|
25
29
|
]);
|
|
26
30
|
// 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)
|
|
31
|
+
const target = { uuid: blank(z.string().min(1).max(200)), path: blank(z.string().min(1).max(10_000)) };
|
|
28
32
|
const oneTarget = (value) => (value.uuid === undefined) !== (value.path === undefined);
|
|
29
|
-
const oneTargetMessage = 'Provide uuid or path, not both';
|
|
33
|
+
const oneTargetMessage = { error: (issue) => issue.input?.uuid === undefined && issue.input?.path === undefined ? 'Provide uuid or path' : 'Provide uuid or path, not both' };
|
|
30
34
|
async function resolveUuid(page, input) {
|
|
31
35
|
if (input.uuid)
|
|
32
36
|
return input.uuid;
|
|
@@ -51,10 +55,10 @@ function failure(error) {
|
|
|
51
55
|
// Playwright wraps in-page errors as "page.evaluate: Error: <message>\n<stack>".
|
|
52
56
|
const message = raw.replace(/^page\.evaluate: (?:Error: )?/, '').split('\n')[0];
|
|
53
57
|
const code = message === 'Cocos Creator 3.x runtime not found' ? 'COCOS_NOT_FOUND'
|
|
54
|
-
: message
|
|
58
|
+
: message.startsWith('Active Cocos scene not found') ? 'SCENE_NOT_READY'
|
|
55
59
|
: message.startsWith('Node not found') ? 'NODE_NOT_FOUND'
|
|
56
|
-
: message
|
|
57
|
-
: message
|
|
60
|
+
: message.startsWith('Component not found') ? 'COMPONENT_NOT_FOUND'
|
|
61
|
+
: message.startsWith('Ambiguous component type') ? 'AMBIGUOUS_COMPONENT'
|
|
58
62
|
: message.startsWith('Invalid mutation') ? 'INVALID_MUTATION'
|
|
59
63
|
: 'CDP_UNAVAILABLE';
|
|
60
64
|
return { ...response({ code, message }), isError: true };
|
|
@@ -65,6 +69,73 @@ export function createServer(browser, options = {}) {
|
|
|
65
69
|
// the DOM overlays draw nothing, the 2D batcher runs in C++ where analyze_batches cannot observe it,
|
|
66
70
|
// and game.step renders outside the native frame loop, which crashed a 3.8.8 Android build in the GFX pipeline.
|
|
67
71
|
const web = !options.native;
|
|
72
|
+
const pageIds = new WeakMap();
|
|
73
|
+
const baselines = new Map();
|
|
74
|
+
let nextPageId = 1;
|
|
75
|
+
let revision = 0;
|
|
76
|
+
const pageId = (page) => {
|
|
77
|
+
let id = pageIds.get(page);
|
|
78
|
+
if (id === undefined) {
|
|
79
|
+
id = nextPageId++;
|
|
80
|
+
pageIds.set(page, id);
|
|
81
|
+
}
|
|
82
|
+
return id;
|
|
83
|
+
};
|
|
84
|
+
const observeChanges = async (input) => {
|
|
85
|
+
try {
|
|
86
|
+
const page = await browser.page(input.pageUrl);
|
|
87
|
+
const uuid = input.uuid ?? (input.path ? await resolveUuid(page, input) : undefined);
|
|
88
|
+
const snapshot = JSON.parse(JSON.stringify(await runBridge(page, { action: 'semanticSnapshot', uuid, maxDepth: input.maxDepth, maxNodes: input.maxNodes })));
|
|
89
|
+
const selector = input.uuid ? `uuid:${input.uuid}` : input.path ? `path:${input.path}` : 'scene';
|
|
90
|
+
const key = JSON.stringify([pageId(page), page.url(), snapshot.documentId ?? null, selector, input.maxDepth ?? 6, input.maxNodes ?? 500]);
|
|
91
|
+
const prior = baselines.get(key);
|
|
92
|
+
if (input.reset)
|
|
93
|
+
baselines.delete(key);
|
|
94
|
+
const baseline = input.reset ? undefined : prior;
|
|
95
|
+
const full = (reason) => {
|
|
96
|
+
const next = { snapshot, revision: ++revision };
|
|
97
|
+
baselines.delete(key);
|
|
98
|
+
baselines.set(key, next);
|
|
99
|
+
while (baselines.size > 32)
|
|
100
|
+
baselines.delete(baselines.keys().next().value);
|
|
101
|
+
return { mode: 'full', reason, revision: next.revision, snapshot };
|
|
102
|
+
};
|
|
103
|
+
if (!baseline)
|
|
104
|
+
return response(full(input.reset ? 'RESET' : 'BASELINE_MISSING'));
|
|
105
|
+
baselines.delete(key);
|
|
106
|
+
baselines.set(key, baseline);
|
|
107
|
+
const before = new Map((baseline.snapshot.nodes ?? []).map((node) => [node.uuid, node]));
|
|
108
|
+
const after = new Map((snapshot.nodes ?? []).map((node) => [node.uuid, node]));
|
|
109
|
+
const nodes = [...after.values()];
|
|
110
|
+
const added = nodes.filter(node => !before.has(node.uuid));
|
|
111
|
+
const removed = [...before.keys()].filter(uuid => !after.has(uuid));
|
|
112
|
+
const changed = nodes.flatMap(node => {
|
|
113
|
+
const old = before.get(node.uuid);
|
|
114
|
+
if (!old)
|
|
115
|
+
return [];
|
|
116
|
+
const changes = Object.fromEntries([...new Set([...Object.keys(old), ...Object.keys(node)])]
|
|
117
|
+
.filter(field => field !== 'uuid' && JSON.stringify(node[field]) !== JSON.stringify(old[field]))
|
|
118
|
+
.map(field => [field, field in node ? node[field] : null]));
|
|
119
|
+
return Object.keys(changes).length ? [{ uuid: node.uuid, changes }] : [];
|
|
120
|
+
});
|
|
121
|
+
const metadataChanged = JSON.stringify({ truncated: snapshot.truncated, truncationReasons: snapshot.truncationReasons }) !== JSON.stringify({ truncated: baseline.snapshot.truncated, truncationReasons: baseline.snapshot.truncationReasons });
|
|
122
|
+
if (!added.length && !removed.length && !changed.length && !metadataChanged)
|
|
123
|
+
return response({ mode: 'unchanged', revision: baseline.revision });
|
|
124
|
+
if (metadataChanged)
|
|
125
|
+
return response(full('TRUNCATION_CHANGED'));
|
|
126
|
+
const delta = { added, removed, changed };
|
|
127
|
+
const deltaResult = { mode: 'delta', revision: revision + 1, delta };
|
|
128
|
+
if (JSON.stringify(delta).length >= JSON.stringify(snapshot).length || !fitsResponse(deltaResult))
|
|
129
|
+
return response(full('DELTA_NOT_SMALLER_THAN_FULL'));
|
|
130
|
+
const next = { snapshot, revision: ++revision };
|
|
131
|
+
baselines.delete(key);
|
|
132
|
+
baselines.set(key, next);
|
|
133
|
+
return response({ ...deltaResult, revision: next.revision });
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return failure(error);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
68
139
|
const execute = async (request, selectedPage) => {
|
|
69
140
|
try {
|
|
70
141
|
return response(await runBridge(await browser.page(selectedPage), request));
|
|
@@ -85,7 +156,7 @@ export function createServer(browser, options = {}) {
|
|
|
85
156
|
const executeNode = (input, build) => withNode(input, (page, uuid) => runBridge(page, build(uuid)));
|
|
86
157
|
server.registerTool('cocos_list_pages', {
|
|
87
158
|
description: 'List bounded summaries of eligible localhost Chromium pages.',
|
|
88
|
-
inputSchema:
|
|
159
|
+
inputSchema: obj({}),
|
|
89
160
|
annotations: readOnly,
|
|
90
161
|
}, async () => {
|
|
91
162
|
try {
|
|
@@ -102,95 +173,120 @@ export function createServer(browser, options = {}) {
|
|
|
102
173
|
});
|
|
103
174
|
server.registerTool('cocos_runtime_info', {
|
|
104
175
|
description: 'Return bounded runtime information for one localhost Cocos Creator 3.x page.',
|
|
105
|
-
inputSchema:
|
|
176
|
+
inputSchema: obj({ pageUrl }),
|
|
106
177
|
annotations: readOnly,
|
|
107
178
|
}, input => execute({ action: 'runtimeInfo' }, input.pageUrl));
|
|
108
179
|
server.registerTool('cocos_runtime_diagnostics', {
|
|
109
180
|
description: 'Return bounded passive Cocos scene hierarchy diagnostics.',
|
|
110
|
-
inputSchema:
|
|
181
|
+
inputSchema: obj({ pageUrl }),
|
|
111
182
|
annotations: readOnly,
|
|
112
183
|
}, input => execute({ action: 'runtimeDiagnostics' }, input.pageUrl));
|
|
113
184
|
server.registerTool('cocos_scene_tree', {
|
|
114
185
|
description: 'Return a bounded Cocos Creator 3.x scene tree from a localhost Chromium page.',
|
|
115
|
-
inputSchema:
|
|
186
|
+
inputSchema: obj({ pageUrl, maxDepth: z.number().int().min(0).max(20).optional(), maxNodes: z.number().int().min(1).max(5_000).optional() }),
|
|
116
187
|
annotations: readOnly,
|
|
117
188
|
}, input => execute({ action: 'sceneTree', maxDepth: input.maxDepth, maxNodes: input.maxNodes }, input.pageUrl));
|
|
118
189
|
server.registerTool('cocos_find_node', {
|
|
119
190
|
description: 'Find Cocos nodes with bounded exact and filter criteria.',
|
|
120
|
-
inputSchema:
|
|
191
|
+
inputSchema: obj({ pageUrl, uuid: blank(z.string().min(1).max(200)), name: blank(z.string().min(1).max(500)), path: blank(z.string().min(1).max(10_000)), nameContains: blank(z.string().min(1).max(500)), componentType: blank(z.string().min(1).max(120)), active: z.boolean().optional(), pathPrefix: blank(z.string().min(1).max(10_000)), limit: z.number().int().min(1).max(100).optional() }).strict()
|
|
121
192
|
.refine(value => [value.uuid, value.name, value.path, value.nameContains, value.componentType, value.active, value.pathPrefix].some(item => item !== undefined), 'Provide at least one filter'),
|
|
122
193
|
annotations: readOnly,
|
|
123
194
|
}, 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));
|
|
124
195
|
server.registerTool('cocos_get_node', {
|
|
125
196
|
description: 'Return bounded context for one exact Cocos node UUID.',
|
|
126
|
-
inputSchema:
|
|
197
|
+
inputSchema: obj({ pageUrl, ...target }).refine(oneTarget, oneTargetMessage),
|
|
127
198
|
annotations: readOnly,
|
|
128
199
|
}, input => executeNode(input, uuid => ({ action: 'getNode', uuid })));
|
|
129
200
|
server.registerTool('cocos_snapshot_subtree', {
|
|
130
201
|
description: 'Return a bounded stateless subtree snapshot for one exact Cocos node UUID.',
|
|
131
|
-
inputSchema:
|
|
202
|
+
inputSchema: obj({ pageUrl, ...target, maxDepth: z.number().int().min(0).max(20).optional(), maxNodes: z.number().int().min(1).max(5_000).optional() }).refine(oneTarget, oneTargetMessage),
|
|
132
203
|
annotations: readOnly,
|
|
133
204
|
}, input => executeNode(input, uuid => ({ action: 'snapshotSubtree', uuid, maxDepth: input.maxDepth, maxNodes: input.maxNodes })));
|
|
205
|
+
server.registerTool('cocos_observe_changes', {
|
|
206
|
+
description: 'Observe bounded semantic scene changes since the prior matching call.',
|
|
207
|
+
inputSchema: obj({ pageUrl, ...target, reset: z.boolean().optional(), maxDepth: z.number().int().min(0).max(20).optional(), maxNodes: z.number().int().min(1).max(5_000).optional() }).refine(value => !(value.uuid && value.path), oneTargetMessage),
|
|
208
|
+
annotations: readOnly,
|
|
209
|
+
}, input => observeChanges(input));
|
|
134
210
|
server.registerTool('cocos_get_node_bounds', {
|
|
135
211
|
description: 'Return bounded canvas and viewport bounds for one Cocos UI node UUID.',
|
|
136
|
-
inputSchema:
|
|
212
|
+
inputSchema: obj({ pageUrl, ...target }).refine(oneTarget, oneTargetMessage),
|
|
137
213
|
annotations: readOnly,
|
|
138
214
|
}, input => executeNode(input, uuid => ({ action: 'getNodeBounds', uuid })));
|
|
139
215
|
server.registerTool('cocos_capture_node', {
|
|
140
|
-
description: 'Capture a bounded viewport-clipped PNG for one visible Cocos UI node UUID.',
|
|
141
|
-
inputSchema:
|
|
216
|
+
description: 'Capture a bounded viewport-clipped PNG for one visible Cocos UI node UUID. Pass maxBytes to cap the base64 image size when the client rejects large results.',
|
|
217
|
+
inputSchema: obj({ pageUrl, ...target, maxBytes: z.number().int().min(1_000).max(200_000).optional() }).refine(oneTarget, oneTargetMessage),
|
|
142
218
|
annotations: readOnly,
|
|
143
|
-
}, input => withNode(input, web ? captureNode : nativeCaptureNode));
|
|
219
|
+
}, input => withNode(input, (page, uuid) => (web ? captureNode : nativeCaptureNode)(page, uuid, input.maxBytes)));
|
|
144
220
|
server.registerTool('cocos_get_components', {
|
|
145
221
|
description: 'List bounded component summaries for one Cocos node UUID.',
|
|
146
|
-
inputSchema:
|
|
222
|
+
inputSchema: obj({ pageUrl, ...target }).refine(oneTarget, oneTargetMessage),
|
|
147
223
|
annotations: readOnly,
|
|
148
224
|
}, input => executeNode(input, uuid => ({ action: 'getComponents', uuid })));
|
|
149
225
|
server.registerTool('cocos_get_properties', {
|
|
150
226
|
description: 'Read bounded, cycle-safe public properties from a Cocos node or one component.',
|
|
151
|
-
inputSchema:
|
|
227
|
+
inputSchema: obj({ pageUrl, ...target, componentType: blank(z.string().min(1)), componentUuid: blank(z.string().min(1)), maxDepth: z.number().int().min(0).max(6).optional() }).refine(oneTarget, oneTargetMessage)
|
|
152
228
|
.refine(value => !(value.componentType && value.componentUuid), 'Provide componentType or componentUuid, not both'),
|
|
153
229
|
annotations: readOnly,
|
|
154
230
|
}, input => executeNode(input, uuid => ({ action: 'getProperties', uuid, componentType: input.componentType, componentUuid: input.componentUuid, maxDepth: input.maxDepth })));
|
|
155
231
|
server.registerTool('cocos_wait_for_property', {
|
|
156
232
|
description: 'Poll one top-level property of a Cocos node or component until it equals a value or the timeout passes.',
|
|
157
|
-
inputSchema:
|
|
233
|
+
inputSchema: obj({ pageUrl, ...target, componentType: blank(z.string().min(1)), componentUuid: blank(z.string().min(1)), 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() }).refine(oneTarget, oneTargetMessage)
|
|
158
234
|
.refine(value => !(value.componentType && value.componentUuid), 'Provide componentType or componentUuid, not both'),
|
|
159
235
|
annotations: readOnly,
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
}, async (input) => {
|
|
237
|
+
try {
|
|
238
|
+
const page = await browser.page(input.pageUrl);
|
|
239
|
+
const deadline = Date.now() + (input.timeoutMs ?? 5_000);
|
|
240
|
+
let polls = 0;
|
|
241
|
+
let value;
|
|
242
|
+
let gone;
|
|
243
|
+
// A path is resolved on every poll so a scene switch or a node still being created does not end the wait early.
|
|
244
|
+
for (let uuid = input.uuid;;) {
|
|
245
|
+
polls++;
|
|
246
|
+
try {
|
|
247
|
+
uuid ??= await resolveUuid(page, input);
|
|
248
|
+
value = (await runBridge(page, { action: 'getProperties', uuid, componentType: input.componentType, componentUuid: input.componentUuid, maxDepth: 0 })).properties?.[input.key];
|
|
249
|
+
gone = undefined;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const message = error instanceof Error ? error.message : '';
|
|
253
|
+
if (!/Node not found|Active Cocos scene not found|Component not found/.test(message))
|
|
254
|
+
throw error;
|
|
255
|
+
gone = message.replace(/^page\.evaluate: (?:Error: )?/, '').split('\n')[0];
|
|
256
|
+
value = undefined;
|
|
257
|
+
if (!input.uuid)
|
|
258
|
+
uuid = undefined;
|
|
259
|
+
}
|
|
260
|
+
if (gone === undefined && value === input.equals || Date.now() >= deadline)
|
|
261
|
+
break;
|
|
262
|
+
await new Promise(resolve => setTimeout(resolve, input.intervalMs ?? 200));
|
|
263
|
+
}
|
|
264
|
+
return response({ matched: gone === undefined && value === input.equals, key: input.key, value: value ?? null, polls, ...(gone ? { gone } : {}) });
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
return failure(error);
|
|
171
268
|
}
|
|
172
|
-
|
|
173
|
-
}));
|
|
269
|
+
});
|
|
174
270
|
if (options.allowRuntimeMutation) {
|
|
175
271
|
server.registerTool('cocos_set_node_active', {
|
|
176
272
|
description: 'Set the active state of one Cocos node selected by exact UUID.',
|
|
177
|
-
inputSchema:
|
|
273
|
+
inputSchema: obj({ pageUrl, ...target, active: z.boolean() }).refine(oneTarget, oneTargetMessage),
|
|
178
274
|
annotations: runtimeMutation,
|
|
179
275
|
}, input => executeNode(input, uuid => ({ action: 'setNodeActive', uuid, active: input.active })));
|
|
180
276
|
server.registerTool('cocos_set_transform', {
|
|
181
277
|
description: 'Update selected Cocos node position, rotation, or scale by exact UUID.',
|
|
182
|
-
inputSchema:
|
|
278
|
+
inputSchema: obj({ pageUrl, ...target, position: vector3.optional(), rotation: quaternion.optional(), scale: vector3.optional() }).refine(oneTarget, oneTargetMessage)
|
|
183
279
|
.refine(value => !!value.position || !!value.rotation || !!value.scale, 'Provide position, rotation, or scale'),
|
|
184
280
|
annotations: runtimeMutation,
|
|
185
281
|
}, input => executeNode(input, uuid => ({ action: 'setTransform', uuid, position: input.position, rotation: input.rotation, scale: input.scale })));
|
|
186
282
|
server.registerTool('cocos_set_property', {
|
|
187
283
|
description: 'Update one bounded public data property selected by node and component UUID.',
|
|
188
|
-
inputSchema:
|
|
284
|
+
inputSchema: obj({ pageUrl, ...target, componentUuid: z.string().min(1), key: z.string().min(1).max(200), value: propertyValue }).refine(oneTarget, oneTargetMessage),
|
|
189
285
|
annotations: runtimeMutation,
|
|
190
286
|
}, input => executeNode(input, uuid => ({ action: 'setProperty', uuid, componentUuid: input.componentUuid, key: input.key, value: input.value })));
|
|
191
287
|
server.registerTool('cocos_click_node', {
|
|
192
288
|
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.',
|
|
193
|
-
inputSchema:
|
|
289
|
+
inputSchema: obj({ pageUrl, ...target }).refine(oneTarget, oneTargetMessage),
|
|
194
290
|
// Game click handlers run arbitrary code: a login button reaches real servers, a buy button spends currency.
|
|
195
291
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
196
292
|
}, input => withNode(input, web ? clickNode : nativeClick));
|
|
@@ -198,48 +294,65 @@ export function createServer(browser, options = {}) {
|
|
|
198
294
|
const realInput = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
|
|
199
295
|
server.registerTool('cocos_drag_node', {
|
|
200
296
|
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.',
|
|
201
|
-
inputSchema:
|
|
297
|
+
inputSchema: obj({ 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() }).refine(oneTarget, oneTargetMessage)
|
|
202
298
|
.refine(value => value.dx !== 0 || value.dy !== 0, 'Provide a non-zero dx or dy'),
|
|
203
299
|
annotations: realInput,
|
|
204
300
|
}, 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)));
|
|
205
301
|
server.registerTool('cocos_type_text', {
|
|
206
302
|
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.',
|
|
207
|
-
inputSchema:
|
|
303
|
+
inputSchema: obj({ pageUrl, ...target, text: z.string().max(2_000), submit: z.boolean().optional() }).refine(oneTarget, oneTargetMessage),
|
|
208
304
|
annotations: realInput,
|
|
209
305
|
}, input => withNode(input, (page, uuid) => (web ? typeText : nativeTypeText)(page, uuid, input.text, input.submit ?? false)));
|
|
306
|
+
server.registerTool('cocos_press_key', {
|
|
307
|
+
description: 'Press and release one key with real keyboard input, for game keyboard handlers and focused inputs: Enter, Escape, Tab, Backspace, Delete, Space, arrows, Home, End, PageUp, PageDown, a letter, or a digit. To fill an EditBox use cocos_type_text. Handlers may call game servers.',
|
|
308
|
+
inputSchema: obj({ pageUrl, key: z.string().regex(keyPattern, 'Use Enter, Escape, Tab, Backspace, Delete, Space, ArrowUp/Down/Left/Right, Home, End, PageUp, PageDown, or one letter or digit') }),
|
|
309
|
+
annotations: realInput,
|
|
310
|
+
}, async (input) => {
|
|
311
|
+
try {
|
|
312
|
+
return response(await (web ? pressKey : nativePressKey)(await browser.page(input.pageUrl), input.key));
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
return failure(error);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
210
318
|
if (web)
|
|
211
319
|
server.registerTool('cocos_analyze_batches', {
|
|
212
320
|
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.',
|
|
213
|
-
inputSchema:
|
|
321
|
+
inputSchema: obj({ pageUrl, limit: z.number().int().min(1).max(500).optional(), tintMs: z.number().int().min(100).max(30_000).optional() }),
|
|
214
322
|
annotations: { ...runtimeMutation, idempotentHint: false },
|
|
215
323
|
}, input => execute({ action: 'analyzeBatches', limit: input.limit, tintMs: input.tintMs }, input.pageUrl));
|
|
324
|
+
server.registerTool('cocos_event_trace', {
|
|
325
|
+
description: 'Count Node events (emit calls) per event name and node for a bounded window, to find event storms such as a TRANSFORM_CHANGED or custom event fired every frame. Wraps Node.prototype.emit for durationMs and restores it; arguments are never recorded.',
|
|
326
|
+
inputSchema: obj({ pageUrl, durationMs: z.number().int().min(100).max(10_000).optional(), limit: z.number().int().min(1).max(200).optional() }),
|
|
327
|
+
annotations: { ...runtimeMutation, idempotentHint: false },
|
|
328
|
+
}, input => execute({ action: 'eventTrace', durationMs: input.durationMs, limit: input.limit }, input.pageUrl));
|
|
216
329
|
for (const action of ['pause', 'resume']) {
|
|
217
330
|
server.registerTool(`cocos_${action}`, {
|
|
218
331
|
description: `${action === 'pause' ? 'Pause' : 'Resume'} the Cocos director through its public API.`,
|
|
219
|
-
inputSchema:
|
|
332
|
+
inputSchema: obj({ pageUrl }),
|
|
220
333
|
annotations: runtimeMutation,
|
|
221
334
|
}, input => execute({ action }, input.pageUrl));
|
|
222
335
|
}
|
|
223
336
|
if (web)
|
|
224
337
|
server.registerTool('cocos_step_frame', {
|
|
225
338
|
description: 'Advance a paused Cocos game by fixed-delta frames through cc.game.step; requires cocos_pause first.',
|
|
226
|
-
inputSchema:
|
|
339
|
+
inputSchema: obj({ pageUrl, frames: z.number().int().min(1).max(60).optional() }),
|
|
227
340
|
annotations: { ...runtimeMutation, idempotentHint: false },
|
|
228
341
|
}, input => execute({ action: 'stepFrame', frames: input.frames }, input.pageUrl));
|
|
229
342
|
server.registerTool('cocos_set_time_scale', {
|
|
230
343
|
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.',
|
|
231
|
-
inputSchema:
|
|
344
|
+
inputSchema: obj({ pageUrl, scale: z.number().gt(0).max(100).optional() }),
|
|
232
345
|
annotations: runtimeMutation,
|
|
233
346
|
}, input => execute({ action: 'timeScale', scale: input.scale }, input.pageUrl));
|
|
234
347
|
server.registerTool('cocos_show_stats', {
|
|
235
348
|
description: 'Show or hide the Cocos profiler overlay (FPS, draw calls, triangles) through the public profiler API.',
|
|
236
|
-
inputSchema:
|
|
349
|
+
inputSchema: obj({ pageUrl, visible: z.boolean() }),
|
|
237
350
|
annotations: runtimeMutation,
|
|
238
351
|
}, input => execute({ action: 'showStats', visible: input.visible }, input.pageUrl));
|
|
239
352
|
if (web)
|
|
240
353
|
server.registerTool('cocos_emulate_device', {
|
|
241
354
|
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.',
|
|
242
|
-
inputSchema:
|
|
355
|
+
inputSchema: obj({
|
|
243
356
|
pageUrl,
|
|
244
357
|
reset: z.boolean().optional(),
|
|
245
358
|
preset: z.enum(devicePresetNames).optional(),
|
|
@@ -268,34 +381,55 @@ export function createServer(browser, options = {}) {
|
|
|
268
381
|
}
|
|
269
382
|
server.registerTool('cocos_explain_click', {
|
|
270
383
|
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.',
|
|
271
|
-
inputSchema:
|
|
384
|
+
inputSchema: obj({ pageUrl, ...target, x: z.number().min(0).max(20_000).optional(), y: z.number().min(0).max(20_000).optional() }).strict()
|
|
272
385
|
.refine(value => (value.x === undefined) === (value.y === undefined), 'Provide x and y together')
|
|
273
|
-
.refine(value => value.
|
|
274
|
-
|
|
386
|
+
.refine(value => value.x !== undefined
|
|
387
|
+
? value.uuid === undefined && value.path === undefined
|
|
388
|
+
: oneTarget(value), 'Provide uuid, path, or x and y'),
|
|
275
389
|
annotations: readOnly,
|
|
276
390
|
}, input => input.uuid === undefined && input.path === undefined
|
|
277
391
|
? execute({ action: 'explainClick', x: input.x, y: input.y }, input.pageUrl)
|
|
278
392
|
: executeNode(input, uuid => ({ action: 'explainClick', uuid, x: input.x, y: input.y })));
|
|
279
393
|
server.registerTool('cocos_listener_report', {
|
|
280
394
|
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.',
|
|
281
|
-
inputSchema:
|
|
395
|
+
inputSchema: obj({ pageUrl, limit: z.number().int().min(1).max(500).optional() }),
|
|
282
396
|
annotations: readOnly,
|
|
283
397
|
}, input => execute({ action: 'listenerReport', limit: input.limit }, input.pageUrl));
|
|
398
|
+
for (const [name, action, description] of [
|
|
399
|
+
['cocos_explain_visibility', 'explainVisibility', 'Explain why a node is not visible: inactive ancestors, disabled or transparent renderer, missing spriteFrame, empty Label, zero opacity/scale/size, a layer no active camera renders, outside the viewport, clipped by a Mask, or possibly covered by an opaque Sprite. Returns reasons (definite) and warnings (possible) without changing anything.'],
|
|
400
|
+
['cocos_explain_layout', 'explainLayout', 'Explain why a node has its size and position: its Widget edges and align mode, a parent Layout that places or resizes it, Label/Sprite size modes, ancestor scale, and the resulting viewport rectangle. Pair with cocos_emulate_device to compare resolutions.'],
|
|
401
|
+
]) {
|
|
402
|
+
server.registerTool(name, {
|
|
403
|
+
description,
|
|
404
|
+
inputSchema: obj({ pageUrl, ...target }).refine(oneTarget, oneTargetMessage),
|
|
405
|
+
annotations: readOnly,
|
|
406
|
+
}, input => executeNode(input, uuid => ({ action, uuid })));
|
|
407
|
+
}
|
|
408
|
+
server.registerTool('cocos_animation_report', {
|
|
409
|
+
description: 'List what is animating now: Animation components with playing states (speed, wrap mode, time), Spine/DragonBones components with their current animation, and tween targets with action counts. Read-only; nothing is played or stopped.',
|
|
410
|
+
inputSchema: obj({ pageUrl, limit: z.number().int().min(1).max(500).optional() }),
|
|
411
|
+
annotations: readOnly,
|
|
412
|
+
}, input => execute({ action: 'animationReport', limit: input.limit }, input.pageUrl));
|
|
413
|
+
server.registerTool('cocos_frame_profile', {
|
|
414
|
+
description: 'Sample real frames for a while and report frame time (avg, p50, p95, max), fps, spike count, the worst frames, and draw call and triangle ranges. Needs the game running (not paused). Subscribes to the director for the sample window only.',
|
|
415
|
+
inputSchema: obj({ pageUrl, frames: z.number().int().min(2).max(600).optional(), timeoutMs: z.number().int().min(100).max(30_000).optional() }),
|
|
416
|
+
annotations: { ...readOnly, idempotentHint: false },
|
|
417
|
+
}, input => execute({ action: 'frameProfile', frames: input.frames, timeoutMs: input.timeoutMs }, input.pageUrl));
|
|
284
418
|
server.registerTool('cocos_dynamic_atlas', {
|
|
285
419
|
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, ...).',
|
|
286
|
-
inputSchema:
|
|
420
|
+
inputSchema: obj({ pageUrl, limit: z.number().int().min(1).max(500).optional() }),
|
|
287
421
|
annotations: readOnly,
|
|
288
422
|
}, input => execute({ action: 'dynamicAtlas', limit: input.limit }, input.pageUrl));
|
|
289
423
|
server.registerTool('cocos_asset_report', {
|
|
290
424
|
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.',
|
|
291
|
-
inputSchema:
|
|
425
|
+
inputSchema: obj({ pageUrl, type: blank(z.string().min(1).max(120)), unusedOnly: z.boolean().optional(), limit: z.number().int().min(1).max(500).optional() }),
|
|
292
426
|
annotations: readOnly,
|
|
293
427
|
}, input => execute({ action: 'assetReport', type: input.type, unusedOnly: input.unusedOnly, limit: input.limit }, input.pageUrl));
|
|
294
428
|
if (options.allowMethodCall) {
|
|
295
429
|
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)]));
|
|
296
430
|
server.registerTool('cocos_call_method', {
|
|
297
431
|
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.',
|
|
298
|
-
inputSchema:
|
|
432
|
+
inputSchema: obj({ pageUrl, ...target, componentUuid: blank(z.string().min(1)), 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() }).refine(oneTarget, oneTargetMessage),
|
|
299
433
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
300
434
|
}, input => executeNode(input, uuid => ({ action: 'callMethod', uuid, componentUuid: input.componentUuid, method: input.method, args: input.args ?? [], awaitMs: input.awaitMs, maxDepth: input.maxDepth })));
|
|
301
435
|
}
|
|
@@ -312,36 +446,36 @@ export function createServer(browser, options = {}) {
|
|
|
312
446
|
};
|
|
313
447
|
server.registerTool('cocos_console_messages', {
|
|
314
448
|
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.',
|
|
315
|
-
inputSchema:
|
|
449
|
+
inputSchema: obj({ pageUrl, types: z.array(z.enum(['log', 'debug', 'info', 'error', 'warning', 'assert', 'trace', 'pageerror'])).max(8).optional(), textContains: blank(z.string().min(1).max(200)), limit: z.number().int().min(1).max(200).optional() }),
|
|
316
450
|
annotations: browserData,
|
|
317
451
|
}, input => run(async () => (web ? consoleMessages : nativeConsoleMessages)(await browser.page(input.pageUrl), input)));
|
|
318
452
|
server.registerTool('cocos_network_requests', {
|
|
319
453
|
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.',
|
|
320
|
-
inputSchema:
|
|
454
|
+
inputSchema: obj({ pageUrl, urlContains: blank(z.string().min(1).max(500)), resourceType: blank(z.string().min(1).max(40)), failedOnly: z.boolean().optional(), limit: z.number().int().min(1).max(200).optional() }),
|
|
321
455
|
annotations: browserData,
|
|
322
456
|
}, input => run(async () => (web ? networkRequests : nativeNetworkRequests)(await browser.page(input.pageUrl), input)));
|
|
323
457
|
server.registerTool('cocos_network_request', {
|
|
324
458
|
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.',
|
|
325
|
-
inputSchema:
|
|
459
|
+
inputSchema: obj({ pageUrl, id: z.number().int().min(1), includeBody: z.boolean().optional() }),
|
|
326
460
|
annotations: browserData,
|
|
327
461
|
}, input => run(async () => (web ? networkRequest : nativeNetworkRequest)(await browser.page(input.pageUrl), input.id, input.includeBody ?? false)));
|
|
328
462
|
server.registerTool('cocos_storage', {
|
|
329
463
|
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.',
|
|
330
|
-
inputSchema:
|
|
464
|
+
inputSchema: obj({ pageUrl, area: z.enum(['local', 'session', 'cookies']), keyContains: blank(z.string().min(1).max(200)), limit: z.number().int().min(1).max(500).optional() }),
|
|
331
465
|
annotations: browserData,
|
|
332
466
|
}, input => run(async () => storage(await browser.page(input.pageUrl), input.area, input.keyContains, input.limit)));
|
|
333
467
|
}
|
|
334
468
|
if (web)
|
|
335
469
|
server.registerTool('cocos_get_selection', {
|
|
336
470
|
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.',
|
|
337
|
-
inputSchema:
|
|
471
|
+
inputSchema: obj({ pageUrl, disable: z.boolean().optional() }),
|
|
338
472
|
// Installs a page-level listener and DOM overlay only; never touches the Cocos graph.
|
|
339
473
|
annotations: { ...readOnly, readOnlyHint: false },
|
|
340
474
|
}, input => execute({ action: 'selection', disable: input.disable }, input.pageUrl));
|
|
341
475
|
if (web)
|
|
342
476
|
server.registerTool('cocos_highlight_node', {
|
|
343
477
|
description: 'Temporarily draw a pointer-transparent DOM overlay around one Cocos UI node.',
|
|
344
|
-
inputSchema:
|
|
478
|
+
inputSchema: obj({ pageUrl, ...target, durationMs: z.number().int().min(100).max(10_000).optional() }).refine(oneTarget, oneTargetMessage),
|
|
345
479
|
annotations: temporaryMutation,
|
|
346
480
|
}, input => executeNode(input, uuid => ({ action: 'highlightNode', uuid, durationMs: input.durationMs })));
|
|
347
481
|
return server;
|