cocos-web-inspector-mcp 0.1.5 → 0.1.6

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 CHANGED
@@ -108,6 +108,7 @@ All tool input objects are strict. Unknown fields are rejected. Inspection tools
108
108
  | `cocos_set_node_active` | Set one node's active state. Registered only with `--allow-runtime-mutation`; returns before/after state. | `pageUrl?`; `uuid`; `active` boolean |
109
109
  | `cocos_set_transform` | Update supplied position, rotation, and/or scale fields for one node. | `pageUrl?`; `uuid`; `position?`, `rotation?`, `scale?` finite vectors |
110
110
  | `cocos_set_property` | Update one bounded public component data property. | `pageUrl?`; node `uuid`; `componentUuid`; `key`; primitive/vector/size/color `value` matching current shape |
111
+ | `cocos_click_node` | Dispatch a real mouse click at the visible center of one UI node so Button/touch handlers run. Registered only with `--allow-runtime-mutation`. | `pageUrl?`; `uuid` |
111
112
  | `cocos_pause` | Pause the Cocos director when its public API supports it. | `pageUrl?` |
112
113
  | `cocos_resume` | Resume the Cocos director when its public API supports it. | `pageUrl?` |
113
114
  | `cocos_scene_tree` | Return a bounded scene tree with node and component summaries. | `pageUrl?`; `maxDepth?` integer `0..20`, default `6`; `maxNodes?` integer `1..5000`, default `500` |
@@ -116,11 +117,12 @@ All tool input objects are strict. Unknown fields are rejected. Inspection tools
116
117
  | `cocos_get_node` | Return one node's path, parent, bounded direct children, and components. | `pageUrl?`; `uuid` |
117
118
  | `cocos_snapshot_subtree` | Return a bounded stateless hierarchy snapshot; clients compare snapshots. | `pageUrl?`; `uuid`; `maxDepth?`; `maxNodes?` |
118
119
  | `cocos_get_node_bounds` | Return bounded canvas/viewport bounds, anchor, world position, and visibility for one UI node. | `pageUrl?`; `uuid` |
119
- | `cocos_capture_node` | Return an in-memory viewport-clipped PNG for one visible UI node; no file is written. | `pageUrl?`; `uuid` |
120
- | `cocos_get_properties` | Serialize public properties for a node or one component selected by type or UUID. | `pageUrl?`; `uuid`; `componentType?` or `componentUuid?`; `maxDepth?` integer `0..6`, default `3` |
120
+ | `cocos_capture_node` | Return an in-memory viewport-clipped image for one visible UI node: PNG, falling back to JPEG when PNG exceeds the response limit; no file is written. | `pageUrl?`; `uuid` |
121
+ | `cocos_get_properties` | Serialize public properties for a node or one component selected by type or UUID. | `pageUrl?`; `uuid`; `componentType?` or `componentUuid?`; `maxDepth?` integer `0..6`, default `3`; `0` returns top-level primitives |
122
+ | `cocos_wait_for_property` | Poll one top-level property until it strictly equals a primitive value or the timeout passes. | `pageUrl?`; `uuid`; `componentType?` or `componentUuid?`; `key`; `equals`; `timeoutMs?` `100..30000`, default `5000`; `intervalMs?` `50..5000`, default `200` |
121
123
  | `cocos_highlight_node` | Draw a temporary pointer-transparent overlay around a UI node. | `pageUrl?`; `uuid`; `durationMs?` integer `100..10000`, default `2000` |
122
124
 
123
- `cocos_runtime_diagnostics` does not enable profiler/statistics systems. FPS, frame time, draw calls, triangles, and generic invalid-reference checks return `UNSUPPORTED_PUBLIC_API` until stable passive public Cocos APIs are verified. `cocos_highlight_node` temporarily mutates the page DOM only. It does not mutate the Cocos node/component graph or game state. `cocos_capture_node` clips only to the visible browser viewport, never falls back to full-page capture, limits captures to 1,024 × 1,024 CSS pixels / 1,048,576 total pixels, returns PNG base64 in-memory, and rejects oversized responses. Mutation results provide `before` values for manual inverse calls, but restoration cannot undo lifecycle callbacks or other runtime side effects. Frame stepping is unsupported pending a verified public Cocos API compatibility matrix.
125
+ `cocos_runtime_diagnostics` does not enable profiler/statistics systems. FPS, frame time, draw calls, triangles, and generic invalid-reference checks return `UNSUPPORTED_PUBLIC_API` until stable passive public Cocos APIs are verified. `cocos_highlight_node` temporarily mutates the page DOM only. It does not mutate the Cocos node/component graph or game state. `cocos_capture_node` clips only to the visible browser viewport, never falls back to full-page capture, limits captures to 1,024 × 1,024 CSS pixels / 1,048,576 total pixels, returns PNG (or JPEG fallback) base64 in-memory, and rejects responses still oversized after JPEG quality steps. Mutation results provide `before` values for manual inverse calls, but restoration cannot undo lifecycle callbacks or other runtime side effects. Frame stepping is unsupported pending a verified public Cocos API compatibility matrix.
124
126
 
125
127
  ### Page selection
126
128
 
@@ -129,6 +131,14 @@ Eligible game pages must use HTTP or HTTPS on a loopback host.
129
131
  - With one eligible page, `pageUrl` may be omitted.
130
132
  - With multiple eligible pages, `pageUrl` is required.
131
133
  - `pageUrl` must exactly match the full URL reported by the browser, including path, query, and fragment.
134
+ - Tabs with identical URLs cannot be told apart; close duplicates before inspecting.
135
+
136
+ Every MCP client pointed at the same CDP endpoint sees every loopback tab in that browser. When several projects run at once, give each project its own Chromium, port, and profile, then configure the server at project scope:
137
+
138
+ ```sh
139
+ chrome --remote-debugging-address=127.0.0.1 --remote-debugging-port=9223 --user-data-dir="$HOME/.cocos-mcp/project-a"
140
+ claude mcp add cocos-web-inspector npx -- -y cocos-web-inspector-mcp@latest --cdp-endpoint http://127.0.0.1:9223
141
+ ```
132
142
 
133
143
  Tool validation, connection, selection, and inspection failures are returned as MCP tool errors. Operational errors include JSON `structuredContent` and text content with stable `code` and `message` fields. Current codes include `CDP_UNAVAILABLE`, `NO_LOCAL_PAGE`, `MULTIPLE_PAGES`, `PAGE_NOT_FOUND`, `COCOS_NOT_FOUND`, `SCENE_NOT_READY`, `NODE_NOT_FOUND`, and `COMPONENT_NOT_FOUND`.
134
144
 
@@ -169,6 +179,8 @@ Inspection still executes fixed bridge code inside the attached page. Treat all
169
179
 
170
180
  Property serialization skips accessors, private-prefixed keys, functions, symbols, cycles, and secret-like key names such as tokens, cookies, passwords, credentials, authorization data, and storage.
171
181
 
182
+ An allowlist of display fields is read directly from their backing data fields, still without invoking getters: `Label.string`, `RichText.string`, `Button.interactable`, `Toggle.isChecked`, and `Sprite.spriteFrame` (name and UUID only).
183
+
172
184
  These controls reduce accidental disclosure; they do not make CDP a complete security boundary. Always:
173
185
 
174
186
  - Use a disposable browser profile.
@@ -190,8 +202,8 @@ The test suite covers URL policy, CDP connection reuse and recovery, scene trave
190
202
 
191
203
  ## Troubleshooting
192
204
 
193
- - `CDP_UNAVAILABLE`: start Chromium with loopback remote debugging; rerun `npm run install:chromium` for development tests.
194
- - `MULTIPLE_PAGES`: call `cocos_list_pages`, then pass the exact reported `pageUrl`.
205
+ - `CDP_UNAVAILABLE`: start Chromium with loopback remote debugging; rerun `npm run install:chromium` for development tests. A `404` usually means Chrome's built-in remote debugging toggle (`chrome://inspect/#remote-debugging`) holds the port; turn it off or use another port, and check listeners with `lsof -nP -iTCP:9222 -sTCP:LISTEN`. When `127.0.0.1` returns 404 or refuses the connection, the server retries `[::1]` on the same port.
206
+ - `MULTIPLE_PAGES`: call `cocos_list_pages`, then pass the exact reported `pageUrl`. If tabs share one URL, close duplicates or use a separate Chromium per project (see Page selection).
195
207
  - `COCOS_NOT_FOUND` or `SCENE_NOT_READY`: wait for the web build to finish loading; use `cocos_runtime_info` after the active scene exists.
196
208
  - Runtime mutation tools missing: restart the server with `--allow-runtime-mutation`; a tool call cannot enable this mode.
197
209
  - Bounds/capture unavailable: select a visible UI node with `UITransform`. Capture is viewport-only, bounded, and never falls back to a full-page screenshot.
@@ -89,6 +89,7 @@ type PropertyValue = boolean | number | string | {
89
89
  };
90
90
  export declare function runBridge(page: Page, request: BridgeRequest): Promise<unknown>;
91
91
  export declare function captureNode(page: Page, uuid: string): Promise<unknown>;
92
+ export declare function clickNode(page: Page, uuid: string): Promise<unknown>;
92
93
  export declare function inspectCocosPage(): unknown;
93
94
  export declare function inspectCocos(request: BridgeRequest): unknown;
94
95
  export {};
@@ -15,11 +15,24 @@ export async function captureNode(page, uuid) {
15
15
  const clip = result.clippedViewport;
16
16
  if (!clip || !Number.isFinite(clip.x) || !Number.isFinite(clip.y) || !Number.isFinite(clip.width) || !Number.isFinite(clip.height) || clip.width <= 0 || clip.height <= 0 || clip.width > 1_024 || clip.height > 1_024 || clip.width * clip.height > 1_048_576)
17
17
  return { captured: false, reason: 'CAPTURE_LIMIT' };
18
- const png = await page.screenshot({ type: 'png', clip });
19
- const data = png.toString('base64');
20
- if (Buffer.byteLength(JSON.stringify({ data }), 'utf8') > MAX_BYTES - 4_096)
21
- return { captured: false, reason: 'RESPONSE_LIMIT' };
22
- return { captured: true, mimeType: 'image/png', data, width: Math.round(clip.width), height: Math.round(clip.height) };
18
+ // ponytail: device-pixel PNG, then CSS-pixel JPEG quality steps; add CDP clip.scale downscaling if captures still hit the limit.
19
+ const attempts = [{ type: 'png', scale: 'device' }, ...[80, 60, 40].map(quality => ({ type: 'jpeg', quality, scale: 'css' }))];
20
+ for (const options of attempts) {
21
+ const data = (await page.screenshot({ ...options, clip })).toString('base64');
22
+ if (Buffer.byteLength(JSON.stringify({ data }), 'utf8') <= MAX_BYTES - 4_096) {
23
+ return { captured: true, mimeType: `image/${options.type}`, data, width: Math.round(clip.width), height: Math.round(clip.height) };
24
+ }
25
+ }
26
+ return { captured: false, reason: 'RESPONSE_LIMIT' };
27
+ }
28
+ export async function clickNode(page, uuid) {
29
+ const result = await page.evaluate(inspectCocos, { action: 'getNodeBounds', uuid });
30
+ if (!result.available || !result.visible)
31
+ return { clicked: false, reason: result.reason ?? 'OUTSIDE_VIEWPORT' };
32
+ const clip = result.clippedViewport;
33
+ const point = { x: clip.x + clip.width / 2, y: clip.y + clip.height / 2 };
34
+ await page.mouse.click(point.x, point.y);
35
+ return { clicked: true, target: { nodeUuid: uuid }, point, runtimeOnly: true };
23
36
  }
24
37
  export function inspectCocosPage() {
25
38
  const root = globalThis;
@@ -158,8 +171,24 @@ export function inspectCocos(request) {
158
171
  const worldY = Math.min(...points.map(point => Number(point.y)));
159
172
  const worldWidth = Math.max(...points.map(point => Number(point.x))) - worldX;
160
173
  const worldHeight = Math.max(...points.map(point => Number(point.y))) - worldY;
174
+ // Project through the owning Canvas camera; the visible-area mapping is wrong once the camera no longer centers on it.
175
+ let camera;
176
+ for (let current = node, depth = 0; current && !camera && depth < 100; current = current.parent, depth++) {
177
+ camera = components(current).find(component => componentName(component) === 'Canvas')?.cameraComponent;
178
+ }
179
+ const screen = typeof camera?.worldToScreen === 'function' && cc.Vec3 && canvas.width > 0 && canvas.height > 0
180
+ ? points.map(point => camera.worldToScreen(new cc.Vec3(Number(point.x), Number(point.y), 0), new cc.Vec3()))
181
+ : undefined;
161
182
  const origin = cc.view?.getVisibleOrigin?.() ?? { x: 0, y: 0 };
162
- const viewport = { x: canvasRect.left + (worldX - Number(origin.x ?? 0)) * canvasRect.width / Number(visible.width), y: canvasRect.top + (Number(origin.y ?? 0) + Number(visible.height) - worldY - worldHeight) * canvasRect.height / Number(visible.height), width: worldWidth * canvasRect.width / Number(visible.width), height: worldHeight * canvasRect.height / Number(visible.height) };
183
+ const viewport = screen?.every(point => Number.isFinite(point.x) && Number.isFinite(point.y))
184
+ ? (() => {
185
+ const xs = screen.map(point => Number(point.x));
186
+ const ys = screen.map(point => Number(point.y));
187
+ const sx = canvasRect.width / canvas.width;
188
+ const sy = canvasRect.height / canvas.height;
189
+ return { x: canvasRect.left + Math.min(...xs) * sx, y: canvasRect.top + (canvas.height - Math.max(...ys)) * sy, width: (Math.max(...xs) - Math.min(...xs)) * sx, height: (Math.max(...ys) - Math.min(...ys)) * sy };
190
+ })()
191
+ : { x: canvasRect.left + (worldX - Number(origin.x ?? 0)) * canvasRect.width / Number(visible.width), y: canvasRect.top + (Number(origin.y ?? 0) + Number(visible.height) - worldY - worldHeight) * canvasRect.height / Number(visible.height), width: worldWidth * canvasRect.width / Number(visible.width), height: worldHeight * canvasRect.height / Number(visible.height) };
163
192
  if (!Object.values(viewport).every(Number.isFinite) || viewport.width <= 0 || viewport.height <= 0)
164
193
  return { available: false, reason: 'INVALID_GEOMETRY' };
165
194
  const clipped = { x: Math.max(0, viewport.x), y: Math.max(0, viewport.y), width: Math.max(0, Math.min(innerWidth, viewport.x + viewport.width) - Math.max(0, viewport.x)), height: Math.max(0, Math.min(innerHeight, viewport.y + viewport.height) - Math.max(0, viewport.y)) };
@@ -372,7 +401,7 @@ export function inspectCocos(request) {
372
401
  }
373
402
  if (reference && uuid && dataProperty(value, 'node'))
374
403
  return { $type: 'Component', uuid: String(uuid), type: componentName(value) };
375
- if (depth >= maxDepth) {
404
+ if (depth > 0 && depth >= maxDepth) {
376
405
  truncate('MAX_DEPTH');
377
406
  return '[MaxDepth]';
378
407
  }
@@ -425,12 +454,39 @@ export function inspectCocos(request) {
425
454
  seen.delete(value);
426
455
  return output;
427
456
  };
457
+ // ponytail: fixed allowlist of display backing fields, read without getters; extend per component when smoke tests need more.
458
+ const displayFields = {};
459
+ const readDisplay = (type, key, backing) => {
460
+ if (typeof cc[type] !== 'function' || !(selected instanceof cc[type]))
461
+ return;
462
+ const value = dataProperty(selected, backing);
463
+ if (typeof value === 'string') {
464
+ if (value.length > 2_000)
465
+ truncate('STRING_LIMIT');
466
+ displayFields[key] = value.slice(0, 2_000);
467
+ }
468
+ else if (value === null || typeof value === 'boolean' || typeof value === 'number')
469
+ displayFields[key] = value;
470
+ };
471
+ readDisplay('Label', 'string', '_string');
472
+ readDisplay('RichText', 'string', '_string');
473
+ readDisplay('Button', 'interactable', '_interactable');
474
+ readDisplay('Toggle', 'isChecked', '_isChecked');
475
+ if (typeof cc.Sprite === 'function' && selected instanceof cc.Sprite) {
476
+ const frame = dataProperty(selected, '_spriteFrame');
477
+ displayFields.spriteFrame = frame && typeof frame === 'object'
478
+ ? { $type: 'SpriteFrame', name: String(dataProperty(frame, '_name') ?? '').slice(0, 500), uuid: String(dataProperty(frame, '_uuid') ?? '') }
479
+ : null;
480
+ }
481
+ const properties = serialize(selected, 0, false);
482
+ Object.assign(properties, displayFields);
483
+ returned += Object.keys(displayFields).length;
428
484
  return {
429
485
  version,
430
486
  node: summary(node),
431
487
  componentType: request.componentType ?? (selected === node ? undefined : componentName(selected)),
432
488
  componentUuid: selected === node ? undefined : String(selected.uuid ?? ''),
433
- properties: serialize(selected, 0, false),
489
+ properties,
434
490
  truncated,
435
491
  truncationReasons: [...truncationReasons],
436
492
  propertyCount,
@@ -85,7 +85,7 @@ export class BrowserConnection {
85
85
  if (selected.length === 0)
86
86
  throw new InspectorError('PAGE_NOT_FOUND', `Local page not found: ${sanitizeUrl(pageUrl)}`);
87
87
  if (selected.length > 1)
88
- throw new InspectorError('MULTIPLE_PAGES', `Multiple localhost pages match pageUrl: ${sanitizeUrl(pageUrl)}`);
88
+ throw new InspectorError('MULTIPLE_PAGES', `Multiple localhost pages match pageUrl: ${sanitizeUrl(pageUrl)}; close duplicate tabs or use a separate Chromium per project`);
89
89
  return selected[0];
90
90
  }
91
91
  if (pages.length === 0)
@@ -113,7 +113,15 @@ export class BrowserConnection {
113
113
  return this.#browser;
114
114
  if (this.#connecting)
115
115
  return this.#connecting;
116
- const connecting = this.connectOverCDP(this.endpoint, { timeout: this.timeout }).then(async (browser) => {
116
+ const options = { timeout: this.timeout };
117
+ const connecting = this.connectOverCDP(this.endpoint, options).catch(error => {
118
+ // A second Chromium on a taken IPv4 port may bind only [::1].
119
+ const url = new URL(this.endpoint);
120
+ if (!['127.0.0.1', 'localhost'].includes(url.hostname) || !/\b404\b|ECONNREFUSED/.test(String(error?.message)))
121
+ throw error;
122
+ url.hostname = '[::1]';
123
+ return this.connectOverCDP(url.toString().replace(/\/$/, ''), options).catch(() => { throw error; });
124
+ }).then(async (browser) => {
117
125
  if (this.#closed) {
118
126
  await browser.close();
119
127
  throw new Error('Browser connection is closed');
@@ -127,7 +135,11 @@ export class BrowserConnection {
127
135
  }).catch(error => {
128
136
  if (error instanceof InspectorError || error instanceof Error && error.message === 'connect failed')
129
137
  throw error;
130
- throw new InspectorError('CDP_UNAVAILABLE', `Unable to connect to Chromium CDP: ${error instanceof Error ? error.message : 'unknown error'}`);
138
+ const message = error instanceof Error ? error.message : 'unknown error';
139
+ const hint = /\b404\b/.test(message)
140
+ ? '; 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
+ : '';
142
+ throw new InspectorError('CDP_UNAVAILABLE', `Unable to connect to Chromium CDP: ${message}${hint}`);
131
143
  });
132
144
  this.#connecting = connecting;
133
145
  try {
@@ -2,7 +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 { captureNode, inspectCocosPage, runBridge } from './bridge.js';
5
+ import { captureNode, clickNode, inspectCocosPage, runBridge } from './bridge.js';
6
6
  const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
7
7
  const pageUrl = z.url().optional();
8
8
  const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
@@ -30,13 +30,16 @@ function response(data) {
30
30
  function failure(error) {
31
31
  if (error instanceof InspectorError)
32
32
  return { ...response({ code: error.code, message: error.message }), isError: true };
33
- const message = error instanceof Error ? error.message : 'Inspector request failed';
33
+ const raw = error instanceof Error ? error.message : 'Inspector request failed';
34
+ // Playwright wraps in-page errors as "page.evaluate: Error: <message>\n<stack>".
35
+ const message = raw.replace(/^page\.evaluate: (?:Error: )?/, '').split('\n')[0];
34
36
  const code = message === 'Cocos Creator 3.x runtime not found' ? 'COCOS_NOT_FOUND'
35
37
  : message === 'Active Cocos scene not found' ? 'SCENE_NOT_READY'
36
38
  : message.startsWith('Node not found') ? 'NODE_NOT_FOUND'
37
39
  : message === 'Component not found' ? 'COMPONENT_NOT_FOUND'
38
40
  : message === 'Ambiguous component type' ? 'AMBIGUOUS_COMPONENT'
39
- : 'CDP_UNAVAILABLE';
41
+ : message.startsWith('Invalid mutation') ? 'INVALID_MUTATION'
42
+ : 'CDP_UNAVAILABLE';
40
43
  return { ...response({ code, message }), isError: true };
41
44
  }
42
45
  export function createServer(browser, options = {}) {
@@ -125,6 +128,31 @@ export function createServer(browser, options = {}) {
125
128
  .refine(value => !(value.componentType && value.componentUuid), 'Provide componentType or componentUuid, not both'),
126
129
  annotations: readOnly,
127
130
  }, input => execute({ action: 'getProperties', uuid: input.uuid, componentType: input.componentType, componentUuid: input.componentUuid, maxDepth: input.maxDepth }, input.pageUrl));
131
+ server.registerTool('cocos_wait_for_property', {
132
+ 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, uuid: z.string().min(1), 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()
134
+ .refine(value => !(value.componentType && value.componentUuid), 'Provide componentType or componentUuid, not both'),
135
+ annotations: readOnly,
136
+ }, async (input) => {
137
+ try {
138
+ const page = await browser.page(input.pageUrl);
139
+ const request = { action: 'getProperties', uuid: input.uuid, componentType: input.componentType, componentUuid: input.componentUuid, maxDepth: 0 };
140
+ const deadline = Date.now() + (input.timeoutMs ?? 5_000);
141
+ let polls = 0;
142
+ let value;
143
+ for (;;) {
144
+ polls++;
145
+ value = (await runBridge(page, request)).properties?.[input.key];
146
+ if (value === input.equals || Date.now() >= deadline)
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 });
151
+ }
152
+ catch (error) {
153
+ return failure(error);
154
+ }
155
+ });
128
156
  if (options.allowRuntimeMutation) {
129
157
  server.registerTool('cocos_set_node_active', {
130
158
  description: 'Set the active state of one Cocos node selected by exact UUID.',
@@ -142,6 +170,18 @@ export function createServer(browser, options = {}) {
142
170
  inputSchema: z.object({ pageUrl, uuid: z.string().min(1), componentUuid: z.string().min(1), key: z.string().min(1).max(200), value: propertyValue }).strict(),
143
171
  annotations: runtimeMutation,
144
172
  }, input => execute({ action: 'setProperty', uuid: input.uuid, componentUuid: input.componentUuid, key: input.key, value: input.value }, input.pageUrl));
173
+ server.registerTool('cocos_click_node', {
174
+ description: 'Dispatch a real mouse click at the visible center of one Cocos UI node, so Button and touch handlers run.',
175
+ inputSchema: z.object({ pageUrl, uuid: z.string().min(1) }).strict(),
176
+ annotations: { ...runtimeMutation, idempotentHint: false },
177
+ }, async (input) => {
178
+ try {
179
+ return response(await clickNode(await browser.page(input.pageUrl), input.uuid));
180
+ }
181
+ catch (error) {
182
+ return failure(error);
183
+ }
184
+ });
145
185
  for (const action of ['pause', 'resume']) {
146
186
  server.registerTool(`cocos_${action}`, {
147
187
  description: `${action === 'pause' ? 'Pause' : 'Resume'} the Cocos director through its public API.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cocos-web-inspector-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Read-only MCP inspector for Cocos Creator 3.x running in local Chromium",
5
5
  "license": "MIT",
6
6
  "repository": {