cocos-web-inspector-mcp 2.1.0 → 2.2.1
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 +26 -1
- package/dist/src/bridge.d.ts +6 -0
- package/dist/src/bridge.js +34 -14
- package/dist/src/index.js +11 -3
- package/dist/src/native.d.ts +26 -0
- package/dist/src/native.js +184 -0
- package/dist/src/server.d.ts +3 -1
- package/dist/src/server.js +87 -73
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,29 @@ chrome.exe --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 --u
|
|
|
45
45
|
|
|
46
46
|
Keep this browser profile separate from normal browsing. CDP provides code-execution-level access to attached pages.
|
|
47
47
|
|
|
48
|
+
### Native builds (Android)
|
|
49
|
+
|
|
50
|
+
A Cocos native build with **Debug** checked runs a V8 inspector inside the app; release builds do not. Attach to it over adb instead of Chromium:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
adb logcat -d -s Cocos | grep "Debugger listening" -A1 # prints ws=IP_ADDR_OF_THIS_DEVICE:<port>/...
|
|
54
|
+
adb forward tcp:<port> tcp:<port>
|
|
55
|
+
npx --yes cocos-web-inspector-mcp --native-endpoint http://127.0.0.1:<port>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The template asks for port 6086, but on Android the engine moves it above 37000 when it cannot list network interfaces (43086 on a Creator 3.8.8 build), so read the port from logcat. `--native-endpoint` must be loopback, and it replaces `--cdp-endpoint` for that server process.
|
|
59
|
+
|
|
60
|
+
The V8 inspector has no page, so native mode registers only the tools that read or change the Cocos graph, plus adb-backed input and logs:
|
|
61
|
+
|
|
62
|
+
- `cocos_click_node` and `cocos_drag_node` tap and swipe with `adb shell input`, at the node's viewport center, which equals screen pixels for a full-screen game. `steps` does not apply to a native drag. Set `ANDROID_SERIAL` when several devices are attached.
|
|
63
|
+
- `cocos_type_text` taps the EditBox, waits for the engine's Android input activity (`CocosEditBoxActivity`), clears it, and types with `adb shell input text`, so `text-changed` fires per character. Text must be printable ASCII without `%s`. The device keyboard still processes the keys, so autocorrect or a Vietnamese Telex layout can rewrite them (`test` becomes `tét`); the result reports `matches: false` with the text the game received. `submit` presses Enter, which closes a single-line box with `editing-did-ended`; `editing-return` fires only from the activity's confirm button. Clearing uses `input keycombination`, Android 12+.
|
|
64
|
+
- `cocos_console_messages` (with `--allow-browser-data`) reads the app process's `Cocos`-tagged logcat lines, so it includes logs from before the server attached, bounded by the device log buffer. `console.log` and `console.debug` both report as `log`; an uncaught error spans several `error` lines.
|
|
65
|
+
- Not registered: `cocos_capture_node`, `cocos_emulate_device`, `cocos_highlight_node`, `cocos_get_selection` (DOM overlays draw nothing), `cocos_analyze_batches` (the native 2D batcher runs in C++), `cocos_step_frame` (stepping from the inspector renders outside the native frame loop and crashed the app), and the network and storage tools.
|
|
66
|
+
|
|
67
|
+
The inspector accepts one session at a time, so the server connects per call; Chrome DevTools can attach between calls, and a call made while DevTools is attached fails with `CDP_UNAVAILABLE`. If calls start failing with `CDP_UNAVAILABLE` while the app runs, the adb server restarted or the device dropped off USB, which takes the forward with it; run `adb forward` again. When a phone keeps dropping off USB (common through hubs), switch adb to Wi-Fi with `adb tcpip 5555` and `adb connect <phone-ip>:5555`, then forward and set `ANDROID_SERIAL=<phone-ip>:5555`; the endpoint stays `127.0.0.1`.
|
|
68
|
+
|
|
69
|
+
Only a build with the inspector compiled in can attach. If a game's Debug build fails, setting `USE_V8_DEBUGGER_FORCE` and `CC_DEBUG_FORCE` to `ON` in `native/engine/common/CMakeLists.txt` keeps the inspector in a release build; never ship such a build.
|
|
70
|
+
|
|
48
71
|
## Install and run
|
|
49
72
|
|
|
50
73
|
Run the published npm package with the default CDP endpoint, `http://127.0.0.1:9222`:
|
|
@@ -73,7 +96,9 @@ Endpoint precedence is:
|
|
|
73
96
|
2. `COCOS_CDP_ENDPOINT`
|
|
74
97
|
3. `http://127.0.0.1:9222`
|
|
75
98
|
|
|
76
|
-
`--
|
|
99
|
+
`--native-endpoint` attaches to a native debug build instead (see Native builds).
|
|
100
|
+
|
|
101
|
+
`--cdp-endpoint`, `--native-endpoint`, and the `--allow-*` flags are the server options; `launch` and `doctor` are separate commands. `--cdp-endpoint` identifies the browser CDP endpoint, not a game page URL.
|
|
77
102
|
|
|
78
103
|
The server uses stdio for MCP. Standard output is reserved for protocol traffic; startup diagnostics are written to standard error.
|
|
79
104
|
|
package/dist/src/bridge.d.ts
CHANGED
|
@@ -134,6 +134,12 @@ type PropertyValue = boolean | number | string | {
|
|
|
134
134
|
};
|
|
135
135
|
export declare function runBridge(page: Page, request: BridgeRequest): Promise<unknown>;
|
|
136
136
|
export declare function captureNode(page: Page, uuid: string): Promise<unknown>;
|
|
137
|
+
export declare function nodeCenter(page: Page, uuid: string): Promise<{
|
|
138
|
+
x: number;
|
|
139
|
+
y: number;
|
|
140
|
+
} | {
|
|
141
|
+
reason: string;
|
|
142
|
+
}>;
|
|
137
143
|
export declare function clickNode(page: Page, uuid: string): Promise<unknown>;
|
|
138
144
|
export declare function dragNode(page: Page, uuid: string, dx: number, dy: number, steps?: number, durationMs?: number): Promise<unknown>;
|
|
139
145
|
export declare function typeText(page: Page, uuid: string, text: string, submit: boolean): Promise<unknown>;
|
package/dist/src/bridge.js
CHANGED
|
@@ -39,7 +39,7 @@ export async function captureNode(page, uuid) {
|
|
|
39
39
|
}
|
|
40
40
|
return { captured: false, reason: 'RESPONSE_LIMIT' };
|
|
41
41
|
}
|
|
42
|
-
async function nodeCenter(page, uuid) {
|
|
42
|
+
export async function nodeCenter(page, uuid) {
|
|
43
43
|
const result = await page.evaluate(inspectCocos, { action: 'getNodeBounds', uuid });
|
|
44
44
|
if (!result.available || !result.visible)
|
|
45
45
|
return { reason: result.reason ?? 'OUTSIDE_VIEWPORT' };
|
|
@@ -220,7 +220,8 @@ export function inspectCocosPage() {
|
|
|
220
220
|
}
|
|
221
221
|
export function inspectCocos(request) {
|
|
222
222
|
const root = globalThis;
|
|
223
|
-
|
|
223
|
+
// Browsers always report a protocol; only the Cocos native (jsb) location shim has an empty one, and its endpoint was already checked to be loopback.
|
|
224
|
+
if (root.location && root.location.protocol !== '') {
|
|
224
225
|
const target = new URL(root.location.href);
|
|
225
226
|
const host = target.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
226
227
|
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
@@ -809,11 +810,9 @@ export function inspectCocos(request) {
|
|
|
809
810
|
}
|
|
810
811
|
}
|
|
811
812
|
if (selected === node) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
displayFields[key] = typeof value === 'string' ? value.slice(0, 500) : value;
|
|
816
|
-
}
|
|
813
|
+
// Native (JSB) nodes keep these in C++ behind accessors; summary reads the same public getters the scene tree reports.
|
|
814
|
+
const { name, active, activeInHierarchy } = summary(node);
|
|
815
|
+
Object.assign(displayFields, { name, active, activeInHierarchy });
|
|
817
816
|
}
|
|
818
817
|
if (typeof cc.Sprite === 'function' && selected instanceof cc.Sprite) {
|
|
819
818
|
const frame = dataProperty(selected, '_spriteFrame');
|
|
@@ -1161,7 +1160,21 @@ export function inspectCocos(request) {
|
|
|
1161
1160
|
}
|
|
1162
1161
|
// Asset helpers read backing fields only; refCount, width, and texture getters stay untouched.
|
|
1163
1162
|
const assetName = (asset) => String(dataProperty(asset, '_name') ?? '').slice(0, 500);
|
|
1164
|
-
|
|
1163
|
+
// Native (JSB) assets keep _uuid behind a C++ accessor, which data reads skip; the asset cache is keyed by uuid, so look it up there.
|
|
1164
|
+
let cachedUuids;
|
|
1165
|
+
const assetUuid = (asset) => {
|
|
1166
|
+
const own = dataProperty(asset, '_uuid');
|
|
1167
|
+
if (typeof own === 'string' && own)
|
|
1168
|
+
return own;
|
|
1169
|
+
if (!cachedUuids) {
|
|
1170
|
+
cachedUuids = new Map();
|
|
1171
|
+
const map = dataProperty(cc.assetManager?.assets ?? {}, '_map');
|
|
1172
|
+
for (const [key, value] of Object.entries((map && typeof map === 'object' ? map : {})))
|
|
1173
|
+
if (value && typeof value === 'object')
|
|
1174
|
+
cachedUuids.set(value, key);
|
|
1175
|
+
}
|
|
1176
|
+
return cachedUuids.get(asset) ?? '';
|
|
1177
|
+
};
|
|
1165
1178
|
const gfxBytes = (texture) => {
|
|
1166
1179
|
const gfx = texture && typeof texture === 'object' ? dataProperty(texture, '_gfxTexture') : undefined;
|
|
1167
1180
|
const size = gfx && typeof gfx === 'object' ? dataProperty(gfx, '_size') : undefined;
|
|
@@ -1351,23 +1364,27 @@ export function inspectCocos(request) {
|
|
|
1351
1364
|
return;
|
|
1352
1365
|
// cachedCameraPriority is refreshed only on real dispatch, so ask the batcher, as UITransform.cameraPriority does.
|
|
1353
1366
|
let camera = 0;
|
|
1367
|
+
// Touches carry their window's id and hitTest skips other windows' cameras: 0 on the web, the native window's id (1) on device.
|
|
1368
|
+
let windowId = 0;
|
|
1354
1369
|
try {
|
|
1355
|
-
|
|
1370
|
+
const found = cc.director?.root?.batcher2D?.getFirstRenderCamera?.(node);
|
|
1371
|
+
camera = Number(found?.priority ?? 0);
|
|
1372
|
+
windowId = Number(found?.systemWindowId ?? 0);
|
|
1356
1373
|
}
|
|
1357
1374
|
catch {
|
|
1358
1375
|
camera = 0;
|
|
1359
1376
|
}
|
|
1360
|
-
candidates.push({ node, order, camera });
|
|
1377
|
+
candidates.push({ node, order, camera, windowId });
|
|
1361
1378
|
});
|
|
1362
|
-
const sorted = candidates.sort((a, b) => b.camera - a.camera || b.order - a.order)
|
|
1379
|
+
const sorted = candidates.sort((a, b) => b.camera - a.camera || b.order - a.order);
|
|
1363
1380
|
const hits = [];
|
|
1364
1381
|
let claimer;
|
|
1365
|
-
for (const node of sorted) {
|
|
1382
|
+
for (const { node, windowId } of sorted) {
|
|
1366
1383
|
const transform = components(node).find(component => componentName(component) === 'UITransform');
|
|
1367
1384
|
let hit = false;
|
|
1368
1385
|
// hitTest is the engine's own public query: matrix math and Mask checks, no state change.
|
|
1369
1386
|
try {
|
|
1370
|
-
hit = !!screenPoint && typeof transform?.hitTest === 'function' && transform.hitTest(screenPoint,
|
|
1387
|
+
hit = !!screenPoint && typeof transform?.hitTest === 'function' && transform.hitTest(screenPoint, windowId);
|
|
1371
1388
|
}
|
|
1372
1389
|
catch {
|
|
1373
1390
|
hit = false;
|
|
@@ -1676,10 +1693,13 @@ export function inspectCocos(request) {
|
|
|
1676
1693
|
// Unused first, then by GPU bytes and refCount: the likeliest leaks lead.
|
|
1677
1694
|
items.sort((a, b) => Number(b.status === 'unused') - Number(a.status === 'unused') || (b.bytes ?? 0) - (a.bytes ?? 0) || b.refCount - a.refCount);
|
|
1678
1695
|
const memory = dataProperty(dataProperty(dataProperty(cc.director, '_root') ?? {}, '_device') ?? {}, '_memoryStatus');
|
|
1696
|
+
// Native (JSB) GFX textures live in C++ and expose no byte size to JS; report that instead of zeros that read as "no textures".
|
|
1697
|
+
const textureCount = Object.entries(byType).filter(([type]) => /Texture/.test(type)).reduce((sum, [, bucket]) => sum + bucket.count, 0);
|
|
1698
|
+
const bytesUnavailable = totalBytes === 0 && textureCount > 0;
|
|
1679
1699
|
return {
|
|
1680
1700
|
version,
|
|
1681
1701
|
assetCount: Math.min(total, 50_000),
|
|
1682
|
-
textureBytes: totalBytes,
|
|
1702
|
+
...(bytesUnavailable ? { textureBytes: null, unavailableMetrics: { textureBytes: 'UNSUPPORTED_PUBLIC_API' } } : { textureBytes: totalBytes }),
|
|
1683
1703
|
...(memory ? { gpuMemory: { textureBytes: Number(dataProperty(memory, 'textureSize') ?? 0), bufferBytes: Number(dataProperty(memory, 'bufferSize') ?? 0) } } : {}),
|
|
1684
1704
|
byType,
|
|
1685
1705
|
assets: items.slice(0, limit),
|
package/dist/src/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { BrowserConnection } from './browser.js';
|
|
4
|
+
import { NativeConnection } from './native.js';
|
|
4
5
|
import { createServer } from './server.js';
|
|
5
6
|
function optionsFromArgs(args) {
|
|
6
7
|
let endpoint = process.env.COCOS_CDP_ENDPOINT ?? 'http://127.0.0.1:9222';
|
|
8
|
+
let nativeEndpoint;
|
|
7
9
|
let allowRuntimeMutation = false;
|
|
8
10
|
let allowBrowserData = false;
|
|
9
11
|
let allowMethodCall = false;
|
|
@@ -28,18 +30,24 @@ function optionsFromArgs(args) {
|
|
|
28
30
|
endpoint = value;
|
|
29
31
|
continue;
|
|
30
32
|
}
|
|
33
|
+
if (arg === '--native-endpoint') {
|
|
34
|
+
nativeEndpoint = args[++index];
|
|
35
|
+
if (!nativeEndpoint)
|
|
36
|
+
throw new Error('--native-endpoint requires a value');
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
31
39
|
if (arg.startsWith('-'))
|
|
32
40
|
throw new Error(`Unknown argument: ${arg}`);
|
|
33
41
|
}
|
|
34
|
-
return { endpoint, allowRuntimeMutation, allowBrowserData, allowMethodCall };
|
|
42
|
+
return { endpoint, nativeEndpoint, allowRuntimeMutation, allowBrowserData, allowMethodCall };
|
|
35
43
|
}
|
|
36
44
|
async function main() {
|
|
37
45
|
const [command, ...rest] = process.argv.slice(2);
|
|
38
46
|
if (command === 'launch' || command === 'doctor')
|
|
39
47
|
return (await import('./cli.js')).commands[command](rest);
|
|
40
48
|
const options = optionsFromArgs(process.argv.slice(2));
|
|
41
|
-
const browser = new BrowserConnection(options.endpoint);
|
|
42
|
-
const server = createServer(browser, { allowRuntimeMutation: options.allowRuntimeMutation, allowBrowserData: options.allowBrowserData, allowMethodCall: options.allowMethodCall });
|
|
49
|
+
const browser = options.nativeEndpoint ? new NativeConnection(options.nativeEndpoint) : new BrowserConnection(options.endpoint);
|
|
50
|
+
const server = createServer(browser, { allowRuntimeMutation: options.allowRuntimeMutation, allowBrowserData: options.allowBrowserData, allowMethodCall: options.allowMethodCall, native: !!options.nativeEndpoint });
|
|
43
51
|
let closing = false;
|
|
44
52
|
const close = async () => {
|
|
45
53
|
if (closing)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
type Evaluate = (webSocketUrl: string, expression: string, timeout: number) => Promise<unknown>;
|
|
3
|
+
export declare class NativeConnection {
|
|
4
|
+
#private;
|
|
5
|
+
private readonly timeout;
|
|
6
|
+
private readonly evaluateExpression;
|
|
7
|
+
constructor(endpoint: string, timeout?: number, evaluateExpression?: Evaluate);
|
|
8
|
+
pages(): Promise<Page[]>;
|
|
9
|
+
page(_pageUrl?: string): Promise<Page>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare function nativeClick(page: Page, uuid: string): Promise<unknown>;
|
|
13
|
+
export declare function nativeDrag(page: Page, uuid: string, dx: number, dy: number, durationMs?: number): Promise<unknown>;
|
|
14
|
+
/** Parses `logcat -v epoch` lines of the Cocos tag; console.log and console.debug share one level there and map to log. */
|
|
15
|
+
export declare function parseCocosLogcat(output: string): Array<{
|
|
16
|
+
type: string;
|
|
17
|
+
text: string;
|
|
18
|
+
time: number;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function nativeConsoleMessages(page: Page, options: {
|
|
21
|
+
types?: string[] | undefined;
|
|
22
|
+
textContains?: string | undefined;
|
|
23
|
+
limit?: number | undefined;
|
|
24
|
+
}): Promise<unknown>;
|
|
25
|
+
export declare function nativeTypeText(page: Page, uuid: string, text: string, submit: boolean): Promise<unknown>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { nodeCenter, runBridge } from './bridge.js';
|
|
4
|
+
import { InspectorError, validateLocalUrl } from './browser.js';
|
|
5
|
+
import { redactText } from './browser-data.js';
|
|
6
|
+
// Cocos native debug builds (V8) run a Node-style inspector: Runtime and Debugger domains only, no pages, one session at a time.
|
|
7
|
+
// ponytail: one WebSocket per call so Chrome DevTools can attach between calls; keep a socket open if per-call latency matters.
|
|
8
|
+
export class NativeConnection {
|
|
9
|
+
timeout;
|
|
10
|
+
evaluateExpression;
|
|
11
|
+
#http;
|
|
12
|
+
#page;
|
|
13
|
+
constructor(endpoint, timeout = 60_000, evaluateExpression = evaluateOverWebSocket) {
|
|
14
|
+
this.timeout = timeout;
|
|
15
|
+
this.evaluateExpression = evaluateExpression;
|
|
16
|
+
const url = validateLocalUrl(endpoint);
|
|
17
|
+
url.protocol = url.protocol === 'wss:' || url.protocol === 'https:' ? 'https:' : 'http:';
|
|
18
|
+
url.pathname = '/';
|
|
19
|
+
this.#http = url;
|
|
20
|
+
// Only the evaluate/url subset of Page that runBridge and cocos_list_pages use.
|
|
21
|
+
this.#page = { url: () => this.#http.href, evaluate: (fn, arg) => this.#evaluate(fn, arg) };
|
|
22
|
+
}
|
|
23
|
+
async pages() {
|
|
24
|
+
return [this.#page];
|
|
25
|
+
}
|
|
26
|
+
async page(_pageUrl) {
|
|
27
|
+
return this.#page;
|
|
28
|
+
}
|
|
29
|
+
async close() { }
|
|
30
|
+
async #evaluate(fn, arg) {
|
|
31
|
+
let targets;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(new URL('/json/list', this.#http), { signal: AbortSignal.timeout(10_000) });
|
|
34
|
+
targets = await response.json();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new InspectorError('CDP_UNAVAILABLE', `Unable to reach the native inspector at ${this.#http.host}: ${error instanceof Error ? error.message : 'unknown error'}; run the debug build, find its port in logcat "Debugger listening", then adb forward tcp:<port> tcp:<port>`);
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(targets) || targets.length === 0)
|
|
40
|
+
throw new InspectorError('NO_LOCAL_PAGE', 'Native inspector lists no target');
|
|
41
|
+
const debuggerUrl = targets.find(target => target.webSocketDebuggerUrl)?.webSocketDebuggerUrl;
|
|
42
|
+
if (!debuggerUrl)
|
|
43
|
+
throw new InspectorError('CDP_UNAVAILABLE', 'Native inspector already has a session; close Chrome DevTools attached to the game');
|
|
44
|
+
// Connect to the validated endpoint host, never to the host the target list names.
|
|
45
|
+
const socketUrl = new URL(new URL(debuggerUrl).pathname, this.#http);
|
|
46
|
+
socketUrl.protocol = this.#http.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
47
|
+
// Functions passed here are self-contained (inspectCocos, inspectCocosPage), so their source runs as is.
|
|
48
|
+
return this.evaluateExpression(socketUrl.href, `(${fn.toString()})(${arg === undefined ? '' : JSON.stringify(arg)})`, this.timeout);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function evaluateOverWebSocket(webSocketUrl, expression, timeout) {
|
|
52
|
+
const socket = new WebSocket(webSocketUrl);
|
|
53
|
+
let timer;
|
|
54
|
+
try {
|
|
55
|
+
return await new Promise((resolve, reject) => {
|
|
56
|
+
timer = setTimeout(() => reject(new InspectorError('CDP_UNAVAILABLE', 'Native inspector did not answer in time')), timeout);
|
|
57
|
+
socket.onerror = () => reject(new InspectorError('CDP_UNAVAILABLE', 'Native inspector connection failed'));
|
|
58
|
+
socket.onclose = () => reject(new InspectorError('CDP_UNAVAILABLE', 'Native inspector closed the connection'));
|
|
59
|
+
socket.onopen = () => socket.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { expression, returnByValue: true, awaitPromise: true } }));
|
|
60
|
+
socket.onmessage = event => {
|
|
61
|
+
const message = JSON.parse(String(event.data));
|
|
62
|
+
if (message.id !== 1)
|
|
63
|
+
return;
|
|
64
|
+
if (message.error)
|
|
65
|
+
return reject(new InspectorError('CDP_UNAVAILABLE', `Native inspector error: ${String(message.error.message).slice(0, 500)}`));
|
|
66
|
+
const exception = message.result?.exceptionDetails;
|
|
67
|
+
// Match Playwright's in-page error text, so the server maps bridge errors to the same codes.
|
|
68
|
+
if (exception)
|
|
69
|
+
return reject(new Error(String(exception.exception?.description ?? exception.text ?? 'Evaluation failed').split('\n')[0].replace(/^Error: /, '')));
|
|
70
|
+
resolve(message.result?.result?.value);
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
socket.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Real input and logs go through adb on the device the inspector is forwarded from. adb picks the device itself;
|
|
80
|
+
// with several attached, set ANDROID_SERIAL. Arguments are numbers or a validated package name, never free text.
|
|
81
|
+
const run = promisify(execFile);
|
|
82
|
+
async function adb(args) {
|
|
83
|
+
try {
|
|
84
|
+
return (await run('adb', args, { timeout: 15_000, maxBuffer: 16 * 1024 * 1024 })).stdout;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message.split('\n')[0] : 'unknown error';
|
|
88
|
+
throw new InspectorError('CDP_UNAVAILABLE', `adb ${args[0]} failed: ${message.slice(0, 300)}; check adb devices, and set ANDROID_SERIAL when several are attached`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ponytail: viewport coordinates equal screen pixels only for a full-screen game (the Cocos default); add the window offset for windowed apps.
|
|
92
|
+
const pixel = (value) => String(Math.round(value));
|
|
93
|
+
const settle = () => new Promise(resolve => setTimeout(resolve, 300));
|
|
94
|
+
export async function nativeClick(page, uuid) {
|
|
95
|
+
const point = await nodeCenter(page, uuid);
|
|
96
|
+
if ('reason' in point)
|
|
97
|
+
return { clicked: false, reason: point.reason };
|
|
98
|
+
await adb(['shell', 'input', 'tap', pixel(point.x), pixel(point.y)]);
|
|
99
|
+
// adb returns once the event is injected; the game handles it on its next frames.
|
|
100
|
+
await settle();
|
|
101
|
+
return { clicked: true, target: { nodeUuid: uuid }, point, input: 'adb', runtimeOnly: true };
|
|
102
|
+
}
|
|
103
|
+
export async function nativeDrag(page, uuid, dx, dy, durationMs = 300) {
|
|
104
|
+
const from = await nodeCenter(page, uuid);
|
|
105
|
+
if ('reason' in from)
|
|
106
|
+
return { dragged: false, reason: from.reason };
|
|
107
|
+
const to = { x: from.x + dx, y: from.y + dy };
|
|
108
|
+
// input swipe interpolates its own moves, so steps does not apply.
|
|
109
|
+
await adb(['shell', 'input', 'swipe', pixel(from.x), pixel(from.y), pixel(to.x), pixel(to.y), String(Math.max(durationMs, 1))]);
|
|
110
|
+
await settle();
|
|
111
|
+
return { dragged: true, target: { nodeUuid: uuid }, from, to, input: 'adb', runtimeOnly: true };
|
|
112
|
+
}
|
|
113
|
+
// The app's own process, from its files directory, so logs of other Cocos apps on the device never mix in.
|
|
114
|
+
async function appProcess(page) {
|
|
115
|
+
const path = String(await page.evaluate(() => globalThis.jsb?.fileUtils?.getWritablePath?.() ?? ''));
|
|
116
|
+
const name = /^\/data\/(?:user\/\d+|data)\/([A-Za-z][\w.]{0,200})\//.exec(path)?.[1];
|
|
117
|
+
if (!name)
|
|
118
|
+
throw new InspectorError('CDP_UNAVAILABLE', 'Unable to identify the app package from the native runtime');
|
|
119
|
+
const pid = (await adb(['shell', 'pidof', name]).catch(() => '')).trim().split(/\s+/)[0] ?? '';
|
|
120
|
+
if (!/^\d+$/.test(pid))
|
|
121
|
+
throw new InspectorError('CDP_UNAVAILABLE', `App ${name} is not running`);
|
|
122
|
+
return pid;
|
|
123
|
+
}
|
|
124
|
+
const levels = { V: 'debug', D: 'log', I: 'info', W: 'warning', E: 'error', F: 'error' };
|
|
125
|
+
/** Parses `logcat -v epoch` lines of the Cocos tag; console.log and console.debug share one level there and map to log. */
|
|
126
|
+
export function parseCocosLogcat(output) {
|
|
127
|
+
const messages = [];
|
|
128
|
+
for (const line of output.split(/\r?\n/)) {
|
|
129
|
+
const match = /^\s*(\d+\.\d+)\s+\d+\s+\d+\s+([VDIWEF])\s+Cocos\s*:\s?(?:\d\d:\d\d:\d\d \[\w+\]: )?(.*)$/.exec(line);
|
|
130
|
+
if (match)
|
|
131
|
+
messages.push({ type: levels[match[2]], text: match[3], time: Number(match[1]) });
|
|
132
|
+
}
|
|
133
|
+
return messages;
|
|
134
|
+
}
|
|
135
|
+
export async function nativeConsoleMessages(page, options) {
|
|
136
|
+
const limit = options.limit ?? 50;
|
|
137
|
+
const output = await adb(['logcat', '-d', '-v', 'epoch', `--pid=${await appProcess(page)}`, '-s', 'Cocos:V']);
|
|
138
|
+
const items = parseCocosLogcat(output)
|
|
139
|
+
.filter(item => !options.types?.length || options.types.includes(item.type))
|
|
140
|
+
.filter(item => !options.textContains || item.text.includes(options.textContains));
|
|
141
|
+
const selected = items.slice(-limit).map(item => ({ type: item.type, text: redactText(item.text), time: new Date(item.time * 1_000).toISOString() }));
|
|
142
|
+
return {
|
|
143
|
+
messages: selected,
|
|
144
|
+
matched: items.length,
|
|
145
|
+
truncated: items.length > limit,
|
|
146
|
+
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).',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Tapping an EditBox on Android opens CocosEditBoxActivity, a native EditText whose every change fires text-changed in the game.
|
|
150
|
+
export async function nativeTypeText(page, uuid, text, submit) {
|
|
151
|
+
// input text only types printable ASCII and turns "%s" into a space, so refuse what it would silently mangle.
|
|
152
|
+
if (!/^[\x20-\x7e]*$/.test(text) || text.includes('%s'))
|
|
153
|
+
throw new Error('Invalid mutation: native typing supports printable ASCII without "%s"');
|
|
154
|
+
const { components } = await runBridge(page, { action: 'getComponents', uuid });
|
|
155
|
+
if (!components?.some(component => component.type === 'EditBox'))
|
|
156
|
+
throw new Error('Component not found');
|
|
157
|
+
const clicked = await nativeClick(page, uuid);
|
|
158
|
+
if (!clicked.clicked)
|
|
159
|
+
return { typed: false, reason: clicked.reason };
|
|
160
|
+
let focused = false;
|
|
161
|
+
for (let attempt = 0; attempt < 10 && !focused; attempt++) {
|
|
162
|
+
focused = (await adb(['shell', 'dumpsys window | grep mCurrentFocus'])).includes('CocosEditBoxActivity');
|
|
163
|
+
if (!focused)
|
|
164
|
+
await settle();
|
|
165
|
+
}
|
|
166
|
+
if (!focused)
|
|
167
|
+
return { typed: false, reason: 'NOT_FOCUSED' };
|
|
168
|
+
// End, Shift+Home, Delete: select the current content and remove it. keycombination needs Android 12+.
|
|
169
|
+
await adb(['shell', 'input', 'keyevent', 'KEYCODE_MOVE_END']);
|
|
170
|
+
await adb(['shell', 'input', 'keycombination', 'KEYCODE_SHIFT_LEFT', 'KEYCODE_MOVE_HOME']);
|
|
171
|
+
await adb(['shell', 'input', 'keyevent', 'KEYCODE_DEL']);
|
|
172
|
+
// adb shell joins its arguments into one device shell command line: single-quote the text so it stays one literal word.
|
|
173
|
+
if (text)
|
|
174
|
+
await adb(['shell', `input text '${text.replace(/ /g, '%s').replace(/'/g, `'\\''`)}'`]);
|
|
175
|
+
// Enter closes a single-line box (editing-did-ended); only the activity's confirm button fires editing-return.
|
|
176
|
+
if (submit)
|
|
177
|
+
await adb(['shell', 'input', 'keyevent', 'KEYCODE_ENTER']);
|
|
178
|
+
await settle();
|
|
179
|
+
const { properties } = await runBridge(page, { action: 'getProperties', uuid, componentType: 'EditBox', maxDepth: 0 });
|
|
180
|
+
const after = typeof properties?.string === 'string' ? { string: properties.string } : { redacted: true };
|
|
181
|
+
// The device keyboard still sees the keys: autocorrect or a Telex layout can rewrite "test" as "tét".
|
|
182
|
+
const matches = typeof properties?.string === 'string' ? properties.string === text : undefined;
|
|
183
|
+
return { typed: true, target: { nodeUuid: uuid }, length: text.length, submitted: submit, after, ...(matches === undefined ? {} : { matches }), ...(matches === false ? { hint: 'The device keyboard changed the text; switch it to a plain English layout without autocorrect' } : {}), input: 'adb', runtimeOnly: true };
|
|
184
|
+
}
|
package/dist/src/server.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { BrowserConnection } from './browser.js';
|
|
3
|
-
|
|
3
|
+
import { type NativeConnection } from './native.js';
|
|
4
|
+
export declare function createServer(browser: BrowserConnection | NativeConnection, options?: {
|
|
4
5
|
allowRuntimeMutation?: boolean;
|
|
5
6
|
allowBrowserData?: boolean;
|
|
6
7
|
allowMethodCall?: boolean;
|
|
8
|
+
native?: boolean;
|
|
7
9
|
}): McpServer;
|
package/dist/src/server.js
CHANGED
|
@@ -2,6 +2,7 @@ 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 { nativeClick, nativeConsoleMessages, nativeDrag, nativeTypeText } from './native.js';
|
|
5
6
|
import { consoleMessages, networkRequest, networkRequests, storage } from './browser-data.js';
|
|
6
7
|
import { captureNode, clickNode, devicePresetNames, dragNode, emulateDevice, inspectCocosPage, networkProfileNames, runBridge, typeText } from './bridge.js';
|
|
7
8
|
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
@@ -60,6 +61,10 @@ function failure(error) {
|
|
|
60
61
|
}
|
|
61
62
|
export function createServer(browser, options = {}) {
|
|
62
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;
|
|
63
68
|
const execute = async (request, selectedPage) => {
|
|
64
69
|
try {
|
|
65
70
|
return response(await runBridge(await browser.page(selectedPage), request));
|
|
@@ -131,11 +136,12 @@ export function createServer(browser, options = {}) {
|
|
|
131
136
|
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
132
137
|
annotations: readOnly,
|
|
133
138
|
}, input => executeNode(input, uuid => ({ action: 'getNodeBounds', uuid })));
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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));
|
|
139
145
|
server.registerTool('cocos_get_components', {
|
|
140
146
|
description: 'List bounded component summaries for one Cocos node UUID.',
|
|
141
147
|
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
@@ -188,7 +194,7 @@ export function createServer(browser, options = {}) {
|
|
|
188
194
|
inputSchema: z.object({ pageUrl, ...target }).strict().refine(oneTarget, oneTargetMessage),
|
|
189
195
|
// Game click handlers run arbitrary code: a login button reaches real servers, a buy button spends currency.
|
|
190
196
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
191
|
-
}, input => withNode(input, clickNode));
|
|
197
|
+
}, input => withNode(input, web ? clickNode : nativeClick));
|
|
192
198
|
// Like click: game drag and input handlers run arbitrary code.
|
|
193
199
|
const realInput = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
|
|
194
200
|
server.registerTool('cocos_drag_node', {
|
|
@@ -196,17 +202,18 @@ export function createServer(browser, options = {}) {
|
|
|
196
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)
|
|
197
203
|
.refine(value => value.dx !== 0 || value.dy !== 0, 'Provide a non-zero dx or dy'),
|
|
198
204
|
annotations: realInput,
|
|
199
|
-
}, input => withNode(input, (page, uuid) => dragNode(page, uuid, input.dx, input.dy, input.steps, input.durationMs)));
|
|
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)));
|
|
200
206
|
server.registerTool('cocos_type_text', {
|
|
201
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.',
|
|
202
208
|
inputSchema: z.object({ pageUrl, ...target, text: z.string().max(2_000), submit: z.boolean().optional() }).strict().refine(oneTarget, oneTargetMessage),
|
|
203
209
|
annotations: realInput,
|
|
204
|
-
}, input => withNode(input, (page, uuid) => typeText(page, uuid, input.text, input.submit ?? false)));
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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));
|
|
210
217
|
for (const action of ['pause', 'resume']) {
|
|
211
218
|
server.registerTool(`cocos_${action}`, {
|
|
212
219
|
description: `${action === 'pause' ? 'Pause' : 'Resume'} the Cocos director through its public API.`,
|
|
@@ -214,11 +221,12 @@ export function createServer(browser, options = {}) {
|
|
|
214
221
|
annotations: runtimeMutation,
|
|
215
222
|
}, input => execute({ action }, input.pageUrl));
|
|
216
223
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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));
|
|
222
230
|
server.registerTool('cocos_set_time_scale', {
|
|
223
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.',
|
|
224
232
|
inputSchema: z.object({ pageUrl, scale: z.number().gt(0).max(100).optional() }).strict(),
|
|
@@ -229,34 +237,35 @@ export function createServer(browser, options = {}) {
|
|
|
229
237
|
inputSchema: z.object({ pageUrl, visible: z.boolean() }).strict(),
|
|
230
238
|
annotations: runtimeMutation,
|
|
231
239
|
}, input => execute({ action: 'showStats', visible: input.visible }, input.pageUrl));
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
});
|
|
260
269
|
}
|
|
261
270
|
server.registerTool('cocos_explain_click', {
|
|
262
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.',
|
|
@@ -306,33 +315,38 @@ export function createServer(browser, options = {}) {
|
|
|
306
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.',
|
|
307
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(),
|
|
308
317
|
annotations: browserData,
|
|
309
|
-
}, input => run(async () => consoleMessages(await browser.page(input.pageUrl), input)));
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
337
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 })));
|
|
337
351
|
return server;
|
|
338
352
|
}
|