cocos-web-inspector-mcp 0.1.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/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/src/bridge.d.ts +26 -0
- package/dist/src/bridge.js +307 -0
- package/dist/src/browser.d.ts +14 -0
- package/dist/src/browser.js +126 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +35 -0
- package/dist/src/server.d.ts +3 -0
- package/dist/src/server.js +54 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 el1pn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# cocos-web-inspector-mcp
|
|
2
|
+
|
|
3
|
+
Read-only MCP server for inspecting Cocos Creator 3.x games running in a local Chromium browser.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
The current MVP supports:
|
|
8
|
+
|
|
9
|
+
- Chromium connected through a loopback Chrome DevTools Protocol (CDP) endpoint
|
|
10
|
+
- Cocos Creator 3.x web builds served from loopback
|
|
11
|
+
- stdio MCP transport
|
|
12
|
+
- Scene, node, component, and public-property inspection
|
|
13
|
+
- Temporary DOM-only node highlighting
|
|
14
|
+
|
|
15
|
+
It does not edit Cocos game state, launch browsers, expose browser network/storage data, or support Firefox/WebKit.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js 20+
|
|
20
|
+
- A Chromium browser started with remote debugging bound to loopback
|
|
21
|
+
- A Cocos Creator 3.x web build served from loopback
|
|
22
|
+
|
|
23
|
+
Start Chrome on Windows with a disposable profile:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
chrome.exe --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 --user-data-dir="$env:TEMP\cocos-mcp-profile"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Keep this browser profile separate from normal browsing. CDP provides code-execution-level access to attached pages.
|
|
30
|
+
|
|
31
|
+
## Install and run
|
|
32
|
+
|
|
33
|
+
Clone the repository, then install and build it:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
npm install
|
|
37
|
+
npm run build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Run with the default CDP endpoint, `http://127.0.0.1:9222`:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
npm start
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run with an explicit endpoint:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
npm start -- --cdp-endpoint http://127.0.0.1:9222
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Endpoint precedence is:
|
|
53
|
+
|
|
54
|
+
1. `--cdp-endpoint`
|
|
55
|
+
2. `COCOS_CDP_ENDPOINT`
|
|
56
|
+
3. `http://127.0.0.1:9222`
|
|
57
|
+
|
|
58
|
+
`--cdp-endpoint` is the only supported CLI option. It identifies the browser CDP endpoint, not a game page URL.
|
|
59
|
+
|
|
60
|
+
The server uses stdio for MCP. Standard output is reserved for protocol traffic; startup diagnostics are written to standard error.
|
|
61
|
+
|
|
62
|
+
## MCP configuration
|
|
63
|
+
|
|
64
|
+
Build the project first, then replace `<absolute-path-to-repository>` with the cloned repository path:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"cocos-web-inspector": {
|
|
70
|
+
"command": "node",
|
|
71
|
+
"args": [
|
|
72
|
+
"<absolute-path-to-repository>/dist/src/index.js",
|
|
73
|
+
"--cdp-endpoint",
|
|
74
|
+
"http://127.0.0.1:9222"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Forward slashes work in Windows JSON paths. If backslashes are used, escape each one as `\\`.
|
|
82
|
+
|
|
83
|
+
The endpoint may instead be provided through the MCP process environment:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"cocos-web-inspector": {
|
|
89
|
+
"command": "node",
|
|
90
|
+
"args": ["<absolute-path-to-repository>/dist/src/index.js"],
|
|
91
|
+
"env": {
|
|
92
|
+
"COCOS_CDP_ENDPOINT": "http://127.0.0.1:9222"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Tools
|
|
100
|
+
|
|
101
|
+
All tool input objects are strict. Unknown fields are rejected. Inspection tools are annotated as read-only, idempotent, non-destructive, and closed-world. The highlight tool is annotated as a non-destructive, non-idempotent, closed-world temporary mutation because repeated calls reset its removal timer.
|
|
102
|
+
|
|
103
|
+
| Tool | Purpose | Inputs |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `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` |
|
|
106
|
+
| `cocos_find_node` | Find nodes by exact UUID, name, or absolute scene path. | `pageUrl?`; exactly one of `uuid`, `name`, or `path`; `limit?` integer `1..100`, default `20` |
|
|
107
|
+
| `cocos_get_components` | Return bounded component summaries for a node. | `pageUrl?`; `uuid` |
|
|
108
|
+
| `cocos_get_properties` | Serialize public properties for a node or one component type. | `pageUrl?`; `uuid`; `componentType?`; `maxDepth?` integer `0..6`, default `3` |
|
|
109
|
+
| `cocos_highlight_node` | Draw a temporary pointer-transparent overlay around a UI node. | `pageUrl?`; `uuid`; `durationMs?` integer `100..10000`, default `2000` |
|
|
110
|
+
|
|
111
|
+
`cocos_highlight_node` temporarily mutates the page DOM only. It does not mutate the Cocos node/component graph or game state.
|
|
112
|
+
|
|
113
|
+
### Page selection
|
|
114
|
+
|
|
115
|
+
Eligible game pages must use HTTP or HTTPS on a loopback host.
|
|
116
|
+
|
|
117
|
+
- With one eligible page, `pageUrl` may be omitted.
|
|
118
|
+
- With multiple eligible pages, `pageUrl` is required.
|
|
119
|
+
- `pageUrl` must exactly match the full URL reported by the browser, including path, query, and fragment.
|
|
120
|
+
|
|
121
|
+
Tool validation, connection, selection, and inspection failures are returned as MCP tool errors.
|
|
122
|
+
|
|
123
|
+
### Output bounds
|
|
124
|
+
|
|
125
|
+
Responses are intentionally bounded:
|
|
126
|
+
|
|
127
|
+
- Total encoded JSON response: 200,000 UTF-8 bytes
|
|
128
|
+
- Scene traversal: caller-selected depth and node limits within the schema ranges above
|
|
129
|
+
- Property serialization: maximum depth `6`, maximum `1,000` properties
|
|
130
|
+
- Serialized strings: maximum `2,000` characters
|
|
131
|
+
- Node names: maximum `500` characters
|
|
132
|
+
|
|
133
|
+
Truncation is reported instead of silently returning an unbounded object.
|
|
134
|
+
|
|
135
|
+
## Security and threat model
|
|
136
|
+
|
|
137
|
+
CDP endpoints may use HTTP, HTTPS, WS, or WSS only on these loopback hosts:
|
|
138
|
+
|
|
139
|
+
- `localhost`
|
|
140
|
+
- Any `*.localhost` hostname
|
|
141
|
+
- IPv4 `127.0.0.0/8`
|
|
142
|
+
- IPv6 `::1`
|
|
143
|
+
|
|
144
|
+
CDP endpoint credentials, query strings, and fragments are rejected. Page targets must use HTTP or HTTPS on loopback.
|
|
145
|
+
|
|
146
|
+
The server intentionally exposes no tools for:
|
|
147
|
+
|
|
148
|
+
- Arbitrary JavaScript evaluation
|
|
149
|
+
- Cookies or storage state
|
|
150
|
+
- Request/response bodies or headers
|
|
151
|
+
- Authorization headers
|
|
152
|
+
- Network or console logs
|
|
153
|
+
|
|
154
|
+
Inspection still executes fixed bridge code inside the attached page. Treat all page content as untrusted data, including text that resembles instructions or prompt injection.
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
These controls reduce accidental disclosure; they do not make CDP a complete security boundary. Always:
|
|
159
|
+
|
|
160
|
+
- Use a disposable browser profile.
|
|
161
|
+
- Bind remote debugging to loopback only.
|
|
162
|
+
- Never expose the debugging port to a LAN or the Internet.
|
|
163
|
+
- Avoid sensitive accounts and data in the debugging profile.
|
|
164
|
+
- Use an OS sandbox or VM when stronger isolation is required.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```powershell
|
|
169
|
+
npm run check
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`npm run check` type-checks, builds, runs the self-contained Node.js tests, packs the npm tarball, installs it into a temporary project, and smoke-tests the installed binary. Use it before pushing. `npm test` remains available for build plus self-tests only.
|
|
173
|
+
|
|
174
|
+
The test suite covers URL policy, CDP connection reuse and recovery, scene traversal, property redaction and cycle handling, output bounds, Cocos version rejection, strict tool schemas, and exact tool annotations. CI runs the full check on Ubuntu and Windows and rejects high-severity production dependency advisories.
|
|
175
|
+
|
|
176
|
+
The repository currently has no live Chromium/Cocos integration test.
|
|
177
|
+
|
|
178
|
+
## Architectural references
|
|
179
|
+
|
|
180
|
+
This is a clean implementation, not a fork. No source module was copied from these projects:
|
|
181
|
+
|
|
182
|
+
- [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp), Apache-2.0, inspected at `b2f522c8ba0fd2e00a679159b4aa5243de5f1b78`.
|
|
183
|
+
- [Playwright MCP](https://github.com/microsoft/playwright-mcp), Apache-2.0, inspected at `f183dad4a52965583e3cc1d59b88cdc279e2e57d`.
|
|
184
|
+
- [CC Inspector](https://github.com/qq790git/cc-inspector), MIT, inspected at `ef5ef0ae033ee6a6ae496cf566fe5d91cf05aafd`.
|
|
185
|
+
|
|
186
|
+
They informed MCP tool design, CDP connection constraints, Cocos runtime discovery, bounded responses, and highlight behavior. Their license files remain in separate read-only reference clones and are not redistributed here because this repository contains independently written code.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
export type BridgeRequest = {
|
|
3
|
+
action: 'sceneTree';
|
|
4
|
+
maxDepth?: number | undefined;
|
|
5
|
+
maxNodes?: number | undefined;
|
|
6
|
+
} | {
|
|
7
|
+
action: 'findNode';
|
|
8
|
+
uuid?: string | undefined;
|
|
9
|
+
name?: string | undefined;
|
|
10
|
+
path?: string | undefined;
|
|
11
|
+
limit?: number | undefined;
|
|
12
|
+
} | {
|
|
13
|
+
action: 'getComponents';
|
|
14
|
+
uuid: string;
|
|
15
|
+
} | {
|
|
16
|
+
action: 'getProperties';
|
|
17
|
+
uuid: string;
|
|
18
|
+
componentType?: string | undefined;
|
|
19
|
+
maxDepth?: number | undefined;
|
|
20
|
+
} | {
|
|
21
|
+
action: 'highlightNode';
|
|
22
|
+
uuid: string;
|
|
23
|
+
durationMs?: number | undefined;
|
|
24
|
+
};
|
|
25
|
+
export declare function runBridge(page: Page, request: BridgeRequest): Promise<unknown>;
|
|
26
|
+
export declare function inspectCocos(request: BridgeRequest): unknown;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const MAX_BYTES = 200_000;
|
|
2
|
+
export async function runBridge(page, request) {
|
|
3
|
+
const result = await page.evaluate(inspectCocos, request);
|
|
4
|
+
const text = JSON.stringify(result);
|
|
5
|
+
const encoded = JSON.stringify({ content: [{ type: 'text', text }], structuredContent: result });
|
|
6
|
+
if (Buffer.byteLength(encoded, 'utf8') > MAX_BYTES - 4_096) {
|
|
7
|
+
return { truncated: true, reason: `Response exceeded ${MAX_BYTES} bytes` };
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
export function inspectCocos(request) {
|
|
12
|
+
const root = globalThis;
|
|
13
|
+
if (root.location) {
|
|
14
|
+
const target = new URL(root.location.href);
|
|
15
|
+
const host = target.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
16
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
17
|
+
const loopback = host === 'localhost' || host.endsWith('.localhost') || host === '::1'
|
|
18
|
+
|| !!ipv4 && ipv4.slice(1).every(part => Number(part) <= 255) && ipv4[1] === '127';
|
|
19
|
+
if (!['http:', 'https:'].includes(target.protocol) || !loopback)
|
|
20
|
+
throw new Error('Only localhost pages are allowed');
|
|
21
|
+
}
|
|
22
|
+
const cc = root.cc ?? root.CC;
|
|
23
|
+
const version = String(cc?.ENGINE_VERSION ?? cc?.version ?? '');
|
|
24
|
+
if (!cc || !version.startsWith('3.'))
|
|
25
|
+
throw new Error('Cocos Creator 3.x runtime not found');
|
|
26
|
+
const scene = cc.director?.getScene?.();
|
|
27
|
+
if (!scene)
|
|
28
|
+
throw new Error('Active Cocos scene not found');
|
|
29
|
+
const sensitiveParts = ['token', 'cookie', 'authorization', 'password', 'secret', 'credential', 'storage', 'jwt', 'apikey', 'privatekey', 'accesskey', 'signingkey', 'authheader'];
|
|
30
|
+
const isSensitiveKey = (key) => {
|
|
31
|
+
const normalized = key.toLowerCase().replace(/[^a-z]/g, '');
|
|
32
|
+
return sensitiveParts.some(part => normalized.includes(part))
|
|
33
|
+
|| normalized.includes('bearer')
|
|
34
|
+
|| normalized.startsWith('session') && ['id', 'key', 'token'].some(part => normalized.includes(part));
|
|
35
|
+
};
|
|
36
|
+
const dataProperty = (value, key) => {
|
|
37
|
+
let current = value;
|
|
38
|
+
const prototypes = new Set();
|
|
39
|
+
for (let depth = 0; current && depth < 20 && !prototypes.has(current); depth++) {
|
|
40
|
+
prototypes.add(current);
|
|
41
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
42
|
+
if (descriptor)
|
|
43
|
+
return 'value' in descriptor ? descriptor.value : undefined;
|
|
44
|
+
current = Object.getPrototypeOf(current);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
const componentName = (component) => {
|
|
49
|
+
// ponytail: constructor metadata is the compatibility ceiling; add explicit engine adapters if minified builds need more.
|
|
50
|
+
const name = dataProperty(component, 'constructor')?.name || dataProperty(component, '__classname__') || 'Component';
|
|
51
|
+
return String(name).replace(/^cc\./, '').slice(0, 120);
|
|
52
|
+
};
|
|
53
|
+
const components = (node) => {
|
|
54
|
+
const value = node?.components;
|
|
55
|
+
return Array.isArray(value) ? value.filter(Boolean) : [];
|
|
56
|
+
};
|
|
57
|
+
const children = (node) => {
|
|
58
|
+
const value = node?.children;
|
|
59
|
+
return Array.isArray(value) ? value.filter(Boolean) : [];
|
|
60
|
+
};
|
|
61
|
+
const summary = (node) => ({
|
|
62
|
+
uuid: String(node?.uuid ?? ''),
|
|
63
|
+
name: String(node?.name ?? '').slice(0, 500),
|
|
64
|
+
active: node?.active !== false,
|
|
65
|
+
activeInHierarchy: node?.activeInHierarchy !== false,
|
|
66
|
+
componentTypes: components(node).slice(0, 100).map(componentName),
|
|
67
|
+
});
|
|
68
|
+
const walk = (visit, limit = 5_000) => {
|
|
69
|
+
const stack = [{ node: scene, path: `/${String(scene.name ?? '')}`, depth: 0 }];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
let count = 0;
|
|
72
|
+
let truncated = false;
|
|
73
|
+
while (stack.length && count < limit) {
|
|
74
|
+
const current = stack.pop();
|
|
75
|
+
if (!current.node || seen.has(current.node))
|
|
76
|
+
continue;
|
|
77
|
+
seen.add(current.node);
|
|
78
|
+
count++;
|
|
79
|
+
if (visit(current.node, current.path, current.depth) === false)
|
|
80
|
+
return { truncated: false };
|
|
81
|
+
const next = children(current.node);
|
|
82
|
+
const start = Math.max(0, next.length - (limit - stack.length));
|
|
83
|
+
if (start > 0)
|
|
84
|
+
truncated = true;
|
|
85
|
+
for (let index = next.length - 1; index >= start; index--) {
|
|
86
|
+
const child = next[index];
|
|
87
|
+
const childName = String(child?.name ?? '').slice(0, 500);
|
|
88
|
+
const childPath = `${current.path}/${childName}`;
|
|
89
|
+
stack.push({ node: child, path: childPath.length <= 10_000 ? childPath : `${childPath.slice(0, 9_999)}…`, depth: current.depth + 1 });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { truncated: truncated || stack.length > 0 };
|
|
93
|
+
};
|
|
94
|
+
const findByUuid = (uuid) => {
|
|
95
|
+
let match;
|
|
96
|
+
const traversal = walk(node => {
|
|
97
|
+
if (node?.uuid === uuid) {
|
|
98
|
+
match = node;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
if (!match)
|
|
103
|
+
throw new Error(traversal.truncated ? 'Node not found within traversal limit' : 'Node not found');
|
|
104
|
+
return match;
|
|
105
|
+
};
|
|
106
|
+
if (request.action === 'sceneTree') {
|
|
107
|
+
const maxDepth = Math.min(Math.max(request.maxDepth ?? 6, 0), 20);
|
|
108
|
+
const maxNodes = Math.min(Math.max(request.maxNodes ?? 500, 1), 5_000);
|
|
109
|
+
let count = 0;
|
|
110
|
+
let truncated = false;
|
|
111
|
+
const build = (node, depth, seen) => {
|
|
112
|
+
if (count >= maxNodes) {
|
|
113
|
+
truncated = true;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
count++;
|
|
117
|
+
const item = summary(node);
|
|
118
|
+
if (depth >= maxDepth) {
|
|
119
|
+
if (children(node).length)
|
|
120
|
+
truncated = true;
|
|
121
|
+
return item;
|
|
122
|
+
}
|
|
123
|
+
if (seen.has(node))
|
|
124
|
+
return { ...item, circular: true };
|
|
125
|
+
seen.add(node);
|
|
126
|
+
item.children = [];
|
|
127
|
+
for (const child of children(node)) {
|
|
128
|
+
if (count >= maxNodes) {
|
|
129
|
+
truncated = true;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const built = build(child, depth + 1, seen);
|
|
133
|
+
if (built)
|
|
134
|
+
item.children.push(built);
|
|
135
|
+
}
|
|
136
|
+
seen.delete(node);
|
|
137
|
+
return item;
|
|
138
|
+
};
|
|
139
|
+
return { version, scene: build(scene, 0, new Set()), nodeCount: count, truncated };
|
|
140
|
+
}
|
|
141
|
+
if (request.action === 'findNode') {
|
|
142
|
+
if (![request.uuid, request.name, request.path].filter(Boolean).length)
|
|
143
|
+
throw new Error('uuid, name, or path is required');
|
|
144
|
+
const limit = Math.min(Math.max(request.limit ?? 20, 1), 100);
|
|
145
|
+
const matches = [];
|
|
146
|
+
let more = false;
|
|
147
|
+
const traversal = walk((node, path) => {
|
|
148
|
+
const matched = request.uuid ? node?.uuid === request.uuid
|
|
149
|
+
: request.path ? path === request.path
|
|
150
|
+
: node?.name === request.name;
|
|
151
|
+
if (matched) {
|
|
152
|
+
if (matches.length < limit)
|
|
153
|
+
matches.push({ ...summary(node), path });
|
|
154
|
+
else {
|
|
155
|
+
more = true;
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return { version, matches, ambiguous: matches.length > 1 || more, truncated: more || traversal.truncated };
|
|
161
|
+
}
|
|
162
|
+
if (request.action === 'getComponents') {
|
|
163
|
+
const node = findByUuid(request.uuid);
|
|
164
|
+
return {
|
|
165
|
+
version,
|
|
166
|
+
node: summary(node),
|
|
167
|
+
components: components(node).slice(0, 200).map(component => ({
|
|
168
|
+
type: componentName(component),
|
|
169
|
+
uuid: String(component.uuid ?? ''),
|
|
170
|
+
enabled: component.enabled !== false,
|
|
171
|
+
})),
|
|
172
|
+
truncated: components(node).length > 200,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (request.action === 'getProperties') {
|
|
176
|
+
const node = findByUuid(request.uuid);
|
|
177
|
+
const selected = request.componentType
|
|
178
|
+
? components(node).find(component => componentName(component) === request.componentType)
|
|
179
|
+
: node;
|
|
180
|
+
if (!selected)
|
|
181
|
+
throw new Error('Component not found');
|
|
182
|
+
const maxDepth = Math.min(Math.max(request.maxDepth ?? 3, 0), 6);
|
|
183
|
+
let propertyCount = 0;
|
|
184
|
+
let truncated = false;
|
|
185
|
+
const seen = new WeakSet();
|
|
186
|
+
const serialize = (value, depth, reference = true) => {
|
|
187
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
188
|
+
return value;
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
if (value.length > 2_000)
|
|
191
|
+
truncated = true;
|
|
192
|
+
return value.slice(0, 2_000);
|
|
193
|
+
}
|
|
194
|
+
if (typeof value !== 'object')
|
|
195
|
+
return undefined;
|
|
196
|
+
const uuid = dataProperty(value, 'uuid');
|
|
197
|
+
if (reference && uuid && Array.isArray(dataProperty(value, 'children'))) {
|
|
198
|
+
return { $type: 'Node', uuid: String(uuid), name: String(dataProperty(value, 'name') ?? '').slice(0, 500) };
|
|
199
|
+
}
|
|
200
|
+
if (reference && uuid && dataProperty(value, 'node'))
|
|
201
|
+
return { $type: 'Component', uuid: String(uuid), type: componentName(value) };
|
|
202
|
+
if (depth >= maxDepth) {
|
|
203
|
+
truncated = true;
|
|
204
|
+
return '[MaxDepth]';
|
|
205
|
+
}
|
|
206
|
+
if (seen.has(value)) {
|
|
207
|
+
truncated = true;
|
|
208
|
+
return '[Circular]';
|
|
209
|
+
}
|
|
210
|
+
seen.add(value);
|
|
211
|
+
const output = Array.isArray(value) ? [] : {};
|
|
212
|
+
let inspected = 0;
|
|
213
|
+
for (const key in value) {
|
|
214
|
+
if (!Object.prototype.hasOwnProperty.call(value, key))
|
|
215
|
+
continue;
|
|
216
|
+
inspected++;
|
|
217
|
+
if (inspected > 1_000) {
|
|
218
|
+
truncated = true;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
if (propertyCount >= 1_000) {
|
|
222
|
+
truncated = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
propertyCount++;
|
|
226
|
+
if (key.startsWith('_') || isSensitiveKey(key))
|
|
227
|
+
continue;
|
|
228
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
229
|
+
if (!descriptor || !('value' in descriptor))
|
|
230
|
+
continue;
|
|
231
|
+
const item = serialize(descriptor.value, depth + 1);
|
|
232
|
+
if (item !== undefined) {
|
|
233
|
+
if (Array.isArray(output)) {
|
|
234
|
+
const index = Number(key);
|
|
235
|
+
if (Number.isInteger(index) && index >= 0 && index < 1_000)
|
|
236
|
+
output[index] = item;
|
|
237
|
+
else
|
|
238
|
+
truncated = true;
|
|
239
|
+
}
|
|
240
|
+
else
|
|
241
|
+
output[key] = item;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
seen.delete(value);
|
|
245
|
+
return output;
|
|
246
|
+
};
|
|
247
|
+
return { version, node: summary(node), componentType: request.componentType, properties: serialize(selected, 0, false), truncated, propertyCount };
|
|
248
|
+
}
|
|
249
|
+
const node = findByUuid(request.uuid);
|
|
250
|
+
const document = root.document;
|
|
251
|
+
if (!document?.body)
|
|
252
|
+
throw new Error('Page document is unavailable');
|
|
253
|
+
const transform = components(node).find(component => componentName(component) === 'UITransform');
|
|
254
|
+
if (!transform?.contentSize || !transform?.anchorPoint)
|
|
255
|
+
throw new Error('Node has no UITransform');
|
|
256
|
+
const canvas = cc.game?.canvas ?? document.querySelector('#GameCanvas');
|
|
257
|
+
if (!(canvas instanceof HTMLCanvasElement))
|
|
258
|
+
throw new Error('Cocos canvas not found');
|
|
259
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
260
|
+
const visible = cc.view?.getVisibleSize?.() ?? { width: canvasRect.width, height: canvasRect.height };
|
|
261
|
+
const position = node.worldPosition ?? node.getWorldPosition?.();
|
|
262
|
+
if (!position)
|
|
263
|
+
throw new Error('Node world position is unavailable');
|
|
264
|
+
const scale = node.worldScale ?? node.scale ?? { x: 1, y: 1 };
|
|
265
|
+
const localWidth = Number(transform.contentSize.width);
|
|
266
|
+
const localHeight = Number(transform.contentSize.height);
|
|
267
|
+
const anchorX = Number(transform.anchorPoint.x);
|
|
268
|
+
const anchorY = Number(transform.anchorPoint.y);
|
|
269
|
+
const left = -localWidth * anchorX;
|
|
270
|
+
const bottom = -localHeight * anchorY;
|
|
271
|
+
const points = typeof transform.convertToWorldSpaceAR === 'function' && cc.Vec3
|
|
272
|
+
? [[left, bottom], [left + localWidth, bottom], [left, bottom + localHeight], [left + localWidth, bottom + localHeight]]
|
|
273
|
+
.map(([x, y]) => transform.convertToWorldSpaceAR(new cc.Vec3(x, y, 0)))
|
|
274
|
+
: [{ x: Number(position.x) + left * Number(scale.x ?? 1), y: Number(position.y) + bottom * Number(scale.y ?? 1) },
|
|
275
|
+
{ x: Number(position.x) + (left + localWidth) * Number(scale.x ?? 1), y: Number(position.y) + (bottom + localHeight) * Number(scale.y ?? 1) }];
|
|
276
|
+
const worldX = Math.min(...points.map(point => Number(point.x)));
|
|
277
|
+
const worldY = Math.min(...points.map(point => Number(point.y)));
|
|
278
|
+
const width = Math.max(...points.map(point => Number(point.x))) - worldX;
|
|
279
|
+
const height = Math.max(...points.map(point => Number(point.y))) - worldY;
|
|
280
|
+
const visibleOrigin = cc.view?.getVisibleOrigin?.() ?? { x: 0, y: 0 };
|
|
281
|
+
const scaleX = canvasRect.width / Number(visible.width);
|
|
282
|
+
const scaleY = canvasRect.height / Number(visible.height);
|
|
283
|
+
const bounds = {
|
|
284
|
+
x: canvasRect.left + (worldX - Number(visibleOrigin.x ?? 0)) * scaleX,
|
|
285
|
+
y: canvasRect.top + (Number(visibleOrigin.y ?? 0) + Number(visible.height) - worldY - height) * scaleY,
|
|
286
|
+
width: width * scaleX,
|
|
287
|
+
height: height * scaleY,
|
|
288
|
+
};
|
|
289
|
+
let overlay = root.__cocosWebInspectorOverlay;
|
|
290
|
+
if (!overlay?.isConnected) {
|
|
291
|
+
overlay = document.createElement('div');
|
|
292
|
+
root.__cocosWebInspectorOverlay = overlay;
|
|
293
|
+
document.body.appendChild(overlay);
|
|
294
|
+
}
|
|
295
|
+
Object.assign(overlay.style, {
|
|
296
|
+
position: 'fixed', pointerEvents: 'none', zIndex: '2147483647', boxSizing: 'border-box',
|
|
297
|
+
border: '2px solid #27c2ff', background: 'rgba(39, 194, 255, 0.2)',
|
|
298
|
+
left: `${bounds.x}px`, top: `${bounds.y}px`, width: `${bounds.width}px`, height: `${bounds.height}px`,
|
|
299
|
+
});
|
|
300
|
+
clearTimeout(root.__cocosWebInspectorHighlightTimer);
|
|
301
|
+
root.__cocosWebInspectorHighlightTimer = setTimeout(() => {
|
|
302
|
+
overlay?.remove();
|
|
303
|
+
if (root.__cocosWebInspectorOverlay === overlay)
|
|
304
|
+
root.__cocosWebInspectorOverlay = undefined;
|
|
305
|
+
}, Math.min(Math.max(request.durationMs ?? 2_000, 100), 10_000));
|
|
306
|
+
return { version, uuid: request.uuid, highlighted: true, bounds };
|
|
307
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Browser, type Page } from 'playwright-core';
|
|
2
|
+
export declare function validateLocalUrl(raw: string, page?: boolean): URL;
|
|
3
|
+
export declare function sanitizeUrl(raw: string): string;
|
|
4
|
+
export declare class BrowserConnection {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly endpoint;
|
|
7
|
+
private readonly timeout;
|
|
8
|
+
private readonly connectOverCDP;
|
|
9
|
+
constructor(endpoint: string, timeout?: number, connectOverCDP?: (endpoint: string, options: {
|
|
10
|
+
timeout: number;
|
|
11
|
+
}) => Promise<Browser>);
|
|
12
|
+
page(pageUrl?: string): Promise<Page>;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { chromium } from 'playwright-core';
|
|
2
|
+
const LOOPBACK_PROTOCOLS = new Set(['http:', 'https:', 'ws:', 'wss:']);
|
|
3
|
+
const PAGE_PROTOCOLS = new Set(['http:', 'https:']);
|
|
4
|
+
function isLoopbackHostname(hostname) {
|
|
5
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
6
|
+
if (host === 'localhost' || host.endsWith('.localhost') || host === '::1')
|
|
7
|
+
return true;
|
|
8
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
9
|
+
return !!match && match.slice(1).every(part => Number(part) <= 255) && match[1] === '127';
|
|
10
|
+
}
|
|
11
|
+
export function validateLocalUrl(raw, page = false) {
|
|
12
|
+
let url;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('Invalid URL');
|
|
18
|
+
}
|
|
19
|
+
const protocols = page ? PAGE_PROTOCOLS : LOOPBACK_PROTOCOLS;
|
|
20
|
+
if (!protocols.has(url.protocol))
|
|
21
|
+
throw new Error(`Unsupported ${page ? 'page' : 'CDP'} URL protocol`);
|
|
22
|
+
if (url.username || url.password)
|
|
23
|
+
throw new Error('URL credentials are not allowed');
|
|
24
|
+
if (!page && (url.search || url.hash))
|
|
25
|
+
throw new Error('CDP URL query and fragment are not allowed');
|
|
26
|
+
if (!isLoopbackHostname(url.hostname))
|
|
27
|
+
throw new Error('Only localhost targets are allowed');
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
export function sanitizeUrl(raw) {
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(raw);
|
|
33
|
+
url.username = '';
|
|
34
|
+
url.password = '';
|
|
35
|
+
url.search = '';
|
|
36
|
+
url.hash = '';
|
|
37
|
+
return url.toString().slice(0, 2_000);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return '<invalid-url>';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class BrowserConnection {
|
|
44
|
+
endpoint;
|
|
45
|
+
timeout;
|
|
46
|
+
connectOverCDP;
|
|
47
|
+
#browser;
|
|
48
|
+
#connecting;
|
|
49
|
+
#closed = false;
|
|
50
|
+
constructor(endpoint, timeout = 10_000, connectOverCDP = (target, options) => chromium.connectOverCDP(target, options)) {
|
|
51
|
+
this.endpoint = endpoint;
|
|
52
|
+
this.timeout = timeout;
|
|
53
|
+
this.connectOverCDP = connectOverCDP;
|
|
54
|
+
validateLocalUrl(endpoint);
|
|
55
|
+
}
|
|
56
|
+
async page(pageUrl) {
|
|
57
|
+
if (this.#closed)
|
|
58
|
+
throw new Error('Browser connection is closed');
|
|
59
|
+
const browser = await this.#connect();
|
|
60
|
+
if (this.#closed)
|
|
61
|
+
throw new Error('Browser connection is closed');
|
|
62
|
+
const pages = browser.contexts().flatMap(context => context.pages()).filter(candidate => {
|
|
63
|
+
try {
|
|
64
|
+
validateLocalUrl(candidate.url(), true);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (pageUrl) {
|
|
72
|
+
const wanted = validateLocalUrl(pageUrl, true).href;
|
|
73
|
+
const selected = pages.filter(candidate => new URL(candidate.url()).href === wanted);
|
|
74
|
+
if (selected.length === 0)
|
|
75
|
+
throw new Error(`Local page not found: ${sanitizeUrl(pageUrl)}`);
|
|
76
|
+
if (selected.length > 1)
|
|
77
|
+
throw new Error(`Multiple localhost pages match pageUrl: ${sanitizeUrl(pageUrl)}`);
|
|
78
|
+
return selected[0];
|
|
79
|
+
}
|
|
80
|
+
if (pages.length === 0)
|
|
81
|
+
throw new Error('No localhost page is attached to Chromium');
|
|
82
|
+
if (pages.length > 1) {
|
|
83
|
+
const choices = pages.slice(0, 10).map(candidate => sanitizeUrl(candidate.url())).join(', ');
|
|
84
|
+
throw new Error(`Multiple localhost pages found; pass pageUrl: ${choices}`);
|
|
85
|
+
}
|
|
86
|
+
return pages[0];
|
|
87
|
+
}
|
|
88
|
+
async close() {
|
|
89
|
+
this.#closed = true;
|
|
90
|
+
const current = this.#browser;
|
|
91
|
+
const connecting = this.#connecting;
|
|
92
|
+
this.#browser = undefined;
|
|
93
|
+
const pending = await connecting?.catch(() => undefined);
|
|
94
|
+
if (this.#browser === pending)
|
|
95
|
+
this.#browser = undefined;
|
|
96
|
+
await Promise.all([...new Set([current, pending].filter((browser) => !!browser))].map(browser => browser.close()));
|
|
97
|
+
}
|
|
98
|
+
async #connect() {
|
|
99
|
+
if (this.#closed)
|
|
100
|
+
throw new Error('Browser connection is closed');
|
|
101
|
+
if (this.#browser?.isConnected())
|
|
102
|
+
return this.#browser;
|
|
103
|
+
if (this.#connecting)
|
|
104
|
+
return this.#connecting;
|
|
105
|
+
const connecting = this.connectOverCDP(this.endpoint, { timeout: this.timeout }).then(async (browser) => {
|
|
106
|
+
if (this.#closed) {
|
|
107
|
+
await browser.close();
|
|
108
|
+
throw new Error('Browser connection is closed');
|
|
109
|
+
}
|
|
110
|
+
this.#browser = browser;
|
|
111
|
+
browser.on('disconnected', () => {
|
|
112
|
+
if (this.#browser === browser)
|
|
113
|
+
this.#browser = undefined;
|
|
114
|
+
});
|
|
115
|
+
return browser;
|
|
116
|
+
});
|
|
117
|
+
this.#connecting = connecting;
|
|
118
|
+
try {
|
|
119
|
+
return await connecting;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (this.#connecting === connecting)
|
|
123
|
+
this.#connecting = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { BrowserConnection } from './browser.js';
|
|
4
|
+
import { createServer } from './server.js';
|
|
5
|
+
function endpointFromArgs(args) {
|
|
6
|
+
const index = args.indexOf('--cdp-endpoint');
|
|
7
|
+
if (index !== -1) {
|
|
8
|
+
const value = args[index + 1];
|
|
9
|
+
if (!value)
|
|
10
|
+
throw new Error('--cdp-endpoint requires a value');
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
const unknown = args.find(arg => arg.startsWith('-'));
|
|
14
|
+
if (unknown)
|
|
15
|
+
throw new Error(`Unknown argument: ${unknown}`);
|
|
16
|
+
return process.env.COCOS_CDP_ENDPOINT ?? 'http://127.0.0.1:9222';
|
|
17
|
+
}
|
|
18
|
+
async function main() {
|
|
19
|
+
const browser = new BrowserConnection(endpointFromArgs(process.argv.slice(2)));
|
|
20
|
+
const server = createServer(browser);
|
|
21
|
+
let closing = false;
|
|
22
|
+
const close = async () => {
|
|
23
|
+
if (closing)
|
|
24
|
+
return;
|
|
25
|
+
closing = true;
|
|
26
|
+
await Promise.allSettled([browser.close(), server.close()]);
|
|
27
|
+
};
|
|
28
|
+
process.once('SIGINT', () => void close());
|
|
29
|
+
process.once('SIGTERM', () => void close());
|
|
30
|
+
await server.connect(new StdioServerTransport());
|
|
31
|
+
}
|
|
32
|
+
main().catch(error => {
|
|
33
|
+
process.stderr.write(`${error instanceof Error ? error.message : 'Failed to start server'}\n`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { runBridge } from './bridge.js';
|
|
4
|
+
const pageUrl = z.url().optional();
|
|
5
|
+
const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
6
|
+
const temporaryMutation = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
7
|
+
function response(data) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
10
|
+
structuredContent: data,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function failure(error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : 'Inspector request failed';
|
|
15
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
16
|
+
}
|
|
17
|
+
export function createServer(browser) {
|
|
18
|
+
const server = new McpServer({ name: 'cocos-web-inspector-mcp', version: '0.1.1' });
|
|
19
|
+
const execute = async (request, selectedPage) => {
|
|
20
|
+
try {
|
|
21
|
+
return response(await runBridge(await browser.page(selectedPage), request));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return failure(error);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
server.registerTool('cocos_scene_tree', {
|
|
28
|
+
description: 'Return a bounded Cocos Creator 3.x scene tree from a localhost Chromium page.',
|
|
29
|
+
inputSchema: z.object({ pageUrl, maxDepth: z.number().int().min(0).max(20).optional(), maxNodes: z.number().int().min(1).max(5_000).optional() }).strict(),
|
|
30
|
+
annotations: readOnly,
|
|
31
|
+
}, input => execute({ action: 'sceneTree', maxDepth: input.maxDepth, maxNodes: input.maxNodes }, input.pageUrl));
|
|
32
|
+
server.registerTool('cocos_find_node', {
|
|
33
|
+
description: 'Find Cocos nodes by exact UUID, name, or absolute scene path.',
|
|
34
|
+
inputSchema: z.object({ pageUrl, uuid: z.string().min(1).optional(), name: z.string().min(1).optional(), path: z.string().min(1).optional(), limit: z.number().int().min(1).max(100).optional() }).strict()
|
|
35
|
+
.refine(value => [value.uuid, value.name, value.path].filter(Boolean).length === 1, 'Provide exactly one of uuid, name, or path'),
|
|
36
|
+
annotations: readOnly,
|
|
37
|
+
}, input => execute({ action: 'findNode', uuid: input.uuid, name: input.name, path: input.path, limit: input.limit }, input.pageUrl));
|
|
38
|
+
server.registerTool('cocos_get_components', {
|
|
39
|
+
description: 'List bounded component summaries for one Cocos node UUID.',
|
|
40
|
+
inputSchema: z.object({ pageUrl, uuid: z.string().min(1) }).strict(),
|
|
41
|
+
annotations: readOnly,
|
|
42
|
+
}, input => execute({ action: 'getComponents', uuid: input.uuid }, input.pageUrl));
|
|
43
|
+
server.registerTool('cocos_get_properties', {
|
|
44
|
+
description: 'Read bounded, cycle-safe public properties from a Cocos node or component.',
|
|
45
|
+
inputSchema: z.object({ pageUrl, uuid: z.string().min(1), componentType: z.string().min(1).optional(), maxDepth: z.number().int().min(0).max(6).optional() }).strict(),
|
|
46
|
+
annotations: readOnly,
|
|
47
|
+
}, input => execute({ action: 'getProperties', uuid: input.uuid, componentType: input.componentType, maxDepth: input.maxDepth }, input.pageUrl));
|
|
48
|
+
server.registerTool('cocos_highlight_node', {
|
|
49
|
+
description: 'Temporarily draw a pointer-transparent DOM overlay around one Cocos UI node.',
|
|
50
|
+
inputSchema: z.object({ pageUrl, uuid: z.string().min(1), durationMs: z.number().int().min(100).max(10_000).optional() }).strict(),
|
|
51
|
+
annotations: temporaryMutation,
|
|
52
|
+
}, input => execute({ action: 'highlightNode', uuid: input.uuid, durationMs: input.durationMs }, input.pageUrl));
|
|
53
|
+
return server;
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cocos-web-inspector-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Read-only MCP inspector for Cocos Creator 3.x running in local Chromium",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/el1pn/cocos-web-inspector-mcp.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/el1pn/cocos-web-inspector-mcp/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/el1pn/cocos-web-inspector-mcp#readme",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"cocos-web-inspector-mcp": "dist/src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/src",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test:built": "node --test dist/test/self-test.js",
|
|
29
|
+
"test": "npm run build && npm run test:built",
|
|
30
|
+
"test:package": "node test/package-test.mjs",
|
|
31
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
32
|
+
"prepack": "npm run clean && npm run build",
|
|
33
|
+
"check": "npm run build && npm run test:built && npm run test:package",
|
|
34
|
+
"audit:prod": "npm audit --omit=dev --audit-level=high",
|
|
35
|
+
"start": "node dist/src/index.js"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
39
|
+
"playwright-core": "^1.63.0",
|
|
40
|
+
"zod": "^4.6.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^26.6.4",
|
|
44
|
+
"typescript": "^7.0.2"
|
|
45
|
+
}
|
|
46
|
+
}
|