chore(mcp): 换用 cocos-mcp-server 扩展, .mcp.json 改 HTTP 直连

旧扩展 @iflow-mcp/harady-cocos-creator-mcp(164 个细粒度工具 + stdio-bridge
转发)整个删除, 换成 cocos-mcp-server v1.7.9(16 个聚合工具, 靠 action 分派)。

新扩展自身起 streamable-http 服务, 不再需要 stdio 桥, 故 .mcp.json 从
node + stdio-bridge.js 改为 { type: "http", url: "http://127.0.0.1:3000/mcp" }。
原配置指向的 client/stdio-bridge.js 已随旧扩展删除, MCP 因此加载失败。

服务器名保留 cocos-creator-mcp(而非 INSTALL.md 建议的 cocos-creator), 以免
打断 CLAUDE.md、cocos-mcp 技能与 settings.local.json 里的
mcp__cocos-creator-mcp__* 允许规则。

gitignore: 新扩展是商业付费产品(vberai), 非 npm 可重装, dist/sidecar/static
共约 16M 发行产物+加密载荷+平台二进制不入库; 顺带忽略 Claude Code 个人本地
配置与编辑器运行时端口文件。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 08:01:07 +08:00
co-authored by Claude Opus 5
parent 4b4e2261b4
commit 42bc0722b8
67 changed files with 3553 additions and 10913 deletions
+13 -4
View File
@@ -31,10 +31,19 @@ yarn-error.log*
cocoscreator_projects/games/*/assets/framework
cocoscreator_projects/games/*/assets/framework.meta
# ---------- cocos-creator-mcp 扩展:本地依赖与构建产物 ----------
# clone 后在该目录跑 `npm install && npm run build` 重新生成
cocoscreator_projects/YouleNexus/extensions/cocos-creator-mcp/node_modules/
cocoscreator_projects/YouleNexus/extensions/cocos-creator-mcp/dist/
# ---------- cocos-mcp-server 扩展:构建产物与二进制 ----------
# 商业付费扩展(vberai),非 npm 可重装:dist/sidecar/static 是发行产物+加密载荷+
# 平台二进制,约 16M,不入库。换机器时从购买渠道重新拷贝整个扩展目录。
cocoscreator_projects/YouleNexus/extensions/cocos-mcp-server/dist/
cocoscreator_projects/YouleNexus/extensions/cocos-mcp-server/sidecar/
cocoscreator_projects/YouleNexus/extensions/cocos-mcp-server/static/
# ---------- Claude Code 个人本地配置 ----------
# .claude/skills/ 入库(团队共享),本地权限覆盖不入库
.claude/settings.local.json
# 运行时状态(编辑器每次启动重写)
cocoscreator_projects/YouleNexus/settings/.mcp-port.json
# ============================================================
# 说明:以下内容【应当】入库,勿忽略——
+2 -7
View File
@@ -1,13 +1,8 @@
{
"mcpServers": {
"cocos-creator-mcp": {
"command": "node",
"args": [
"G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/extensions/cocos-creator-mcp/client/stdio-bridge.js"
],
"env": {
"COCOS_MCP_URL": "http://127.0.0.1:3000/mcp"
}
"type": "http",
"url": "http://127.0.0.1:3000/mcp"
}
}
}
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Yusuke Harada
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,539 +0,0 @@
# Cocos Creator MCP
[MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server extension for Cocos Creator 3.8+.
AI assistants like Claude can control Cocos Creator editor through this extension — creating nodes, editing scenes, managing prefabs, building projects, and more.
## Features
- **164 Tools** across 13 categories — comprehensive editor automation
- **Streamable HTTP (SSE)** — Native support for MCP's Streamable HTTP transport
- **JSON-RPC 2.0** — Standard MCP protocol compliance
- **Prefab Property Persistence** — Component properties are correctly preserved when saving prefabs
- **Preview in Editor** — Start editor preview programmatically (no manual button click needed)
- **Screenshot Capture** — Capture editor window and game preview screenshots (WebP / PNG)
- **Video Recording** — Record game preview canvas to video (MP4 / WebM) via Preview Recorder panel
- **Game Command Control** — Send commands to running game preview (screenshot, click, navigate, state, inspect)
- **Client Scripts** — Drop-in TypeScript files for game preview integration (`client/`)
- **Auto Start** — Server starts automatically when the extension loads
- **Tool Call Logging** — All tool invocations logged with timing for debugging
- **UUID Validation** — Input validation helpers for better error messages
- **i18n** — English, Japanese, Chinese
- **Regression Tests** — 200+ assertions covering core tool flows
## Quick Start
### 1. Install
Copy or symlink this extension into your Cocos Creator project's `extensions/` directory:
```bash
# Windows (Junction — no admin required)
mklink /J "your-project\extensions\cocos-creator-mcp" "path\to\cocos-creator-mcp"
# macOS / Linux
ln -s /path/to/cocos-creator-mcp your-project/extensions/cocos-creator-mcp
```
### 2. Build
```bash
cd cocos-creator-mcp
npm install
npm run build
```
### 3. Enable in Cocos Creator
1. Open your project in Cocos Creator
2. Go to **Extension > Extension Manager**
3. Enable **Cocos Creator MCP**
4. Open the panel: **Extension > Cocos Creator MCP > Open Panel**
5. Click **Start Server** (or set `autoStart: true` in config)
### 4. Connect from Claude Code
Pick one of the two transports below.
#### Option A — stdio bridge (recommended for Claude Code VSCode extension)
The Claude Code VSCode extension currently has a bug where it unconditionally
tries OAuth Dynamic Client Registration for HTTP-type MCP servers and fails
with `SDK auth failed` (see upstream issues
[#26917](https://github.com/anthropics/claude-code/issues/26917),
[#38102](https://github.com/anthropics/claude-code/issues/38102),
[#29697](https://github.com/anthropics/claude-code/issues/29697)).
To avoid it entirely, use the bundled stdio bridge. It speaks JSON-RPC on
stdin/stdout and forwards to the HTTP server internally.
```json
{
"mcpServers": {
"cocos-creator-mcp": {
"command": "node",
"args": [
"<ABSOLUTE_PATH_TO>/cocos-creator-mcp/client/stdio-bridge.js"
]
}
}
}
```
Optional env var: `COCOS_MCP_URL` (default `http://127.0.0.1:3000/mcp`).
#### Option B — direct HTTP
Works with Claude Code CLI, Cursor, Cline, and other clients that don't force
OAuth on HTTP MCP. The server ships minimal dummy OAuth endpoints
(`/.well-known/oauth-*`, `/oauth/register|authorize|token`) so OAuth-requiring
clients can still complete a pro-forma flow on localhost.
```json
{
"mcpServers": {
"cocos-creator-mcp": {
"type": "http",
"url": "http://127.0.0.1:3000/mcp"
}
}
}
```
> The dummy OAuth endpoints will be removed once upstream issues
> ([#26917](https://github.com/anthropics/claude-code/issues/26917),
> [#38102](https://github.com/anthropics/claude-code/issues/38102))
> are resolved or real authentication is introduced.
### 5. Verify
```bash
curl http://127.0.0.1:3000/health
# {"status":"ok","tools":164}
```
## Available Tools (164)
<details>
<summary><strong>Scene (6)</strong> — Scene lifecycle and hierarchy</summary>
| Tool | Description |
|------|-------------|
| `scene_get_hierarchy` | Get the node tree (with optional component info) |
| `scene_open` | Open a scene by UUID or db:// path |
| `scene_save` | Save the current scene |
| `scene_get_list` | List all .scene files |
| `scene_close` | Close the current scene |
| `scene_get_current` | Get name and UUID of the current scene |
</details>
<details>
<summary><strong>Scene Advanced (30)</strong> — Undo, clipboard, queries, property manipulation</summary>
| Tool | Description |
|------|-------------|
| `scene_execute_script` | Execute a custom scene script method |
| `scene_snapshot` | Take a snapshot for undo |
| `scene_snapshot_abort` | Abort the current undo snapshot |
| `scene_begin_undo` | Begin recording undo operations |
| `scene_end_undo` | End undo recording |
| `scene_cancel_undo` | Cancel undo recording |
| `scene_query_dirty` | Check if scene has unsaved changes |
| `scene_query_ready` | Check if scene is fully loaded |
| `scene_query_classes` | List all available component classes |
| `scene_query_components` | Query components for a node |
| `scene_query_component_has_script` | Check if a component has a script file |
| `scene_query_node_tree` | Get raw node tree from editor |
| `scene_query_node` | Get full property dump of a node |
| `scene_query_component` | Get full property dump of a component |
| `scene_query_nodes_by_asset` | Find nodes referencing an asset |
| `scene_query_scene_bounds` | Get scene bounding rect |
| `scene_soft_reload` | Soft reload scene |
| `scene_reset_node_transform` | Reset transform to default |
| `scene_reset_property` | Reset a specific property to default |
| `scene_reset_component` | Reset a component to defaults |
| `scene_copy_node` | Copy node to clipboard |
| `scene_paste_node` | Paste node from clipboard |
| `scene_cut_node` | Cut node to clipboard |
| `scene_create` | Create a new empty scene |
| `scene_save_as` | Save scene to a new file |
| `scene_set_parent` | Reparent node(s) with official API |
| `scene_restore_prefab` | Restore prefab node to original state |
| `scene_execute_component_method` | Call a method on a component |
| `scene_move_array_element` | Reorder array property element |
| `scene_remove_array_element` | Remove array property element |
</details>
<details>
<summary><strong>Scene View (19)</strong> — Gizmo, camera, grid, viewport</summary>
| Tool | Description |
|------|-------------|
| `view_change_gizmo_tool` | Switch gizmo tool (move/rotate/scale/rect) |
| `view_query_gizmo_tool` | Get current gizmo tool |
| `view_change_gizmo_pivot` | Change pivot mode (center/pivot) |
| `view_query_gizmo_pivot` | Get current pivot mode |
| `view_change_gizmo_coordinate` | Change coordinate system (local/global) |
| `view_query_gizmo_coordinate` | Get current coordinate system |
| `view_change_mode_2d_3d` | Switch 2D/3D view |
| `view_query_mode_2d_3d` | Get current view mode |
| `view_set_grid_visible` | Show/hide grid |
| `view_query_grid_visible` | Check grid visibility |
| `view_set_icon_gizmo_3d` | Toggle 3D icon gizmos |
| `view_query_icon_gizmo_3d` | Check 3D icon gizmo state |
| `view_set_icon_gizmo_size` | Set icon gizmo size |
| `view_query_icon_gizmo_size` | Get icon gizmo size |
| `view_focus_on_node` | Focus camera on node(s) |
| `view_align_with_view` | Align node with camera view |
| `view_align_view_with_node` | Align camera with node |
| `view_get_status` | Get all view settings at once |
| `view_reset` | Reset scene view to default |
</details>
<details>
<summary><strong>Node (14)</strong> — Create, edit, move, delete nodes</summary>
| Tool | Description |
|------|-------------|
| `node_create` | Create node (with optional components) |
| `node_get_info` | Get node details (position, scale, components) |
| `node_find_by_name` | Find nodes by name |
| `node_set_property` | Set node property |
| `node_set_transform` | Set position/rotation/scale at once |
| `node_set_active` | Set node visibility |
| `node_set_layer` | Set node layer |
| `node_delete` | Delete node |
| `node_move` | Move node to new parent |
| `node_duplicate` | Duplicate node |
| `node_get_all` | List all nodes |
| `node_detect_type` | Detect node type (2D/3D/Node) |
| `node_create_tree` | Create a node hierarchy in one call (v1.6) |
| `node_set_layout` | Set UITransform + Widget + color/opacity at once (v1.13) |
</details>
<details>
<summary><strong>Component (8)</strong> — Add, remove, configure components</summary>
| Tool | Description |
|------|-------------|
| `component_add` | Add component (e.g. `cc.Label`, `cc.Sprite`) |
| `component_remove` | Remove component |
| `component_get_components` | List components on node |
| `component_set_property` | Set component property (Label.string, fontSize, etc.) |
| `component_get_info` | Get full component dump by UUID |
| `component_get_available` | List all available component classes |
| `component_auto_bind` | Auto-match `@property` fields to nodes by name (v1.12) |
| `component_query_enum` | Query enum values of a component property (v1.6) |
</details>
<details>
<summary><strong>Prefab (12)</strong> — Prefab lifecycle and validation</summary>
| Tool | Description |
|------|-------------|
| `prefab_list` | List all prefabs |
| `prefab_create` | Create prefab from node (properties preserved) |
| `prefab_instantiate` | Instantiate prefab into scene |
| `prefab_get_info` | Get prefab asset info |
| `prefab_update` | Apply prefab changes |
| `prefab_revert` | Revert prefab instance to original |
| `prefab_duplicate` | Copy prefab to new path |
| `prefab_validate` | Validate prefab for broken references |
| `prefab_open` | Open prefab for editing (v1.5) |
| `prefab_close` | Close prefab editing mode and return to scene (v1.5) |
| `prefab_create_and_replace` | Create prefab and replace instance in one call (v1.5) |
| `prefab_create_from_spec` | Create node tree + auto-bind + prefab_create in one call (v1.12) |
</details>
<details>
<summary><strong>Asset (18)</strong> — CRUD, queries, metadata, dependencies</summary>
| Tool | Description |
|------|-------------|
| `asset_create` | Create new asset |
| `asset_delete` | Delete asset |
| `asset_move` | Move/rename asset |
| `asset_copy` | Copy asset |
| `asset_save` | Save asset |
| `asset_reimport` | Re-import asset |
| `asset_import` | Import external file into project |
| `asset_query_path` | Get file path for UUID |
| `asset_query_uuid` | Get UUID for path |
| `asset_query_url` | Get URL for UUID |
| `asset_get_details` | Get asset metadata |
| `asset_get_dependencies` | Get asset dependencies |
| `asset_open_external` | Open in external editor |
| `asset_save_meta` | Save asset meta/importer settings |
| `asset_generate_available_url` | Generate non-conflicting asset path |
| `asset_query_ready` | Check if asset DB is ready |
| `asset_query_users` | Find assets that reference this asset |
| `asset_query_missing` | Check for missing references |
</details>
<details>
<summary><strong>Project (8)</strong> — Project info, settings, engine</summary>
| Tool | Description |
|------|-------------|
| `project_get_info` | Get project name and path |
| `project_refresh_assets` | Refresh asset database |
| `project_get_asset_info` | Get asset info by UUID |
| `project_find_asset` | Find assets by glob pattern |
| `project_get_settings` | Get project settings |
| `project_set_settings` | Set a project setting |
| `project_get_engine_info` | Get engine version and paths |
| `project_query_scripts` | Query all script plugins |
</details>
<details>
<summary><strong>Preferences (4)</strong> — Editor preferences</summary>
| Tool | Description |
|------|-------------|
| `preferences_get` | Get preference value |
| `preferences_set` | Set preference value |
| `preferences_get_all` | Get all preferences for a protocol |
| `preferences_reset` | Reset preference to default |
</details>
<details>
<summary><strong>Debug (22)</strong> — Editor info, logs, preview, screenshots, recording, game control</summary>
| Tool | Description |
|------|-------------|
| `debug_get_editor_info` | Get editor version and environment |
| `debug_list_messages` | List available Editor messages |
| `debug_execute_script` | Execute scene script method |
| `debug_get_console_logs` | Get console log entries (scene + game preview) |
| `debug_clear_console` | Clear editor console and log buffers |
| `debug_preview` | Start Preview in Editor (play button) |
| `debug_clear_code_cache` | Clear code cache (Developer > Cache) |
| `debug_screenshot` | Capture editor window screenshot |
| `debug_game_command` | Send command to game preview (screenshot/click/navigate/state/inspect) |
| `debug_batch_screenshot` | Navigate to multiple pages and screenshot each |
| `debug_record_start` | Start recording game preview canvas (webm/mp4) |
| `debug_record_stop` | Stop recording and save video file |
| `debug_reload_extension` | Reload this MCP extension (after build) |
| `debug_list_extensions` | List installed extensions |
| `debug_get_extension_info` | Get extension details |
| `debug_get_project_logs` | Read project log entries |
| `debug_search_project_logs` | Search patterns in project logs |
| `debug_get_log_file_info` | Get log file metadata |
| `debug_validate_scene` | Validate scene for common issues |
| `debug_query_devices` | List connected devices |
| `debug_open_url` | Open URL in system browser |
| `debug_wait_compile` | Wait for TypeScript compile to finish (v1.12) |
</details>
<details>
<summary><strong>Server (7)</strong> — Editor server and network</summary>
| Tool | Description |
|------|-------------|
| `server_query_ip_list` | Get editor server IPs |
| `server_query_port` | Get editor server port |
| `server_get_status` | Get full server status |
| `server_check_connectivity` | Check if editor server is reachable |
| `server_get_network_interfaces` | Get network interface details |
| `server_get_build_hash` | Get build hash of MCP dist files (v1.6) |
| `server_check_code_sync` | Check if runtime matches dist hash (v1.6) |
</details>
<details>
<summary><strong>Builder (5)</strong> — Build and preview</summary>
| Tool | Description |
|------|-------------|
| `builder_open_panel` | Open Build panel |
| `builder_get_settings` | Get build configuration |
| `builder_query_tasks` | Query active build tasks |
| `builder_run_preview` | Start preview server |
| `builder_stop_preview` | Stop preview server |
</details>
<details>
<summary><strong>Reference Image (11)</strong> — Scene overlay images</summary>
| Tool | Description |
|------|-------------|
| `refimage_add` | Add a reference image |
| `refimage_remove` | Remove a reference image |
| `refimage_list` | List all reference images |
| `refimage_clear_all` | Remove all reference images |
| `refimage_switch` | Switch active reference image |
| `refimage_set_position` | Set image position |
| `refimage_set_scale` | Set image scale |
| `refimage_set_opacity` | Set image opacity |
| `refimage_query_config` | Get reference image config |
| `refimage_query_current` | Get current active image info |
| `refimage_refresh` | Refresh image display |
</details>
## Client Scripts
The `client/` directory contains TypeScript files for runtime communication between the game preview and the MCP server. Since the extension is installed in `extensions/`, these files can be imported directly — no copying needed.
### McpConsoleCapture
Captures `console.log/warn/error` from the game preview and sends them to the MCP server.
```typescript
// Import from extensions/ (adjust relative path as needed)
import { initMcpConsoleCapture } from "../../extensions/cocos-creator-mcp/client/McpConsoleCapture";
initMcpConsoleCapture();
```
### McpDebugClient
Enables AI-driven game control: screenshots, node clicking, and custom commands.
```typescript
import { initMcpDebugClient } from "../../extensions/cocos-creator-mcp/client/McpDebugClient";
initMcpDebugClient({
customCommands: {
// Add project-specific commands
state: () => ({ success: true, data: { dump: MyDb.dump() } }),
navigate: async (args) => {
await MyRouter.goTo(args.page);
return { success: true };
},
},
});
```
**Built-in commands** (no setup needed):
- `screenshot` — Capture game screen via RenderTexture
- `click` — Click a node by name
**Custom commands** (project-specific):
- Register any handler via `customCommands` option
- Called via `debug_game_command` MCP tool
Both scripts silently ignore when the MCP server is not running, so they are safe to leave in development builds.
## Console Log Capture (Details)
`debug_get_console_logs` captures logs from two sources:
### Scene Process Logs (automatic)
Console output from scene scripts (`console.log/warn/error` in the scene renderer process) is automatically captured. No setup required.
### Game Preview Logs (opt-in)
Game code runs in a browser during preview, which is a separate process. To capture game preview logs, your game code needs to send logs to the MCP server's `/log` endpoint.
**Setup:**
Add a console capture script to your game project:
```typescript
const MCP_LOG_URL = "http://127.0.0.1:3000/log";
const FLUSH_INTERVAL = 500;
let buffer: Array<{ timestamp: string; level: string; message: string }> = [];
function hook(level: string, original: (...args: any[]) => void) {
return function (...args: any[]) {
original.apply(console, args);
buffer.push({
timestamp: new Date().toISOString(),
level,
message: args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" "),
});
};
}
console.log = hook("log", console.log);
console.warn = hook("warn", console.warn);
console.error = hook("error", console.error);
setInterval(() => {
if (buffer.length === 0) return;
const entries = buffer.splice(0, 50);
fetch(MCP_LOG_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(entries),
}).catch(() => {}); // silently ignore if MCP server is not running
}, FLUSH_INTERVAL);
```
**`POST /log` format:**
```json
[
{ "timestamp": "2026-03-26T12:00:00.000Z", "level": "log", "message": "Hello" },
{ "timestamp": "2026-03-26T12:00:01.000Z", "level": "error", "message": "Something failed" }
]
```
Both scene and game logs are merged chronologically when retrieved via `debug_get_console_logs`. Each log entry includes a `source` field (`"scene"` or `"game"`) to distinguish the origin.
## Configuration
Settings are stored in `{project}/settings/cocos-creator-mcp.json`:
```json
{
"port": 3000,
"autoStart": true
}
```
| Option | Default | Description |
|--------|---------|-------------|
| `port` | `3000` | HTTP server port |
| `autoStart` | `false` | Start server automatically when extension loads |
## Testing
```bash
node test/regression.mjs # default port 3000
node test/regression.mjs 3001 # custom port
```
## Version History
- **v0.1** — MCP server + scene/node tools (13 tools)
- **v0.5** — Component, prefab, project, debug tools (27 tools)
- **v1.0** — Full tool coverage (145 tools, 13 categories, 224 test assertions)
- **v1.1** — Console log capture (scene process auto-capture + game preview via `/log` endpoint)
- **v1.2** — AI autonomous development: Preview in Editor, screenshot capture, game command control, code cache clear, scene save fix. Client scripts for game preview integration (`client/`)
- **v1.3** — `scene:set-property` for prefab save support, prefab_create overwrite guard, param alias (`component` → `componentType`)
- **v1.5** — `prefab_create_and_replace`, batch `set_property`, `prefab_open`
- **v1.6** — `debug_batch_screenshot`, widget support in `create_tree`, `component_query_enum`, `server_check_code_sync`
- **v1.8.0** — Preview Recorder panel: `debug_record_start` / `debug_record_stop` (MediaRecorder via canvas.captureStream, MP4/WebM, quality presets)
- **v1.8.1** — Fix: `component_set_property` cc.Asset references (cc.Font etc.) falling back to cc.Node when type is unspecified
- **v1.8.2** — Preview Recorder: screenshot button (webp/png toggle, max width), section-based UI layout
- **v1.9.0** — Preview Recorder auto-archive of old recordings + preflight "preview not running" check
- **v1.10.0** — `scene_create` asset-db fallback, stringified args preventive validation, test coverage expansion
- **v1.11.0** — HTTP MCP OAuth workaround (stdio bridge + dummy OAuth endpoints for Claude Code VSCode upstream bug) + dialog prevention for scene switching tools (`force` param, `ensureSceneSafeToSwitch`, `safeSaveScene`) + regression tests for both
- **v1.12.0** — Prefab authoring efficiency: `component_auto_bind` (auto-match `@property` fields to node names), `debug_wait_compile` (wait for TS compile to finish), `prefab_create_from_spec` (create node tree + auto-bind + prefab_create in one call)
- **v1.13.0** — `nodeName` parameter on component/get_components/auto_bind (no UUID required), `screenshot` auto-return option on `component_set_property` / `node_set_layout`, `node_set_layout` unified tool (UITransform + Widget + color/opacity in one call), dialog auto-response for untitled+dirty scenes, shared screenshot / node-resolve utilities
- **v1.14.0** — Widget `_alignFlags` auto-recalc bug fix: `setProperty` / `setProperties` / `node_set_layout` now re-query `isAlign*` values from scene and rebuild `_alignFlags` bitmask after isAlign updates (Editor bug where bitmask was not updated automatically, causing prefabs to save with `_alignFlags: 45` stuck state). Also `node_create` component addition now waits for editor reflection (`waitForComponent`) to fix flaky tests
## Development
```bash
npm run watch # Watch mode
npm run build # One-time build
```
After building, reload the extension in Cocos Creator:
- **Extension Manager** — disable then re-enable
- **Developer > Reload** — reloads main process
- **Full restart** — required for scene script or new category changes
## Requirements
- Cocos Creator 3.8+
- Node.js 18+
## Known Limitations
- **`scene_create`**: Does not work on Cocos Creator 3.8.x because the underlying `scene:new-scene` Editor message is not exposed on that version. As a workaround, create the `.scene` JSON file directly under `db://assets/` and call `project_refresh_assets` so the editor picks it up. See [#13](https://github.com/harady/cocos-creator-mcp/issues/13) for details.
## License
MIT
File diff suppressed because one or more lines are too long
@@ -1,91 +0,0 @@
/**
* McpConsoleCapture — ゲームプレビュー時のconsole.log/warn/errorをMCPサーバーに送信する。
*
* MCPサーバーの POST /log エンドポイントにバッチ送信する。
* MCPサーバー未起動時は黙って無視(本番影響なし)。
*
* Usage:
* import { initMcpConsoleCapture } from "./McpConsoleCapture";
* initMcpConsoleCapture(); // or initMcpConsoleCapture({ mcpBaseUrl: "http://127.0.0.1:3001" });
*/
export interface McpConsoleCaptureConfig {
/** MCPサーバーのベースURL (default: "http://127.0.0.1:3000") */
mcpBaseUrl?: string;
/** バッチ送信間隔ms (default: 500) */
flushInterval?: number;
/** 1回の送信最大件数 (default: 50) */
maxBatchSize?: number;
}
interface LogEntry {
timestamp: string;
level: "log" | "warn" | "error";
message: string;
}
let _buffer: LogEntry[] = [];
let _timer: any = null;
let _initialized = false;
let _config: Required<McpConsoleCaptureConfig>;
function formatArgs(args: any[]): string {
return args.map(a => {
if (typeof a === "string") return a;
if (a instanceof Error) return `${a.message}\n${a.stack || ""}`;
try { return JSON.stringify(a); } catch { return String(a); }
}).join(" ");
}
function flush(): void {
if (_buffer.length === 0) return;
const entries = _buffer.splice(0, _config.maxBatchSize);
fetch(`${_config.mcpBaseUrl}/log`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(entries),
}).catch(() => {});
}
/** ゲーム起動時に1回呼ぶ。console.log/warn/errorをフックしてMCPに送信開始 */
export function initMcpConsoleCapture(config?: McpConsoleCaptureConfig): void {
if (_initialized) return;
_initialized = true;
_config = {
mcpBaseUrl: config?.mcpBaseUrl ?? "http://127.0.0.1:3000",
flushInterval: config?.flushInterval ?? 500,
maxBatchSize: config?.maxBatchSize ?? 50,
};
const originalLog = console.log;
const originalWarn = console.warn;
const originalError = console.error;
function hook(level: LogEntry["level"], original: (...args: any[]) => void) {
return function (...args: any[]) {
original.apply(console, args);
_buffer.push({
timestamp: new Date().toISOString(),
level,
message: formatArgs(args),
});
};
}
console.log = hook("log", originalLog);
console.warn = hook("warn", originalWarn);
console.error = hook("error", originalError);
_timer = setInterval(flush, _config.flushInterval);
console.log("[McpConsoleCapture] initialized");
}
/** 停止 */
export function stopMcpConsoleCapture(): void {
if (_timer) {
clearInterval(_timer);
_timer = null;
}
flush();
_initialized = false;
}
File diff suppressed because one or more lines are too long
@@ -1,315 +0,0 @@
/**
* McpDebugClient — MCPサーバーとCocosCreatorプレビューの双方向通信
*
* MCPサーバーのコマンドキューをポーリングし、コマンドを実行して結果を返す。
* MCPサーバー未起動時は黙って無視(本番影響なし)。
*
* ビルトインコマンド:
* - screenshot: RenderTexture経由でゲーム画面キャプチャ
* - click: ノード名指定でクリックイベント発火
*
* カスタムコマンドを追加可能(プロジェクト固有の機能):
* initMcpDebugClient({
* customCommands: {
* state: () => ({ success: true, data: myDb.dump() }),
* navigate: async (args) => { await myRouter.go(args.page); return { success: true }; },
* },
* });
*/
import { director, Node, Button, Camera, RenderTexture, gfx } from "cc";
export interface McpDebugClientConfig {
/** MCPサーバーのベースURL (default: "http://127.0.0.1:3000") */
mcpBaseUrl?: string;
/** ポーリング間隔ms (default: 500) */
pollInterval?: number;
/** プロジェクト固有のコマンドハンドラー */
customCommands?: Record<string, (args: any) => any | Promise<any>>;
}
interface McpCommand {
id: string;
type: string;
args?: any;
}
let _polling = false;
let _timer: any = null;
let _config: Required<Pick<McpDebugClientConfig, "mcpBaseUrl" | "pollInterval">> & { customCommands: Record<string, (args: any) => any | Promise<any>> };
async function pollCommand(): Promise<void> {
if (_polling) return;
_polling = true;
try {
const res = await fetch(`${_config.mcpBaseUrl}/game/command`);
if (!res.ok) return;
const cmd: McpCommand | null = await res.json();
if (!cmd) return;
console.log(`[McpDebugClient] command: ${cmd.type}`, cmd.args || "");
const result = await executeCommand(cmd);
await fetch(`${_config.mcpBaseUrl}/game/result`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(result),
}).catch(() => {});
} catch {
// MCP server not running
} finally {
_polling = false;
}
}
async function executeCommand(cmd: McpCommand): Promise<any> {
try {
// ビルトインコマンド
switch (cmd.type) {
case "screenshot":
return { id: cmd.id, ...takeScreenshot() };
case "click":
return { id: cmd.id, ...clickNode(cmd.args?.name) };
case "record_start":
return { id: cmd.id, ...startRecording(cmd.args) };
case "record_stop":
return { id: cmd.id, ...(await stopRecording()) };
}
// カスタムコマンド
const handler = _config.customCommands[cmd.type];
if (handler) {
const result = await handler(cmd.args);
return { id: cmd.id, ...result };
}
return { id: cmd.id, success: false, error: `Unknown command: ${cmd.type}` };
} catch (e: any) {
return { id: cmd.id, success: false, error: e.message || String(e) };
}
}
// ─── ビルトインコマンド ───
function takeScreenshot(): { success: boolean; data?: any; error?: string } {
try {
const scene = director.getScene();
if (!scene) return { success: false, error: "No active scene" };
let camera: Camera | null = null;
const findCamera = (node: Node) => {
const cam = node.getComponent(Camera);
if (cam && cam.enabled) { camera = cam; return; }
for (const child of node.children) {
if (camera) return;
findCamera(child);
}
};
findCamera(scene);
if (!camera) return { success: false, error: "No camera found" };
const cam = camera as Camera;
const width = Math.floor(cam.camera.width);
const height = Math.floor(cam.camera.height);
const rt = new RenderTexture();
rt.reset({ width, height });
const prevTarget = cam.targetTexture;
cam.targetTexture = rt;
director.root!.frameMove(0);
cam.targetTexture = prevTarget;
const region = new gfx.BufferTextureCopy();
region.texOffset.x = 0;
region.texOffset.y = 0;
region.texExtent.width = width;
region.texExtent.height = height;
const buffer = new Uint8Array(width * height * 4);
const gfxTex = rt.getGFXTexture()!;
director.root!.device.copyTextureToBuffers(gfxTex, [buffer], [region]);
const cvs = document.createElement("canvas");
cvs.width = width;
cvs.height = height;
const ctx = cvs.getContext("2d")!;
const imageData = ctx.createImageData(width, height);
for (let y = 0; y < height; y++) {
const srcRow = (height - 1 - y) * width * 4;
const dstRow = y * width * 4;
for (let x = 0; x < width * 4; x++) {
imageData.data[dstRow + x] = buffer[srcRow + x];
}
}
ctx.putImageData(imageData, 0, 0);
const dataUrl = cvs.toDataURL("image/png");
rt.destroy();
return { success: true, data: { dataUrl, width, height } };
} catch (e: any) {
return { success: false, error: e.message };
}
}
function clickNode(name?: string): { success: boolean; error?: string } {
if (!name) return { success: false, error: "name argument required" };
const scene = director.getScene();
if (!scene) return { success: false, error: "No active scene" };
const found = findNodeByName(scene, name);
if (!found) return { success: false, error: `Node '${name}' not found` };
found.emit(Button.EventType.CLICK, found);
return { success: true };
}
// ─── 録画(MediaRecorder経由) ───
let _mediaRecorder: MediaRecorder | null = null;
let _recordChunks: Blob[] = [];
let _recordStream: MediaStream | null = null;
let _recordId: string | null = null;
const QUALITY_PRESETS: Record<string, number> = {
low: 0.15,
medium: 0.25,
high: 0.40,
ultra: 0.60,
};
function startRecording(args?: { fps?: number; videoBitsPerSecond?: number; quality?: string; coefficient?: number; format?: "webm" | "mp4" }): { success: boolean; error?: string; data?: any } {
// 前回の録画状態が残っていたら強制クリア
if (_mediaRecorder) {
try { if (_mediaRecorder.state !== "inactive") _mediaRecorder.stop(); } catch {}
_recordStream?.getTracks().forEach(t => { try { t.stop(); } catch {} });
_mediaRecorder = null;
_recordStream = null;
_recordChunks = [];
_recordId = null;
}
try {
// GameView canvas取得
const canvas = document.getElementById("GameCanvas") as HTMLCanvasElement
|| document.querySelector("canvas") as HTMLCanvasElement;
if (!canvas) return { success: false, error: "canvas not found" };
const fps = args?.fps ?? 30;
const quality = args?.quality ?? "medium";
const coef = args?.coefficient ?? QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.medium;
const autoBps = Math.round(canvas.width * canvas.height * fps * coef);
const bps = args?.videoBitsPerSecond ?? autoBps;
const format = args?.format ?? "mp4";
_recordStream = canvas.captureStream(fps);
_recordChunks = [];
_recordId = `rec_${new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").substring(0, 19)}`;
// 指定フォーマット優先、非対応ならwebmにfallback
const mp4Candidates = [
"video/mp4;codecs=h264",
"video/mp4;codecs=avc1.42E01E",
"video/mp4",
];
const webmCandidates = [
"video/webm;codecs=vp9",
"video/webm;codecs=vp8",
"video/webm",
];
const candidates = format === "mp4"
? [...mp4Candidates, ...webmCandidates]
: webmCandidates;
let mimeType = "";
for (const c of candidates) {
if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(c)) {
mimeType = c;
break;
}
}
if (!mimeType) return { success: false, error: "No supported MediaRecorder mimeType" };
_mediaRecorder = new MediaRecorder(_recordStream, { mimeType, videoBitsPerSecond: bps });
_mediaRecorder.ondataavailable = (e: BlobEvent) => {
if (e.data.size > 0) _recordChunks.push(e.data);
};
_mediaRecorder.start();
return { success: true, data: { id: _recordId, mimeType, fps, videoBitsPerSecond: bps, quality, canvasWidth: canvas.width, canvasHeight: canvas.height } };
} catch (e: any) {
_mediaRecorder = null;
_recordStream = null;
return { success: false, error: e.message || String(e) };
}
}
async function stopRecording(): Promise<{ success: boolean; error?: string; data?: any }> {
if (!_mediaRecorder || !_recordId) return { success: false, error: "not recording" };
const id = _recordId;
const mimeType = _mediaRecorder.mimeType;
return new Promise((resolve) => {
_mediaRecorder!.onstop = async () => {
try {
const blob = new Blob(_recordChunks, { type: mimeType });
_recordStream?.getTracks().forEach(t => t.stop());
_mediaRecorder = null;
_recordStream = null;
_recordChunks = [];
_recordId = null;
// Blob → base64 → POST /game/recording
const base64 = await blobToBase64(blob);
const uploadRes = await fetch(`${_config.mcpBaseUrl}/game/recording`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, base64, mimeType }),
});
const uploadData = await uploadRes.json();
resolve({ success: true, data: { id, size: blob.size, ...uploadData } });
} catch (e: any) {
resolve({ success: false, error: e.message || String(e) });
}
};
_mediaRecorder!.stop();
});
}
function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result as string;
// "data:video/webm;base64,XXX" → XXX だけ抽出
const commaIdx = result.indexOf(",");
resolve(commaIdx >= 0 ? result.substring(commaIdx + 1) : result);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
function findNodeByName(root: Node, name: string): Node | null {
if (root.name === name) return root;
for (const child of root.children) {
const found = findNodeByName(child, name);
if (found) return found;
}
return null;
}
// ─── 初期化・停止 ───
export function initMcpDebugClient(config?: McpDebugClientConfig): void {
if (_timer) return;
_config = {
mcpBaseUrl: config?.mcpBaseUrl ?? "http://127.0.0.1:3000",
pollInterval: config?.pollInterval ?? 500,
customCommands: config?.customCommands ?? {},
};
_timer = setInterval(pollCommand, _config.pollInterval);
console.log("[McpDebugClient] initialized");
}
export function stopMcpDebugClient(): void {
if (_timer) {
clearInterval(_timer);
_timer = null;
}
}
@@ -1,171 +0,0 @@
#!/usr/bin/env node
/**
* cocos-creator-mcp stdio bridge
*
* stdin で line-delimited JSON-RPC を読み取り、HTTP MCP エンドポイントに転送する。
* レスポンスは stdout に 1 行ずつ書き出す。
*
* Claude Code VSCode 拡張の HTTP MCP OAuth 強制バグ (#26917 等) を回避するための
* 代替トランスポート。ユーザーは .mcp.json で stdio 型として設定する:
*
* {
* "mcpServers": {
* "cocos-creator-mcp": {
* "command": "node",
* "args": [
* "C:/path/to/cocos-creator-mcp/client/stdio-bridge.js"
* ]
* }
* }
* }
*
* Optional 環境変数:
* COCOS_MCP_URL - MCP サーバーの URL (default: http://127.0.0.1:3000/mcp)
*/
"use strict";
const http = require("http");
const { URL } = require("url");
const readline = require("readline");
const DEFAULT_URL = "http://127.0.0.1:3000/mcp";
const serverUrl = new URL(process.env.COCOS_MCP_URL || DEFAULT_URL);
let sessionId = null;
function log(...args) {
// デバッグログは stderr に出す(stdout は JSON-RPC 応答専用)
console.error("[stdio-bridge]", ...args);
}
function postJsonRpc(body) {
return new Promise((resolve, reject) => {
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Content-Length": Buffer.byteLength(body),
};
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
const req = http.request(
{
hostname: serverUrl.hostname,
port: serverUrl.port || 80,
path: serverUrl.pathname,
method: "POST",
headers,
},
(res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
const responseSession = res.headers["mcp-session-id"];
if (responseSession && !sessionId) {
sessionId = responseSession;
log("session established:", sessionId);
}
const text = Buffer.concat(chunks).toString("utf8");
resolve({ status: res.statusCode || 0, body: text });
});
}
);
req.on("error", (err) => reject(err));
req.write(body);
req.end();
});
}
function writeResponse(obj) {
process.stdout.write(JSON.stringify(obj) + "\n");
}
async function handleLine(line) {
const trimmed = line.trim();
if (!trimmed) return;
let request;
try {
request = JSON.parse(trimmed);
} catch (e) {
writeResponse({
jsonrpc: "2.0",
id: null,
error: { code: -32700, message: "Parse error", data: e.message },
});
return;
}
try {
const { status, body } = await postJsonRpc(trimmed);
// Notifications (no id) may yield empty body
if (!body) {
if (request.id !== undefined) {
writeResponse({
jsonrpc: "2.0",
id: request.id,
error: { code: -32603, message: "Empty response from server", data: { status } },
});
}
return;
}
// Pass through server response (single JSON object per line)
try {
const parsed = JSON.parse(body);
writeResponse(parsed);
} catch {
// サーバーが SSE 等で返した場合のフォールバック
process.stdout.write(body + (body.endsWith("\n") ? "" : "\n"));
}
} catch (err) {
writeResponse({
jsonrpc: "2.0",
id: request.id !== undefined ? request.id : null,
error: {
code: -32603,
message: "HTTP bridge error",
data: err && err.message ? err.message : String(err),
},
});
}
}
let inflight = 0;
let stdinClosed = false;
function maybeExit() {
if (stdinClosed && inflight === 0) {
log("stdin closed, no inflight, exiting");
process.exit(0);
}
}
function main() {
log(`starting, target=${serverUrl.href}`);
const rl = readline.createInterface({
input: process.stdin,
terminal: false,
});
rl.on("line", (line) => {
inflight++;
handleLine(line)
.catch((e) => log("handler error:", e))
.finally(() => {
inflight--;
maybeExit();
});
});
rl.on("close", () => {
stdinClosed = true;
maybeExit();
});
process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));
}
main();
@@ -1,9 +0,0 @@
"use strict";
module.exports = {
description: "MCP (Model Context Protocol) server for Cocos Creator",
panel_title: "Cocos Creator MCP",
open_panel: "Open Panel",
recorder_title: "Preview Recorder",
open_recorder: "Open Recorder",
};
@@ -1,9 +0,0 @@
"use strict";
module.exports = {
description: "Cocos Creator用 MCP (Model Context Protocol) サーバー",
panel_title: "Cocos Creator MCP",
open_panel: "パネルを開く",
recorder_title: "Preview Recorder",
open_recorder: "Previewレコーダー",
};
@@ -1,9 +0,0 @@
"use strict";
module.exports = {
description: "Cocos Creator MCP (模型上下文协议) 服务器",
panel_title: "Cocos Creator MCP",
open_panel: "打开面板",
recorder_title: "Preview Recorder",
open_recorder: "打开录制器",
};
@@ -1,310 +0,0 @@
{
"name": "@iflow-mcp/harady-cocos-creator-mcp",
"version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@iflow-mcp/harady-cocos-creator-mcp",
"version": "1.14.0",
"dependencies": {
"vue": "^3.1.4",
"wasm-vips": "^0.0.17"
},
"bin": {
"iflow-mcp_harady-cocos-creator-mcp": "client/stdio-bridge.js"
},
"devDependencies": {
"@cocos/creator-types": "^3.8.6",
"@types/node": "^18.17.1",
"typescript": "^5.8.2"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dependencies": {
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@cocos/creator-types": {
"version": "3.8.7",
"resolved": "https://registry.npmmirror.com/@cocos/creator-types/-/creator-types-3.8.7.tgz",
"integrity": "sha512-QTtuC+w1s1yQ8DPlgFSiIGS+Pko6v2QdrrQRN8/SttEBo53JjyXEWHXpRkg1lGiHq0u/aadm0cN/Y4jtEmis/w==",
"dev": true
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
},
"node_modules/@types/node": {
"version": "18.19.130",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
"dependencies": {
"@babel/parser": "^7.29.7",
"@vue/shared": "3.5.39",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
"dependencies": {
"@vue/compiler-core": "3.5.39",
"@vue/shared": "3.5.39"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
"dependencies": {
"@babel/parser": "^7.29.7",
"@vue/compiler-core": "3.5.39",
"@vue/compiler-dom": "3.5.39",
"@vue/compiler-ssr": "3.5.39",
"@vue/shared": "3.5.39",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.15",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
"dependencies": {
"@vue/compiler-dom": "3.5.39",
"@vue/shared": "3.5.39"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.39.tgz",
"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
"dependencies": {
"@vue/shared": "3.5.39"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
"dependencies": {
"@vue/reactivity": "3.5.39",
"@vue/shared": "3.5.39"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
"dependencies": {
"@vue/reactivity": "3.5.39",
"@vue/runtime-core": "3.5.39",
"@vue/shared": "3.5.39",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
"dependencies": {
"@vue/compiler-ssr": "3.5.39",
"@vue/shared": "3.5.39"
},
"peerDependencies": {
"vue": "3.5.39"
}
},
"node_modules/@vue/shared": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz",
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA=="
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/nanoid": {
"version": "3.3.15",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz",
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/vue": {
"version": "3.5.39",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.39.tgz",
"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
"dependencies": {
"@vue/compiler-dom": "3.5.39",
"@vue/compiler-sfc": "3.5.39",
"@vue/runtime-dom": "3.5.39",
"@vue/server-renderer": "3.5.39",
"@vue/shared": "3.5.39"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/wasm-vips": {
"version": "0.0.17",
"resolved": "https://registry.npmmirror.com/wasm-vips/-/wasm-vips-0.0.17.tgz",
"integrity": "sha512-nhkqUNJDUymImoXGrVfImC4wzIFTb9KfBpAngb7dcEQNPP1gVTx4+WL3VVVDSXQpMsyeacsQDOx0+DM33Rpurg==",
"engines": {
"node": ">=16.4.0"
}
}
}
}
@@ -1,109 +0,0 @@
{
"package_version": 2,
"name": "cocos-creator-mcp",
"version": "1.14.0",
"author": "harady",
"editor": ">=3.8.0",
"scripts": {
"build": "tsc && node scripts/postbuild.js",
"watch": "tsc -w"
},
"description": "MCP Server for Cocos Creator [2ade3d2fa124]",
"main": "./dist/main.js",
"bin": {
"iflow-mcp_harady-cocos-creator-mcp": "client/stdio-bridge.js"
},
"dependencies": {
"vue": "^3.1.4",
"wasm-vips": "^0.0.17"
},
"devDependencies": {
"@cocos/creator-types": "^3.8.6",
"@types/node": "^18.17.1",
"typescript": "^5.8.2"
},
"panels": {
"default": {
"title": "i18n:cocos-creator-mcp.panel_title",
"type": "dockable",
"main": "dist/panels/default",
"size": {
"min-width": 350,
"min-height": 200,
"width": 450,
"height": 350
}
},
"recorder": {
"title": "i18n:cocos-creator-mcp.recorder_title",
"type": "dockable",
"main": "dist/panels/recorder",
"size": {
"min-width": 400,
"min-height": 400,
"width": 500,
"height": 600
}
}
},
"contributions": {
"menu": [
{
"path": "i18n:menu.extension/Cocos Creator MCP",
"label": "i18n:cocos-creator-mcp.open_panel",
"message": "open-panel"
},
{
"path": "i18n:menu.extension/Cocos Creator MCP",
"label": "i18n:cocos-creator-mcp.open_recorder",
"message": "open-recorder"
}
],
"messages": {
"open-panel": {
"methods": [
"openPanel"
]
},
"open-recorder": {
"methods": [
"openRecorder"
]
},
"start-server": {
"methods": [
"startServer"
]
},
"stop-server": {
"methods": [
"stopServer"
]
},
"get-server-status": {
"methods": [
"getServerStatus"
]
},
"update-port": {
"methods": [
"updatePort"
]
}
},
"scene": {
"script": "./dist/scene.js",
"methods": [
"getNodeInfo",
"getAllNodes",
"findNodesByName",
"setNodeProperty",
"setComponentProperty",
"getSceneHierarchy",
"moveNode",
"addComponentToNode",
"removeComponentFromNode"
]
}
}
}
@@ -1 +0,0 @@
@iflow-mcp/harady-cocos-creator-mcp
@@ -1,5 +0,0 @@
{
"push_platform": "github",
"fork_url": "https://github.com/iflow-mcp/harady-cocos-creator-mcp",
"fork_branch": "iflow"
}
@@ -1,98 +0,0 @@
const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
// dist/ 配下の全 .js ファイルの内容からハッシュを生成
// コードが同じなら常に同じ値になる
const distDir = path.join(__dirname, "..", "dist");
const hash = crypto.createHash("sha256");
// dist/ 配下を再帰的に走査して全 .js ファイルを収集
function collectJsFiles(dir, prefix = "") {
let files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
files = files.concat(collectJsFiles(path.join(dir, entry.name), relPath));
} else if (entry.name.endsWith(".js")) {
files.push(relPath);
}
}
return files;
}
const jsFiles = collectJsFiles(distDir).sort();
for (const file of jsFiles) {
const content = fs.readFileSync(path.join(distDir, file), "utf8");
// __BUILD_HASH__ プレースホルダーは除外して計算(自己参照を避ける)
hash.update(content.replace(/__BUILD_HASH__/g, ""));
}
const buildHash = hash.digest("hex").substring(0, 12);
// mcp-server.js にハッシュを埋め込む
const serverFile = path.join(distDir, "mcp-server.js");
const serverContent = fs.readFileSync(serverFile, "utf8");
fs.writeFileSync(serverFile, serverContent.replace("__BUILD_HASH__", buildHash));
// package.json の description にハッシュを付与
const pkgFile = path.join(__dirname, "..", "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
const baseDesc = pkg.description.replace(/\s*\[[a-f0-9]+\]$/, ""); // 既存ハッシュを除去
pkg.description = `${baseDesc} [${buildHash}]`;
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 4) + "\n");
console.log("BUILD_HASH:", buildHash);
// ── dist自動同期: ゲームプロジェクトの extensions/ にコピー ──
// SYNC_TARGET 環境変数 or settings ファイルで同期先を指定
const settingsFile = path.join(__dirname, "..", "sync-targets.json");
let syncTargets = [];
if (process.env.SYNC_TARGET) {
syncTargets = process.env.SYNC_TARGET.split(",").map(s => s.trim());
} else if (fs.existsSync(settingsFile)) {
syncTargets = JSON.parse(fs.readFileSync(settingsFile, "utf8")).targets || [];
}
if (syncTargets.length > 0) {
const rootDir = path.join(__dirname, "..");
const filesToSync = ["dist", "client", "i18n", "static", "package.json", "package-lock.json", "node_modules"];
for (const target of syncTargets) {
const targetDir = path.resolve(target);
if (!fs.existsSync(path.dirname(targetDir))) {
console.warn(`SYNC SKIP: parent dir not found: ${targetDir}`);
continue;
}
console.log(`SYNC: ${targetDir}`);
for (const entry of filesToSync) {
const src = path.join(rootDir, entry);
const dst = path.join(targetDir, entry);
if (!fs.existsSync(src)) continue;
if (fs.statSync(src).isDirectory()) {
copyDirSync(src, dst);
} else {
fs.mkdirSync(path.dirname(dst), { recursive: true });
fs.copyFileSync(src, dst);
}
}
console.log(`SYNC DONE: ${targetDir}`);
}
} else {
console.log("SYNC: no targets configured (set SYNC_TARGET or create sync-targets.json)");
}
function copyDirSync(src, dst) {
fs.mkdirSync(dst, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const srcPath = path.join(src, entry.name);
const dstPath = path.join(dst, entry.name);
if (entry.isDirectory()) {
copyDirSync(srcPath, dstPath);
} else {
fs.copyFileSync(srcPath, dstPath);
}
}
}
@@ -1,44 +0,0 @@
import fs from "fs";
import path from "path";
const HOURS_24 = 24 * 60 * 60 * 1000;
/**
* 24時間以上前のファイルを OLD_yyyyMM フォルダに移動する.
*/
export function archiveOldFiles(dir: string): void {
try {
let files: string[];
try {
files = fs.readdirSync(dir);
} catch {
return;
}
const now = Date.now();
let archived = 0;
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) continue;
if (now - stat.mtimeMs < HOURS_24) continue;
const fileDate = new Date(stat.mtimeMs);
const yyyy = fileDate.getFullYear();
const mm = String(fileDate.getMonth() + 1).padStart(2, "0");
const archiveDir = path.join(dir, `OLD_${yyyy}${mm}`);
fs.mkdirSync(archiveDir, { recursive: true });
fs.renameSync(filePath, path.join(archiveDir, file));
archived++;
}
if (archived > 0) {
console.log(`[cocos-creator-mcp] ${archived}件のファイルを月別フォルダにアーカイブ (${dir})`);
}
} catch (e) {
console.warn("[cocos-creator-mcp] アーカイブ失敗:", e);
}
}
@@ -1,137 +0,0 @@
import { McpServer } from "./mcp-server";
import { SceneTools } from "./tools/scene-tools";
import { NodeTools } from "./tools/node-tools";
import { ComponentTools } from "./tools/component-tools";
import { PrefabTools } from "./tools/prefab-tools";
import { ProjectTools } from "./tools/project-tools";
import { DebugTools } from "./tools/debug-tools";
import { SceneAdvancedTools } from "./tools/scene-advanced-tools";
import { SceneViewTools } from "./tools/scene-view-tools";
import { AssetTools } from "./tools/asset-tools";
import { PreferencesTools } from "./tools/preferences-tools";
import { ServerTools } from "./tools/server-tools";
import { BuilderTools } from "./tools/builder-tools";
import { ReferenceImageTools } from "./tools/reference-image-tools";
import { ServerConfig, DEFAULT_CONFIG } from "./types";
import path from "path";
import fs from "fs";
let server: McpServer | null = null;
function getSettingsPath(): string {
return path.join(Editor.Project.path, "settings", "cocos-creator-mcp.json");
}
function loadConfig(): ServerConfig {
try {
const p = getSettingsPath();
if (fs.existsSync(p)) {
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
return { ...DEFAULT_CONFIG, ...data };
}
} catch (e) {
console.warn("[cocos-creator-mcp] Failed to load settings, using defaults");
}
return { ...DEFAULT_CONFIG };
}
function saveConfig(config: ServerConfig): void {
try {
const p = getSettingsPath();
const dir = path.dirname(p);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(p, JSON.stringify(config, null, 2), "utf-8");
} catch (e) {
console.error("[cocos-creator-mcp] Failed to save settings:", e);
}
}
function createServer(config: ServerConfig): McpServer {
const s = new McpServer(config);
s.register(new SceneTools());
s.register(new NodeTools());
const componentTools = new ComponentTools();
s.register(componentTools);
s.register(new PrefabTools(componentTools));
s.register(new ProjectTools());
s.register(new DebugTools());
s.register(new SceneAdvancedTools());
s.register(new SceneViewTools());
s.register(new AssetTools());
s.register(new PreferencesTools());
s.register(new ServerTools());
s.register(new BuilderTools());
s.register(new ReferenceImageTools());
return s;
}
export const methods: Record<string, (...args: any[]) => any> = {
openPanel() {
Editor.Panel.open("cocos-creator-mcp");
},
openRecorder() {
Editor.Panel.open("cocos-creator-mcp.recorder");
},
async startServer() {
if (server?.isRunning) return { running: true, port: server.port };
const config = loadConfig();
server = createServer(config);
await server.start();
return { running: true, port: server.port };
},
async stopServer() {
if (server) {
await server.stop();
server = null;
}
return { running: false };
},
async updatePort(_port: number) {
const config = loadConfig();
config.port = _port;
saveConfig(config);
// Restart server if running
if (server?.isRunning) {
await server.stop();
server = createServer(config);
await server.start();
}
return { port: _port, running: server?.isRunning ?? false };
},
getServerStatus() {
return {
running: server?.isRunning ?? false,
port: server?.port ?? loadConfig().port,
toolCount: server?.getAllTools().length ?? 0,
toolNames: server?.getAllTools().map((t) => t.name).sort() ?? [],
};
},
};
export async function load() {
console.log("[cocos-creator-mcp] Extension loaded");
const config = loadConfig();
server = createServer(config);
if (config.autoStart) {
try {
await server.start();
console.log(`[cocos-creator-mcp] Auto-started on port ${config.port}`);
} catch (e) {
console.error("[cocos-creator-mcp] Auto-start failed:", e);
}
}
}
export async function unload() {
if (server) {
await server.stop();
server = null;
}
console.log("[cocos-creator-mcp] Extension unloaded");
}
@@ -1,542 +0,0 @@
import http from "http";
import { ToolCategory, ToolDefinition, JsonRpcRequest, JsonRpcResponse, ServerConfig, DEFAULT_CONFIG } from "./types";
import { archiveOldFiles } from "./archive";
const MCP_PROTOCOL_VERSION = "2024-11-05";
const SESSION_ID = `cocos-mcp-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
/** ビルド時にコードベースのSHA256ハッシュが埋め込まれる */
export const BUILD_HASH = "__BUILD_HASH__";
// ─── Game Preview Log Buffer ───
interface GameLogEntry {
timestamp: string;
level: "log" | "warn" | "error";
message: string;
}
const MAX_GAME_LOG_BUFFER = 500;
const _gameLogs: GameLogEntry[] = [];
/** Access game preview log buffer from debug-tools */
export function getGameLogs(count: number, level?: string): { logs: GameLogEntry[]; total: number } {
let logs = _gameLogs;
if (level) {
logs = logs.filter(l => l.level === level);
}
return { logs: logs.slice(-count), total: _gameLogs.length };
}
export function clearGameLogs(): void {
_gameLogs.length = 0;
}
// ─── Game Debug Command Queue ───
interface GameCommand {
id: string;
type: string;
args?: any;
timestamp: string;
}
interface GameCommandResult {
id: string;
success: boolean;
data?: any;
error?: string;
timestamp: string;
}
let _pendingCommand: GameCommand | null = null;
let _commandResult: GameCommandResult | null = null;
let _commandIdCounter = 0;
/** Queue a command for the game to execute */
export function queueGameCommand(type: string, args?: any): string {
const id = `cmd_${++_commandIdCounter}_${Date.now()}`;
_pendingCommand = { id, type, args, timestamp: new Date().toISOString() };
_commandResult = null;
return id;
}
/** Get the result of the last command (poll until available) */
export function getCommandResult(): GameCommandResult | null {
return _commandResult;
}
/** Clear command state */
export function clearCommandState(): void {
_pendingCommand = null;
_commandResult = null;
}
// ─── Recording Storage ───
interface RecordingInfo {
path: string;
size: number;
createdAt: string;
}
const _recordings = new Map<string, RecordingInfo>();
/** Get completed recording info by id */
export function getRecording(id: string): RecordingInfo | undefined {
return _recordings.get(id);
}
export function setRecording(id: string, info: RecordingInfo): void {
_recordings.set(id, info);
}
export class McpServer {
private server: http.Server | null = null;
private tools: Map<string, ToolCategory> = new Map();
private toolIndex: Map<string, ToolCategory> = new Map(); // toolName -> category
private config: ServerConfig;
constructor(config?: Partial<ServerConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
/** Register a tool category */
register(category: ToolCategory): void {
this.tools.set(category.categoryName, category);
for (const tool of category.getTools()) {
this.toolIndex.set(tool.name, category);
}
}
/** Get all tool definitions */
getAllTools(): ToolDefinition[] {
const all: ToolDefinition[] = [];
for (const cat of this.tools.values()) {
all.push(...cat.getTools());
}
return all;
}
/** Start HTTP server */
start(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.server) {
resolve();
return;
}
this.server = http.createServer((req, res) => this.handleRequest(req, res));
this.server.listen(this.config.port, "127.0.0.1", () => {
console.log(`[cocos-creator-mcp] Server started on http://127.0.0.1:${this.config.port}/mcp`);
resolve();
});
this.server.on("error", (e) => {
console.error(`[cocos-creator-mcp] Server error:`, e);
reject(e);
});
});
}
/** Stop HTTP server */
stop(): Promise<void> {
return new Promise((resolve) => {
if (!this.server) {
resolve();
return;
}
this.server.close(() => {
this.server = null;
console.log("[cocos-creator-mcp] Server stopped");
resolve();
});
});
}
get isRunning(): boolean {
return this.server !== null;
}
get port(): number {
return this.config.port;
}
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
// CORS
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
const url = req.url || "/";
const origin = `http://127.0.0.1:${this.config.port}`;
// ─── OAuth endpoints (MCP spec 2025-06-18 / RFC 9728 / RFC 8414 / RFC 7591) ───
//
// Claude Code の VSCode 拡張は HTTP トランスポートの MCP サーバーに対して
// 無条件で OAuth discovery / DCR を試みる (#26917 等の既知バグ)。
// cocos-creator-mcp は localhost-only のローカル開発ツールで本物の認証は不要だが、
// クライアントを満足させるため OAuth エンドポイント群をダミー実装して常時許可する。
//
// TODO: 以下のいずれかが発生したら削除する
// 1. anthropics/claude-code #26917 / #38102 等の HTTP OAuth バグが修正される
// 2. 本物の認証機構を実装する必要が出る(偽 OAuth と衝突するため)
// 3. MCP spec が PKCE 検証・トークンローテーション必須等に更新される
// 4. stdio ブリッジが十分定着して HTTP transport 自体を deprecate する
// RFC 9728 Protected Resource Metadata
if (url === "/.well-known/oauth-protected-resource" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
resource: `${origin}/mcp`,
authorization_servers: [origin],
bearer_methods_supported: ["header"],
scopes_supported: ["mcp"],
}));
return;
}
// RFC 8414 Authorization Server Metadata
if (url === "/.well-known/oauth-authorization-server" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
issuer: origin,
authorization_endpoint: `${origin}/oauth/authorize`,
token_endpoint: `${origin}/oauth/token`,
registration_endpoint: `${origin}/oauth/register`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256", "plain"],
token_endpoint_auth_methods_supported: ["none"],
scopes_supported: ["mcp"],
}));
return;
}
// RFC 7591 Dynamic Client Registration — accept anything, return dummy client
if (url === "/oauth/register" && req.method === "POST") {
const body = await readBody(req);
let reg: any = {};
try { reg = JSON.parse(body); } catch { /* ignore */ }
const clientId = `cocos-mcp-client-${Date.now()}`;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
client_id: clientId,
client_id_issued_at: Math.floor(Date.now() / 1000),
client_name: reg.client_name || "cocos-creator-mcp client",
redirect_uris: reg.redirect_uris || [],
token_endpoint_auth_method: "none",
grant_types: ["authorization_code"],
response_types: ["code"],
}));
return;
}
// OAuth authorization endpoint — auto-consent, redirect immediately with code
if (url.startsWith("/oauth/authorize") && req.method === "GET") {
const parsed = new URL(url, origin);
const redirectUri = parsed.searchParams.get("redirect_uri") || "";
const state = parsed.searchParams.get("state") || "";
if (!redirectUri) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "invalid_request", error_description: "redirect_uri required" }));
return;
}
const code = `cocos-mcp-code-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const location = `${redirectUri}${redirectUri.includes("?") ? "&" : "?"}code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
res.writeHead(302, { Location: location });
res.end();
return;
}
// OAuth token endpoint — always issue a dummy token
if (url === "/oauth/token" && req.method === "POST") {
await readBody(req); // drain
res.writeHead(200, {
"Content-Type": "application/json",
"Cache-Control": "no-store",
});
res.end(JSON.stringify({
access_token: "cocos-mcp-public-token",
token_type: "Bearer",
expires_in: 86400,
scope: "mcp",
}));
return;
}
// Health check
if (url === "/health" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", tools: this.getAllTools().length }));
return;
}
// Game debug command queue — game polls for commands
if (url === "/game/command" && req.method === "GET") {
const cmd = _pendingCommand;
_pendingCommand = null; // consume
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(cmd));
return;
}
// Game debug command result — game posts result
if (url === "/game/result" && req.method === "POST") {
const body = await readBody(req);
try {
_commandResult = JSON.parse(body);
} catch { /* ignore */ }
res.writeHead(204);
res.end();
return;
}
// Game preview recording receiver
if (url === "/game/recording" && req.method === "POST") {
const body = await readBody(req);
try {
const { id, base64, mimeType, savePath } = JSON.parse(body);
if (!id || !base64) throw new Error("id/base64 required");
const fs = require("fs");
const path = require("path");
const buffer = Buffer.from(base64, "base64");
// savePath指定があればそこに保存(絶対パスまたはプロジェクト相対パス)
const projectPath = (global as any).Editor?.Project?.path
|| process.cwd();
let dir: string;
if (savePath) {
dir = path.isAbsolute(savePath) ? savePath : path.join(projectPath, savePath);
} else {
dir = path.join(projectPath, "temp", "recordings");
}
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const mt = (mimeType || "").toLowerCase();
const ext = mt.includes("webm") ? "webm"
: mt.includes("mp4") ? "mp4"
: "bin";
const fileName = `${id}.${ext}`;
const filePath = path.join(dir, fileName);
fs.writeFileSync(filePath, buffer);
setRecording(id, {
path: filePath,
size: buffer.length,
createdAt: new Date().toISOString(),
});
if (this.config.autoArchiveRecordings) {
archiveOldFiles(dir);
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, path: filePath, size: buffer.length }));
} catch (e: any) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: e.message }));
}
return;
}
// Game preview log receiver
if (url === "/log" && req.method === "POST") {
const body = await readBody(req);
try {
const entries: GameLogEntry[] = JSON.parse(body);
for (const entry of (Array.isArray(entries) ? entries : [entries])) {
_gameLogs.push({
timestamp: entry.timestamp || new Date().toISOString(),
level: entry.level || "log",
message: entry.message || "",
});
// __debug_state__ ログから userId を debug-menu.json に保存
try {
const msg = JSON.parse(entry.message || "");
if (msg.__debug_state__ && msg.userId) {
const _fs = require("fs");
const _path = require("path");
const projectPath = (global as any).Editor?.Project?.path || process.cwd();
const settingsPath = _path.join(projectPath, "settings", "debug-menu.json");
_fs.writeFileSync(settingsPath, JSON.stringify({ userId: msg.userId }, null, 2), "utf-8");
}
} catch { /* not debug_state */ }
}
if (_gameLogs.length > MAX_GAME_LOG_BUFFER) {
_gameLogs.splice(0, _gameLogs.length - MAX_GAME_LOG_BUFFER);
}
} catch { /* ignore malformed */ }
res.writeHead(204);
res.end();
return;
}
// MCP endpoint
if (url === "/mcp") {
if (req.method === "GET") {
// SSE keepalive stream
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
// Send initial comment to keep connection alive
res.write(": connected\n\n");
return;
}
if (req.method === "POST") {
await this.handleMcpPost(req, res);
return;
}
if (req.method === "DELETE") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
}
// 404
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
}
private async handleMcpPost(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const body = await readBody(req);
let rpc: JsonRpcRequest;
try {
rpc = JSON.parse(body);
} catch {
this.sendJsonRpc(res, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
return;
}
const accept = req.headers["accept"] || "";
const wantSse = accept.includes("text/event-stream");
let response: JsonRpcResponse;
switch (rpc.method) {
case "initialize":
response = {
jsonrpc: "2.0",
id: rpc.id,
result: {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: {
name: "cocos-creator-mcp",
version: "1.0.0",
},
},
};
break;
case "notifications/initialized":
// No response needed for notification
res.writeHead(204, { "Mcp-Session-Id": SESSION_ID });
res.end();
return;
case "tools/list":
response = {
jsonrpc: "2.0",
id: rpc.id,
result: { tools: this.getAllTools() },
};
break;
case "tools/call": {
const toolName = rpc.params?.name;
const args = rpc.params?.arguments || {};
const category = this.toolIndex.get(toolName);
if (!category) {
response = {
jsonrpc: "2.0",
id: rpc.id,
error: { code: -32602, message: `Unknown tool: ${toolName}` },
};
} else {
try {
const start = Date.now();
console.log(`[cocos-creator-mcp] ▶ ${toolName}`, Object.keys(args).length > 0 ? JSON.stringify(args).substring(0, 200) : "");
const timeoutMs = (toolName.startsWith("prefab_") || toolName === "scene_open") ? 120000 : 30000;
const result = await withTimeout(category.execute(toolName, args), timeoutMs, `Tool ${toolName} timed out`);
console.log(`[cocos-creator-mcp] ✓ ${toolName} (${Date.now() - start}ms)`);
response = {
jsonrpc: "2.0",
id: rpc.id,
result,
};
} catch (e: any) {
console.error(`[cocos-creator-mcp] ✗ ${toolName}:`, e.message || String(e));
response = {
jsonrpc: "2.0",
id: rpc.id,
error: { code: -32603, message: e.message || String(e) },
};
}
}
break;
}
default:
response = {
jsonrpc: "2.0",
id: rpc.id,
error: { code: -32601, message: `Method not found: ${rpc.method}` },
};
}
if (wantSse) {
this.sendSse(res, [response]);
} else {
this.sendJsonRpc(res, response);
}
}
private sendJsonRpc(res: http.ServerResponse, data: JsonRpcResponse): void {
res.writeHead(200, {
"Content-Type": "application/json",
"Mcp-Session-Id": SESSION_ID,
});
res.end(JSON.stringify(data));
}
private sendSse(res: http.ServerResponse, messages: JsonRpcResponse[]): void {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Mcp-Session-Id": SESSION_ID,
});
for (const msg of messages) {
res.write(`event: message\ndata: ${JSON.stringify(msg)}\n\n`);
}
res.end();
}
}
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(message)), ms);
promise.then(
(v) => { clearTimeout(timer); resolve(v); },
(e) => { clearTimeout(timer); reject(e); },
);
});
}
function readBody(req: http.IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
req.on("error", reject);
});
}
@@ -1,36 +0,0 @@
/**
* Shared node UUID resolution — resolves nodeName to UUID via scene script.
*/
const EXT_NAME = "cocos-creator-mcp";
export interface ResolvedNode {
uuid: string;
name: string;
}
/**
* Resolve a node UUID from either `uuid` or `nodeName` parameter.
* If `uuid` is provided, it is returned as-is.
* If `nodeName` is provided, searches the scene for a matching node.
* Throws if neither is provided or no node is found.
*/
export async function resolveNodeUuid(args: { uuid?: string; nodeName?: string }): Promise<ResolvedNode> {
if (args.uuid) {
return { uuid: args.uuid, name: "" };
}
if (!args.nodeName) {
throw new Error("Either 'uuid' or 'nodeName' is required");
}
const result = await Editor.Message.request("scene", "execute-scene-script", {
name: EXT_NAME,
method: "findNodesByName",
args: [args.nodeName],
});
if (!result?.success || !result?.data?.length) {
throw new Error(`Node not found: "${args.nodeName}"`);
}
// 最初のマッチを使用
const node = result.data[0];
return { uuid: node.uuid, name: node.name || args.nodeName };
}
@@ -1,117 +0,0 @@
const { createApp } = require("vue");
const panelDataMap = new WeakMap<any, any>();
module.exports = Editor.Panel.define({
template: `
<div id="app">
<h2>Cocos Creator MCP</h2>
<div class="status">
<span>Status: <strong :class="running ? 'on' : 'off'">{{ running ? 'Running' : 'Stopped' }}</strong></span>
</div>
<div class="port-row">
<label>Port:</label>
<input type="number" v-model.number="editPort" :disabled="running" min="1024" max="65535" />
<ui-button v-if="!running && editPort !== port" @confirm="applyPort" class="small-btn">Apply</ui-button>
</div>
<div class="actions">
<ui-button v-if="!running" @confirm="start">Start Server</ui-button>
<ui-button v-if="running" @confirm="stop">Stop Server</ui-button>
</div>
<div v-if="running" class="info">
<p>Endpoint: <code>http://127.0.0.1:{{ port }}/mcp</code></p>
<p>Tools: <strong>{{ toolCount }}</strong></p>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
`,
style: `
#app { padding: 12px; font-family: sans-serif; color: #ccc; }
h2 { margin: 0 0 8px 0; font-size: 16px; }
.status { margin: 8px 0; }
.on { color: #4f4; }
.off { color: #f66; }
.port-row { margin: 8px 0; display: flex; align-items: center; gap: 8px; }
.port-row label { font-size: 12px; }
.port-row input { width: 80px; padding: 3px 6px; background: #222; color: #ccc; border: 1px solid #444; border-radius: 3px; font-size: 12px; }
.port-row input:disabled { opacity: 0.5; }
.actions { margin: 8px 0; }
.info { margin: 8px 0; padding: 8px; background: var(--color-normal-fill-emphasis); border-radius: 4px; }
.info p { margin: 4px 0; font-size: 12px; }
.info code { background: #333; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
.error { margin: 8px 0; color: #f66; font-size: 12px; }
`,
$: { app: "#app" },
ready() {
if (!this.$.app) return;
const app = createApp({
data() {
return {
running: false,
port: 3000,
editPort: 3000,
toolCount: 0,
error: "",
};
},
methods: {
async start(this: any) {
try {
this.error = "";
const result = await Editor.Message.request("cocos-creator-mcp", "start-server");
this.running = result.running;
this.port = result.port;
this.editPort = result.port;
await this.refresh();
} catch (e: any) {
this.error = e.message || String(e);
}
},
async stop(this: any) {
try {
await Editor.Message.request("cocos-creator-mcp", "stop-server");
this.running = false;
this.toolCount = 0;
} catch (e: any) {
this.error = e.message || String(e);
}
},
async applyPort(this: any) {
try {
this.error = "";
const result = await Editor.Message.request("cocos-creator-mcp", "update-port", this.editPort);
this.port = result.port;
this.running = result.running;
if (this.running) await this.refresh();
} catch (e: any) {
this.error = e.message || String(e);
}
},
async refresh(this: any) {
try {
const status = await Editor.Message.request("cocos-creator-mcp", "get-server-status");
this.running = status.running;
this.port = status.port;
this.editPort = status.port;
this.toolCount = status.toolCount;
} catch (e: any) {
console.warn("[cocos-creator-mcp] refresh failed:", e);
}
},
},
async mounted(this: any) {
try {
await this.refresh();
} catch (e) {
console.warn("[cocos-creator-mcp] mounted refresh failed:", e);
}
},
});
app.mount(this.$.app);
panelDataMap.set(this, app);
},
close() {
const app = panelDataMap.get(this);
if (app) app.unmount();
},
});
@@ -1,540 +0,0 @@
const { createApp: createAppRec } = require("vue");
const panelDataMapRec = new WeakMap<any, any>();
module.exports = Editor.Panel.define({
template: `
<div id="app">
<h2>Preview Recorder</h2>
<div class="controls">
<button v-if="!recording" @click="start" class="btn btn-start">● 録画開始</button>
<button v-else @click="stop" class="btn btn-stop" :disabled="stopping">■ 録画停止{{ stopping ? '中...' : '' }}</button>
<button @click="screenshot" class="btn btn-shot" :disabled="shooting">📸 スクショ{{ shooting ? '中...' : '' }}</button>
</div>
<div v-if="recording" class="status-row">
<span class="rec-dot">●</span> REC <strong>{{ elapsed }}s</strong>
<span class="info">{{ recordingInfo }}</span>
</div>
<div class="section-title">録画設定</div>
<div class="row">
<label>FPS:</label>
<input type="number" v-model.number="fps" :disabled="recording" min="10" max="60" />
<label>形式:</label>
<select v-model="format" :disabled="recording">
<option value="mp4">MP4</option>
<option value="webm">WebM</option>
</select>
</div>
<div class="row">
<label>品質:</label>
<select v-model="quality" @change="onQualityChange" :disabled="recording">
<option value="low">低</option>
<option value="medium">中</option>
<option value="high">高</option>
<option value="ultra">最高</option>
<option value="custom">カスタム</option>
</select>
<label title="ビットレート = 幅 × 高さ × FPS × 品質係数">品質係数:</label>
<input type="number" v-model.number="coefficient" @input="onCoefChange"
:disabled="recording" min="0.01" max="2" step="0.01" class="custom-bitrate"
title="ビットレート = 幅 × 高さ × FPS × 品質係数" />
<button @click="resetQuality" class="btn btn-small" :disabled="recording" title="録画設定を初期値に戻す">↺</button>
</div>
<div class="section-title">スクショ設定</div>
<div class="row">
<label>形式:</label>
<select v-model="shotFormat" :disabled="shooting">
<option value="png">PNG</option>
<option value="webp">WebP</option>
</select>
</div>
<div class="section-title">保存先</div>
<div class="row">
<input type="text" v-model="savePath" :disabled="recording" class="path-input" placeholder="temp/recordings" />
<button @click="selectSaveFolder" class="btn btn-small" :disabled="recording">📁 選択</button>
<button @click="resetSavePath" class="btn btn-small" :disabled="recording" title="保存先を初期値に戻す">↺</button>
</div>
<div class="row">
<button @click="openSaveFolder" class="btn btn-small">📂 保存フォルダを開く</button>
<label class="checkbox-label" title="ONにすると、録画/スクショ保存時に24時間以上前のファイルを OLD_yyyyMM フォルダに自動移動します"><input type="checkbox" v-model="autoArchive" @change="onAutoArchiveChange" /> 古いファイルを自動整理</label>
<button class="btn btn-help" @click="showArchiveHelp = !showArchiveHelp" title="自動整理の説明">?</button>
</div>
<div v-if="showArchiveHelp" class="help-box">
<strong>自動整理について</strong><br>
ONにすると、録画やスクショを保存するたびに、24時間以上前の古いファイルを<br>
<code>OLD_yyyyMM/</code> フォルダ(例: OLD_202604/)に自動で移動します。<br>
保存フォルダ直下には直近のファイルだけが残り、整理された状態を保てます。
</div>
<div v-if="lastResult" class="result" :class="lastError ? 'error' : 'success'">
<div v-if="!lastError">
<strong>✓ {{ lastResult.kind === 'shot' ? 'スクショ保存' : '録画完了' }}</strong><br>
<code>{{ lastResult.path }}</code><br>
{{ (lastResult.size / 1024).toFixed(1) }} KB
</div>
<div v-else>
<strong>✗ エラー:</strong> {{ lastResult.error || lastResult.message || 'unknown' }}
</div>
</div>
<div class="note">
※ ゲームプレビュー実行中に使ってください(GameDebugClientが必要)
</div>
</div>
`,
style: `
#app { padding: 16px; font-family: sans-serif; color: #ccc; font-size: 12px; }
h2 { margin: 0 0 12px 0; font-size: 18px; }
.controls { margin: 12px 0; }
.btn {
padding: 10px 20px;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
}
.btn:disabled { background: #555; color: #999; cursor: not-allowed; }
.btn-start { background: #d44; }
.btn-start:hover { background: #e55; }
.btn-stop { background: #888; }
.btn-stop:hover { background: #999; }
.btn-shot { background: #468; margin-left: 8px; }
.btn-shot:hover { background: #579; }
.section-title {
margin-top: 14px;
margin-bottom: 4px;
padding: 4px 0 3px 0;
font-size: 11px;
font-weight: bold;
color: #8af;
border-bottom: 1px solid #333;
}
.btn-small {
padding: 4px 10px;
background: #4a8;
font-size: 11px;
margin-left: 8px;
font-weight: normal;
}
.btn-small:hover { background: #5b9; }
.status-row { margin: 10px 0; font-size: 14px; }
.rec-dot { color: #f44; animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.3; } }
.info { color: #888; font-size: 11px; margin-left: 8px; }
.row { margin: 10px 0; display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
.row label { font-size: 12px; }
.row input { width: 60px; padding: 4px 8px; background: #222; color: #ccc; border: 1px solid #444; border-radius: 3px; }
.row select { padding: 4px 8px; background: #222; color: #ccc; border: 1px solid #444; border-radius: 3px; }
.path-input { flex: 1; min-width: 200px; width: auto !important; font-family: monospace; }
.custom-bitrate { width: 70px !important; }
.unit { font-size: 11px; color: #888; }
.result { margin: 12px 0; padding: 10px; border-radius: 4px; font-size: 12px; line-height: 1.5; }
.result.success { background: #1a3a1a; color: #afa; }
.result.error { background: #3a1a1a; color: #faa; }
.result code { font-size: 10px; word-break: break-all; background: #000; padding: 2px 4px; border-radius: 2px; }
.checkbox-label { font-size: 12px; display: flex; align-items: center; gap: 4px; margin-left: auto; cursor: pointer; }
.checkbox-label input[type="checkbox"] { cursor: pointer; }
.btn-help { padding: 2px 7px; background: #555; font-size: 11px; font-weight: bold; border-radius: 50%; min-width: 20px; margin-left: 4px; }
.btn-help:hover { background: #777; }
.help-box { margin: 8px 0; padding: 10px; background: #1a2a3a; border: 1px solid #345; border-radius: 4px; font-size: 11px; line-height: 1.6; color: #bcd; }
.help-box code { background: #000; padding: 1px 4px; border-radius: 2px; font-size: 10px; }
.note { margin-top: 16px; padding-top: 8px; border-top: 1px solid #333; font-size: 10px; color: #888; }
`,
$: { app: "#app" },
ready() {
if (!this.$.app) return;
const MCP_BASE = "http://127.0.0.1:3000";
const STORAGE_KEY = "cocos-mcp-recorder-settings";
const PERSISTED_KEYS = ["fps", "quality", "coefficient", "format", "savePath", "shotFormat"];
// localStorage から設定を読み込み
const loadSettings = () => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch { return {}; }
};
const saved = loadSettings();
// プロジェクト設定から autoArchiveRecordings を読み込み
const loadProjectConfig = () => {
try {
const fs = require("fs");
const path = require("path");
const p = path.join(Editor.Project.path, "settings", "cocos-creator-mcp.json");
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8"));
} catch { /* ignore */ }
return {};
};
const projectCfg = loadProjectConfig();
const app = createAppRec({
data() {
return {
recording: false,
stopping: false,
shooting: false,
elapsed: "0.0",
recordingInfo: "",
fps: saved.fps ?? 30,
quality: saved.quality ?? "medium",
coefficient: saved.coefficient ?? 0.25,
format: saved.format ?? "mp4",
savePath: saved.savePath ?? "temp/recordings",
shotFormat: saved.shotFormat ?? "png",
autoArchive: projectCfg.autoArchiveRecordings ?? false,
showArchiveHelp: false,
lastResult: null as any,
lastError: false,
_startTime: 0,
_timer: null as any,
_aliveCheckTimer: null as any,
};
},
watch: {
fps(this: any) { this.saveSettings(); },
quality(this: any) { this.saveSettings(); },
coefficient(this: any) { this.saveSettings(); },
format(this: any) { this.saveSettings(); },
savePath(this: any) { this.saveSettings(); },
shotFormat(this: any) { this.saveSettings(); },
},
methods: {
saveSettings(this: any) {
const data: any = {};
for (const key of PERSISTED_KEYS) data[key] = this[key];
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch { /* ignore */ }
},
async isPreviewRunning(this: any): Promise<boolean> {
try {
const res = await fetch(`${MCP_BASE}/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: 98, method: "tools/call",
params: {
name: "debug_game_command",
arguments: { type: "inspect", args: { name: "Canvas" }, timeout: 1500 },
},
}),
});
const json = await res.json();
const content = json.result?.content?.[0]?.text;
const parsed = content ? JSON.parse(content) : null;
return !!parsed?.success;
} catch {
return false;
}
},
async start(this: any) {
this.lastResult = null;
if (!await this.isPreviewRunning()) {
this.lastResult = { error: "ゲームプレビューが実行されていません。プレビューを開始してから録画してください。" };
this.lastError = true;
return;
}
try {
const res = await fetch(`${MCP_BASE}/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "debug_record_start",
arguments: {
fps: this.fps,
coefficient: this.coefficient,
format: this.format,
savePath: this.savePath,
},
},
}),
});
const json = await res.json();
const content = json.result?.content?.[0]?.text;
const parsed = content ? JSON.parse(content) : null;
if (parsed?.success && parsed.data?.id) {
this.recording = true;
const d = parsed.data;
const mbps = d?.videoBitsPerSecond ? (d.videoBitsPerSecond / 1_000_000).toFixed(2) : "?";
this.recordingInfo = `${d?.canvasWidth || "?"}x${d?.canvasHeight || "?"} @ ${d?.fps || "?"}fps / ${mbps}Mbps / ${d?.mimeType || ""}`;
this._startTime = Date.now();
this._timer = setInterval(() => {
this.elapsed = ((Date.now() - this._startTime) / 1000).toFixed(1);
}, 100);
// プレビュー停止検知用ポーリング(2秒毎)
this._aliveCheckTimer = setInterval(() => {
this.checkPreviewAlive();
}, 2000);
} else {
// 可能な限り詳細なエラー情報を表示
const errDetail = parsed?.data?.error
|| parsed?.error
|| (parsed?.data ? JSON.stringify(parsed.data) : null)
|| (parsed ? JSON.stringify(parsed).substring(0, 200) : "no response")
|| "録画開始失敗";
this.lastResult = { error: errDetail };
this.lastError = true;
}
} catch (e: any) {
this.lastResult = { error: `通信エラー: ${e.message}` };
this.lastError = true;
}
},
async stop(this: any) {
this.stopping = true;
try {
const res = await fetch(`${MCP_BASE}/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: {
name: "debug_record_stop",
arguments: { timeout: 60000 },
},
}),
});
const json = await res.json();
const content = json.result?.content?.[0]?.text;
const parsed = content ? JSON.parse(content) : null;
if (parsed?.success && parsed.data?.path) {
this.lastResult = { path: parsed.data.path, size: parsed.data.size };
this.lastError = false;
} else {
const errDetail = parsed?.data?.error
|| parsed?.error
|| (parsed?.data ? JSON.stringify(parsed.data) : null)
|| "録画停止失敗";
this.lastResult = { error: errDetail };
this.lastError = true;
}
} catch (e: any) {
this.lastResult = { error: `通信エラー: ${e.message}` };
this.lastError = true;
} finally {
this.recording = false;
this.stopping = false;
if (this._timer) { clearInterval(this._timer); this._timer = null; }
if (this._aliveCheckTimer) { clearInterval(this._aliveCheckTimer); this._aliveCheckTimer = null; }
}
},
onQualityChange(this: any) {
const map: Record<string, number> = { low: 0.15, medium: 0.25, high: 0.40, ultra: 0.60 };
if (this.quality !== "custom") this.coefficient = map[this.quality];
},
onCoefChange(this: any) {
const map: Record<number, string> = { 0.15: "low", 0.25: "medium", 0.40: "high", 0.60: "ultra" };
this.quality = map[this.coefficient] || "custom";
},
resetQuality(this: any) {
this.fps = 30;
this.quality = "medium";
this.coefficient = 0.25;
this.format = "mp4";
},
resetSavePath(this: any) {
this.savePath = "temp/recordings";
},
onAutoArchiveChange(this: any) {
try {
const fs = require("fs");
const path = require("path");
const p = path.join(Editor.Project.path, "settings", "cocos-creator-mcp.json");
let cfg: any = {};
if (fs.existsSync(p)) cfg = JSON.parse(fs.readFileSync(p, "utf-8"));
cfg.autoArchiveRecordings = this.autoArchive;
const dir = path.dirname(p);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(p, JSON.stringify(cfg, null, 2), "utf-8");
} catch (e) {
console.warn("[recorder] 設定保存失敗:", e);
}
},
async screenshot(this: any) {
this.shooting = true;
if (!await this.isPreviewRunning()) {
this.lastResult = { error: "ゲームプレビューが実行されていません。プレビューを開始してからスクショしてください。" };
this.lastError = true;
this.shooting = false;
return;
}
try {
const res = await fetch(`${MCP_BASE}/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: {
name: "debug_game_command",
arguments: { type: "screenshot", args: {}, timeout: 5000, maxWidth: 0, imageFormat: this.shotFormat },
},
}),
});
const json = await res.json();
const content = json.result?.content?.[0]?.text;
const parsed = content ? JSON.parse(content) : null;
if (parsed?.success && parsed.path) {
// savePath 配下にコピー
const fs = require("fs");
const path = require("path");
const projectPath = Editor.Project.path;
let destDir = this.savePath || "temp/recordings";
if (!path.isAbsolute(destDir)) destDir = path.join(projectPath, destDir);
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const ext = path.extname(parsed.path) || ".png";
const destPath = path.join(destDir, `screenshot_${ts}${ext}`);
fs.copyFileSync(parsed.path, destPath);
// 設定に応じて古いファイルをアーカイブ
try {
const settingsPath = path.join(projectPath, "settings", "cocos-creator-mcp.json");
if (fs.existsSync(settingsPath)) {
const cfg = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
if (cfg.autoArchiveRecordings) {
const { archiveOldFiles } = require("../../archive");
archiveOldFiles(destDir);
}
}
} catch { /* ignore */ }
this.lastResult = { kind: "shot", path: destPath, size: parsed.size };
this.lastError = false;
} else {
const errDetail = parsed?.error || parsed?.message || "スクショ失敗";
this.lastResult = { error: errDetail };
this.lastError = true;
}
} catch (e: any) {
this.lastResult = { error: `通信エラー: ${e.message}` };
this.lastError = true;
} finally {
this.shooting = false;
}
},
async checkPreviewAlive(this: any) {
if (!this.recording) return;
try {
// MCP経由で軽量なinspectコマンドを送る
const res = await fetch(`${MCP_BASE}/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: 99, method: "tools/call",
params: {
name: "debug_game_command",
arguments: { type: "inspect", args: { name: "Canvas" }, timeout: 1500 },
},
}),
});
const json = await res.json();
const content = json.result?.content?.[0]?.text;
const parsed = content ? JSON.parse(content) : null;
if (parsed?.success) {
// プレビュー生存
return;
}
// 応答なし → プレビュー停止とみなして録画停止状態に
console.warn("[PreviewRecorder] preview not responding, stopping recording state");
this.recording = false;
if (this._timer) { clearInterval(this._timer); this._timer = null; }
if (this._aliveCheckTimer) { clearInterval(this._aliveCheckTimer); this._aliveCheckTimer = null; }
this.lastResult = { error: "プレビューが停止したため録画を中断しました(動画は保存されません)" };
this.lastError = true;
} catch (e) {
// ネットワークエラーは通信エラーとして無視(一時的かもしれない)
}
},
async selectSaveFolder(this: any) {
try {
const result = await (Editor.Dialog as any).select({
title: "保存先フォルダを選択",
type: "directory",
multi: false,
});
if (result?.filePaths?.length) {
const path = require("path");
const projectPath = Editor.Project.path;
const absPath = result.filePaths[0];
// プロジェクト配下なら相対パスで保持
const relPath = path.relative(projectPath, absPath);
if (!relPath.startsWith("..") && !path.isAbsolute(relPath)) {
this.savePath = relPath.replace(/\\/g, "/");
} else {
this.savePath = absPath;
}
}
} catch (e: any) {
console.error("[PreviewRecorder] selectSaveFolder failed:", e);
}
},
openSaveFolder(this: any) {
const path = require("path");
const fs = require("fs");
const projectPath = Editor.Project.path;
let dir = this.savePath || "temp/recordings";
if (!path.isAbsolute(dir)) dir = path.join(projectPath, dir);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
try {
const { spawn } = require("child_process");
const platform = process.platform;
const [cmd, ...args] = platform === "win32"
? ["explorer.exe", dir.replace(/\//g, "\\")]
: platform === "darwin"
? ["open", dir]
: ["xdg-open", dir];
const p = spawn(cmd, args, { detached: true, stdio: "ignore" });
p.unref();
} catch (e: any) {
console.error("[PreviewRecorder] openSaveFolder failed:", e);
this.lastResult = { error: `フォルダを開けませんでした: ${e.message}` };
this.lastError = true;
}
},
openFolder(this: any) {
if (!this.lastResult?.path) return;
const filePath = this.lastResult.path;
try {
const { shell } = require("electron");
if (shell?.showItemInFolder) {
shell.showItemInFolder(filePath);
return;
}
} catch (e) { /* fallback */ }
// フォールバック: OS別コマンド
try {
const { exec } = require("child_process");
const platform = process.platform;
if (platform === "win32") {
exec(`explorer.exe /select,"${filePath.replace(/\//g, "\\")}"`);
} else if (platform === "darwin") {
exec(`open -R "${filePath}"`);
} else {
const dir = require("path").dirname(filePath);
exec(`xdg-open "${dir}"`);
}
} catch (e: any) {
console.error("[PreviewRecorder] openFolder failed:", e);
}
},
},
});
app.mount(this.$.app);
panelDataMapRec.set(this, app);
},
beforeClose() { },
close() {
const app = panelDataMapRec.get(this);
if (app) app.unmount();
panelDataMapRec.delete(this);
},
});
@@ -1,509 +0,0 @@
/**
* Scene script — runs inside CocosCreator's scene renderer process.
* These methods are called via Editor.Message.request('scene', 'execute-scene-script', ...)
* and have access to the `cc` module (engine runtime).
*/
import { join } from "path";
module.paths.push(join(Editor.App.path, "node_modules"));
// ─── Console Log Buffer ───
interface ConsoleLogEntry {
timestamp: string;
level: "log" | "warn" | "error";
message: string;
}
const MAX_LOG_BUFFER = 500;
const _consoleLogs: ConsoleLogEntry[] = [];
const _originalLog = console.log;
const _originalWarn = console.warn;
const _originalError = console.error;
function formatArgs(args: any[]): string {
return args.map(a => {
if (typeof a === "string") return a;
try { return JSON.stringify(a); } catch { return String(a); }
}).join(" ");
}
function pushLog(level: ConsoleLogEntry["level"], args: any[]): void {
_consoleLogs.push({
timestamp: new Date().toISOString(),
level,
message: formatArgs(args),
});
if (_consoleLogs.length > MAX_LOG_BUFFER) {
_consoleLogs.splice(0, _consoleLogs.length - MAX_LOG_BUFFER);
}
}
console.log = function (...args: any[]) {
_originalLog.apply(console, args);
pushLog("log", args);
};
console.warn = function (...args: any[]) {
_originalWarn.apply(console, args);
pushLog("warn", args);
};
console.error = function (...args: any[]) {
_originalError.apply(console, args);
pushLog("error", args);
};
function getScene() {
const { director } = require("cc");
return director.getScene();
}
function findNode(uuid: string) {
const scene = getScene();
if (!scene) return null;
// Recursive search — getChildByUuid is not recursive in cc
const queue = [...scene.children];
while (queue.length > 0) {
const node = queue.shift()!;
if (node.uuid === uuid) return node;
if (node.children) queue.push(...node.children);
}
return null;
}
/**
* Recursively build a node tree from a JSON spec.
* Returns { uuid, name, children: [...] }
*/
function buildNodeRecursive(parent: any, spec: any): any {
const { Node, js, Vec3 } = require("cc");
const node = new Node(spec.name || "Node");
parent.addChild(node);
// Add components
if (spec.components && Array.isArray(spec.components)) {
const { Sprite, Label } = require("cc");
for (const compName of spec.components) {
const CompClass = js.getClassByName(compName);
if (CompClass) {
if (!node.getComponent(CompClass)) {
const comp = node.addComponent(CompClass);
// Sprite: sizeMode=CUSTOM でUITransformサイズの上書きを防ぐ
if (comp instanceof Sprite) {
comp.sizeMode = 0; // SizeMode.CUSTOM
}
// Label: useSystemFont + 色を黒に(白パネル上で見えるように)
if (comp instanceof Label) {
comp.useSystemFont = true;
const { Color } = require("cc");
comp.color = new Color(51, 51, 51, 255);
}
}
}
}
}
// Set component properties
// Format: { "cc.UITransform.contentSize": {width:720, height:1280} }
if (spec.properties) {
for (const [key, value] of Object.entries(spec.properties)) {
const dotIdx = key.lastIndexOf(".");
if (dotIdx < 0) continue;
const compName = key.substring(0, dotIdx);
const propName = key.substring(dotIdx + 1);
const CompClass = js.getClassByName(compName);
if (!CompClass) continue;
const comp = node.getComponent(CompClass);
if (!comp) continue;
try {
// contentSize needs special handling (Size type)
if (propName === "contentSize" && value && typeof value === "object") {
comp.setContentSize((value as any).width ?? 0, (value as any).height ?? 0);
} else {
comp[propName] = value;
}
} catch { /* skip invalid property */ }
}
}
// Set UITransform anchorPoint if specified
if (spec.anchorPoint) {
const { UITransform } = require("cc");
const ut = node.getComponent(UITransform);
if (ut) ut.setAnchorPoint(spec.anchorPoint.x ?? 0.5, spec.anchorPoint.y ?? 0.5);
}
// Set node properties (position, scale, active)
if (spec.active === false) node.active = false;
if (spec.position) node.setPosition(spec.position.x || 0, spec.position.y || 0, spec.position.z || 0);
if (spec.scale) node.setScale(spec.scale.x ?? 1, spec.scale.y ?? 1, spec.scale.z ?? 1);
// Set Widget properties if specified
// Format: { top: 0, bottom: 0, left: 0, right: 0 } — each field enables the corresponding alignment
if (spec.widget) {
const { Widget } = require("cc");
let w = node.getComponent(Widget);
if (!w) w = node.addComponent(Widget);
const wSpec = spec.widget;
if (wSpec.top !== undefined) { w.isAlignTop = true; w.top = wSpec.top; }
if (wSpec.bottom !== undefined) { w.isAlignBottom = true; w.bottom = wSpec.bottom; }
if (wSpec.left !== undefined) { w.isAlignLeft = true; w.left = wSpec.left; }
if (wSpec.right !== undefined) { w.isAlignRight = true; w.right = wSpec.right; }
if (wSpec.horizontalCenter !== undefined) { w.isAlignHorizontalCenter = true; w.horizontalCenter = wSpec.horizontalCenter; }
if (wSpec.verticalCenter !== undefined) { w.isAlignVerticalCenter = true; w.verticalCenter = wSpec.verticalCenter; }
}
// Build children
const childResults: any[] = [];
if (spec.children && Array.isArray(spec.children)) {
for (const childSpec of spec.children) {
childResults.push(buildNodeRecursive(node, childSpec));
}
}
return { uuid: node.uuid, name: node.name, children: childResults };
}
function collectNodeInfo(node: any, includeComponents: boolean = false): any {
const info: any = {
uuid: node.uuid,
name: node.name,
active: node.active,
position: { x: node.position.x, y: node.position.y, z: node.position.z },
scale: { x: node.scale.x, y: node.scale.y, z: node.scale.z },
parent: node.parent?.uuid || null,
childCount: node.children?.length || 0,
};
if (includeComponents && node.components) {
info.components = node.components.map((c: any) => ({
type: c.constructor.name,
uuid: c.uuid,
enabled: c.enabled,
}));
}
return info;
}
export const methods: Record<string, (...args: any[]) => any> = {
getSceneHierarchy(includeComponents: boolean = false) {
try {
const scene = getScene();
if (!scene) return { success: false, error: "No active scene" };
const walk = (node: any): any => {
const item: any = {
uuid: node.uuid,
name: node.name,
active: node.active,
children: [],
};
if (includeComponents && node.components) {
item.components = node.components.map((c: any) => ({
type: c.constructor.name,
uuid: c.uuid,
enabled: c.enabled,
}));
}
if (node.children) {
item.children = node.children.map((ch: any) => walk(ch));
}
return item;
};
return {
success: true,
sceneName: scene.name,
sceneUuid: scene.uuid,
hierarchy: scene.children.map((ch: any) => walk(ch)),
};
} catch (e: any) {
return { success: false, error: e.message };
}
},
getNodeInfo(uuid: string) {
try {
const node = findNode(uuid);
if (!node) return { success: false, error: `Node ${uuid} not found` };
return { success: true, data: collectNodeInfo(node, true) };
} catch (e: any) {
return { success: false, error: e.message };
}
},
getAllNodes() {
try {
const scene = getScene();
if (!scene) return { success: false, error: "No active scene" };
const nodes: any[] = [];
const walk = (node: any) => {
nodes.push(collectNodeInfo(node));
if (node.children) node.children.forEach(walk);
};
scene.children.forEach(walk);
return { success: true, data: nodes };
} catch (e: any) {
return { success: false, error: e.message };
}
},
/**
* 指定ノードの子孫から名前で検索する。auto_bind 用。
* rootUuid が空の場合はシーン全体を検索(後方互換)。
*/
findDescendantsByName(rootUuid: string, name: string) {
try {
let root: any;
if (rootUuid) {
root = findNode(rootUuid);
if (!root) return { success: false, error: `Root node ${rootUuid} not found` };
} else {
root = getScene();
if (!root) return { success: false, error: "No active scene" };
}
const results: any[] = [];
const walk = (node: any) => {
if (node.name === name) results.push(collectNodeInfo(node));
if (node.children) node.children.forEach(walk);
};
if (root.children) root.children.forEach(walk);
return { success: true, data: results };
} catch (e: any) {
return { success: false, error: e.message };
}
},
/**
* 指定ノードの全子孫を depth 付きで返す。auto_bind の一括検索用。
*/
getAllDescendants(rootUuid: string) {
try {
const root = findNode(rootUuid);
if (!root) return { success: false, error: `Node ${rootUuid} not found` };
const results: Array<{uuid: string, name: string, depth: number}> = [];
const walk = (node: any, depth: number) => {
results.push({ uuid: node.uuid, name: node.name, depth });
if (node.children) node.children.forEach((c: any) => walk(c, depth + 1));
};
if (root.children) root.children.forEach((c: any) => walk(c, 1));
return { success: true, data: results };
} catch (e: any) {
return { success: false, error: e.message };
}
},
findNodesByName(name: string) {
try {
const scene = getScene();
if (!scene) return { success: false, error: "No active scene" };
const results: any[] = [];
const walk = (node: any) => {
if (node.name === name) results.push(collectNodeInfo(node));
if (node.children) node.children.forEach(walk);
};
scene.children.forEach(walk);
return { success: true, data: results };
} catch (e: any) {
return { success: false, error: e.message };
}
},
findNodeByPath(path: string) {
try {
const scene = getScene();
if (!scene) return { success: false, error: "No active scene" };
const parts = path.split("/");
let current: any = null;
const walk = (node: any): any => {
if (node.name === parts[0]) return node;
if (node.children) {
for (const child of node.children) {
const found = walk(child);
if (found) return found;
}
}
return null;
};
for (const child of scene.children) {
current = walk(child);
if (current) break;
}
if (!current) return { success: false, error: `Node "${parts[0]}" not found` };
for (let i = 1; i < parts.length; i++) {
const child = current.children?.find((c: any) => c.name === parts[i]);
if (!child) return { success: false, error: `Child "${parts[i]}" not found in "${current.name}"` };
current = child;
}
return { success: true, data: collectNodeInfo(current) };
} catch (e: any) {
return { success: false, error: e.message };
}
},
setNodeProperty(uuid: string, property: string, value: any) {
try {
const node = findNode(uuid);
if (!node) return { success: false, error: `Node ${uuid} not found` };
switch (property) {
case "position":
node.setPosition(value.x ?? 0, value.y ?? 0, value.z ?? 0);
break;
case "rotation":
node.setRotationFromEuler(value.x ?? 0, value.y ?? 0, value.z ?? 0);
break;
case "scale":
node.setScale(value.x ?? 1, value.y ?? 1, value.z ?? 1);
break;
case "active":
node.active = !!value;
break;
case "name":
node.name = String(value);
break;
default:
(node as any)[property] = value;
}
return { success: true };
} catch (e: any) {
return { success: false, error: e.message };
}
},
setComponentProperty(uuid: string, componentType: string, property: string, value: any) {
try {
const { js } = require("cc");
const node = findNode(uuid);
if (!node) return { success: false, error: `Node ${uuid} not found` };
const CompClass = js.getClassByName(componentType);
if (!CompClass) return { success: false, error: `Component class ${componentType} not found` };
const comp = node.getComponent(CompClass);
if (!comp) return { success: false, error: `Component ${componentType} not on node ${uuid}` };
comp[property] = value;
return { success: true };
} catch (e: any) {
return { success: false, error: e.message };
}
},
addComponentToNode(uuid: string, componentType: string) {
try {
const { js } = require("cc");
const node = findNode(uuid);
if (!node) return { success: false, error: `Node ${uuid} not found` };
const CompClass = js.getClassByName(componentType);
if (!CompClass) return { success: false, error: `Component class ${componentType} not found` };
const comp = node.addComponent(CompClass);
return { success: true, data: { uuid: comp.uuid, type: componentType } };
} catch (e: any) {
return { success: false, error: e.message };
}
},
moveNode(uuid: string, parentUuid: string) {
try {
const node = findNode(uuid);
if (!node) return { success: false, error: `Node ${uuid} not found` };
const parent = findNode(parentUuid);
if (!parent) return { success: false, error: `Parent ${parentUuid} not found` };
node.setParent(parent);
return { success: true };
} catch (e: any) {
return { success: false, error: e.message };
}
},
/**
* Build a node tree from a JSON spec in one call.
* Spec format:
* {
* name: string,
* components?: string[], // e.g. ["cc.UITransform", "cc.Layout"]
* properties?: Record<string, any>, // e.g. { "cc.UITransform.contentSize": {width:720,height:1280} }
* children?: NodeSpec[]
* }
*/
buildNodeTree(parentUuid: string, spec: any) {
try {
const { Node, js, UITransform } = require("cc");
const parent = findNode(parentUuid);
if (!parent) return { success: false, error: `Parent ${parentUuid} not found` };
const result = buildNodeRecursive(parent, spec);
return { success: true, data: result };
} catch (e: any) {
return { success: false, error: e.message };
}
},
testLog(message: string = "test message") {
console.log("[testLog]", message);
const methods = Object.keys(exports.methods || {});
return { success: true, bufferSize: _consoleLogs.length, methods };
},
getConsoleLogs(count: number = 50, level?: string) {
let logs = _consoleLogs;
if (level) {
logs = logs.filter(l => l.level === level);
}
return { success: true, logs: logs.slice(-count), total: _consoleLogs.length };
},
clearConsoleLogs() {
_consoleLogs.length = 0;
return { success: true };
},
async setPropertyViaEditor(nodeUuid: string, path: string, dump: any) {
try {
// scene:set-property API
// uuid: ノードUUID(コンポーネントUUIDではない)
// path: __comps__.{index}.{property} 形式
// dump: { value, type } 形式
const opts = { uuid: nodeUuid, path, dump };
const result = await (Editor as any).Message.request("scene", "set-property", opts);
return { success: true, result };
} catch (e: any) {
return { success: false, error: e.message || String(e) };
}
},
removeComponentFromNode(uuid: string, componentType: string) {
try {
const { js } = require("cc");
const node = findNode(uuid);
if (!node) return { success: false, error: `Node ${uuid} not found` };
const CompClass = js.getClassByName(componentType);
if (!CompClass) return { success: false, error: `Component class ${componentType} not found` };
const comp = node.getComponent(CompClass);
if (!comp) return { success: false, error: `Component ${componentType} not on node` };
node.removeComponent(comp);
return { success: true };
} catch (e: any) {
return { success: false, error: e.message };
}
},
};
@@ -1,100 +0,0 @@
/**
* Shared screenshot utility — used by debug-tools and other tools that need screenshots.
*/
export interface ProcessedImage {
buffer: Buffer;
width: number;
height: number;
format: string;
}
export interface ScreenshotResult {
success: boolean;
path: string;
size: number;
format: string;
originalSize: string;
savedSize: string;
}
export async function processImage(pngBuffer: Buffer, maxWidth: number, desiredFormat?: string): Promise<ProcessedImage> {
try {
const Vips = require("wasm-vips");
const vips = await Vips();
let image = vips.Image.newFromBuffer(pngBuffer);
if (maxWidth > 0 && image.width > maxWidth) {
image = image.thumbnailImage(maxWidth);
}
if (desiredFormat === "png") {
const pngOut = image.pngsaveBuffer();
const result = { buffer: Buffer.from(pngOut), width: image.width, height: image.height, format: "png" };
image.delete();
return result;
}
const outBuf = image.webpsaveBuffer({ Q: 85 });
const result = { buffer: Buffer.from(outBuf), width: image.width, height: image.height, format: "webp" };
image.delete();
return result;
} catch {
// Fallback: NativeImage resize + JPEG
const electron = require("electron");
let image = electron.nativeImage.createFromBuffer(pngBuffer);
if (maxWidth > 0) {
const size = image.getSize();
if (size.width > maxWidth) {
const ratio = maxWidth / size.width;
image = image.resize({ width: Math.round(size.width * ratio), height: Math.round(size.height * ratio) });
}
}
const size = image.getSize();
const buffer = image.toJPEG(85);
return { buffer, width: size.width, height: size.height, format: "jpeg" };
}
}
export async function takeEditorScreenshot(savePath?: string | undefined, maxWidth?: number): Promise<ScreenshotResult> {
const fs = require("fs");
const path = require("path");
const electron = require("electron");
const windows = electron.BrowserWindow.getAllWindows();
if (!windows || windows.length === 0) {
throw new Error("No editor window found");
}
// Find the main (largest) window
let win = windows[0];
let maxArea = 0;
for (const w of windows) {
const bounds = w.getBounds();
const area = bounds.width * bounds.height;
if (area > maxArea) {
maxArea = area;
win = w;
}
}
// Bring to front and wait for render
win.show();
await new Promise(r => setTimeout(r, 300));
const nativeImage = await win.webContents.capturePage();
const originalSize = nativeImage.getSize();
const pngBuffer = nativeImage.toPNG();
const effectiveMaxWidth = maxWidth !== undefined ? maxWidth : 960;
const { buffer, width, height, format } = await processImage(pngBuffer, effectiveMaxWidth);
const ext = format === "webp" ? "webp" : format === "jpeg" ? "jpg" : "png";
if (!savePath) {
const dir = path.join(Editor.Project.tmpDir, "screenshots");
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
savePath = path.join(dir, `screenshot_${timestamp}.${ext}`);
}
const finalPath = savePath!;
fs.writeFileSync(finalPath, buffer);
return {
success: true, path: finalPath, size: buffer.length, format,
originalSize: `${originalSize.width}x${originalSize.height}`,
savedSize: `${width}x${height}`,
};
}
@@ -1,38 +0,0 @@
import { ToolResult } from "./types";
/** Helper to create a successful text result */
export function ok(data: any): ToolResult {
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
/** Helper to create an error result */
export function err(message: string): ToolResult {
return {
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
isError: true,
};
}
/** Validate that a string looks like a CocosCreator UUID (not empty, reasonable format) */
export function validateUuid(uuid: string, label: string = "uuid"): string | null {
if (!uuid || typeof uuid !== "string") {
return `${label} is required`;
}
if (uuid.trim().length === 0) {
return `${label} cannot be empty`;
}
// CocosCreator UUIDs: either standard format (8-4-4-4-12) or compressed (22 chars with +/=)
// Be permissive — just reject obviously wrong values
if (uuid.length < 10) {
return `${label} "${uuid}" is too short to be a valid UUID`;
}
return null; // valid
}
/** Validate UUID and return err() if invalid, null if valid */
export function checkUuid(uuid: string, label: string = "uuid"): ToolResult | null {
const error = validateUuid(uuid, label);
return error ? err(error) : null;
}
@@ -1,290 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
export class AssetTools implements ToolCategory {
readonly categoryName = "asset";
getTools(): ToolDefinition[] {
return [
{
name: "asset_create",
description: "Create a new asset file in the project.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "db:// path for the new asset" },
content: { type: "string", description: "File content (for text-based assets)" },
},
required: ["path"],
},
},
{
name: "asset_delete",
description: "Delete an asset from the project.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "db:// path of the asset to delete" },
},
required: ["path"],
},
},
{
name: "asset_move",
description: "Move/rename an asset.",
inputSchema: {
type: "object",
properties: {
source: { type: "string", description: "Current db:// path" },
destination: { type: "string", description: "New db:// path" },
},
required: ["source", "destination"],
},
},
{
name: "asset_copy",
description: "Copy an asset to a new location.",
inputSchema: {
type: "object",
properties: {
source: { type: "string", description: "Source db:// path" },
destination: { type: "string", description: "Destination db:// path" },
},
required: ["source", "destination"],
},
},
{
name: "asset_save",
description: "Save an asset by UUID.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Asset UUID" },
},
required: ["uuid"],
},
},
{
name: "asset_reimport",
description: "Re-import an asset to refresh it.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "db:// path of the asset" },
},
required: ["path"],
},
},
{
name: "asset_query_path",
description: "Get the file path for an asset UUID.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Asset UUID" },
},
required: ["uuid"],
},
},
{
name: "asset_query_uuid",
description: "Get the UUID for an asset path.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "db:// path of the asset" },
},
required: ["path"],
},
},
{
name: "asset_query_url",
description: "Get the URL for an asset UUID.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Asset UUID" },
},
required: ["uuid"],
},
},
{
name: "asset_get_details",
description: "Get detailed metadata for an asset (type, size, importer, etc.).",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Asset UUID" },
},
required: ["uuid"],
},
},
{
name: "asset_get_dependencies",
description: "Get all assets that a given asset depends on.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Asset UUID" },
},
required: ["uuid"],
},
},
{
name: "asset_open_external",
description: "Open an asset in the system's default external editor.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "db:// path of the asset" },
},
required: ["path"],
},
},
{
name: "asset_import",
description: "Import an external file into the project assets.",
inputSchema: {
type: "object",
properties: {
source: { type: "string", description: "Source file path on disk" },
target: { type: "string", description: "Target db:// path in project" },
},
required: ["source", "target"],
},
},
{
name: "asset_save_meta",
description: "Save asset meta information (importer settings, etc.).",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Asset UUID" },
meta: { type: "string", description: "Meta JSON string" },
},
required: ["uuid", "meta"],
},
},
{
name: "asset_generate_available_url",
description: "Generate a non-conflicting asset URL (avoids name collisions).",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "Desired db:// path" },
},
required: ["url"],
},
},
{
name: "asset_query_ready",
description: "Check if the asset database is ready.",
inputSchema: { type: "object", properties: {} },
},
// ── 以下、既存MCP未対応のEditor API ──
{
name: "asset_query_users",
description: "Find all assets that use/reference a given asset.",
inputSchema: {
type: "object",
properties: { uuid: { type: "string", description: "Asset UUID" } },
required: ["uuid"],
},
},
{
name: "asset_query_missing",
description: "Check if an asset has missing references.",
inputSchema: {
type: "object",
properties: { uuid: { type: "string", description: "Asset UUID" } },
required: ["uuid"],
},
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "asset_create":
await (Editor.Message.request as any)("asset-db", "create-asset", args.path, args.content || null);
return ok({ success: true, path: args.path });
case "asset_delete":
await (Editor.Message.request as any)("asset-db", "delete-asset", args.path);
return ok({ success: true, path: args.path });
case "asset_move":
await (Editor.Message.request as any)("asset-db", "move-asset", args.source, args.destination);
return ok({ success: true, source: args.source, destination: args.destination });
case "asset_copy":
await (Editor.Message.request as any)("asset-db", "copy-asset", args.source, args.destination);
return ok({ success: true, source: args.source, destination: args.destination });
case "asset_save":
await (Editor.Message.request as any)("asset-db", "save-asset", args.uuid);
return ok({ success: true, uuid: args.uuid });
case "asset_reimport":
await (Editor.Message.request as any)("asset-db", "reimport-asset", args.path);
return ok({ success: true, path: args.path });
case "asset_query_path": {
const path = await (Editor.Message.request as any)("asset-db", "query-path", args.uuid);
return ok({ success: true, uuid: args.uuid, path });
}
case "asset_query_uuid": {
const uuid = await (Editor.Message.request as any)("asset-db", "query-uuid", args.path);
return ok({ success: true, path: args.path, uuid });
}
case "asset_query_url": {
const url = await (Editor.Message.request as any)("asset-db", "query-url", args.uuid);
return ok({ success: true, uuid: args.uuid, url });
}
case "asset_get_details": {
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", args.uuid);
const meta = await (Editor.Message.request as any)("asset-db", "query-asset-meta", args.uuid).catch(() => null);
return ok({ success: true, info, meta });
}
case "asset_get_dependencies": {
// Try multiple API names as they vary by CocosCreator version
let deps;
try {
deps = await (Editor.Message.request as any)("asset-db", "query-depends", args.uuid);
} catch {
try {
deps = await (Editor.Message.request as any)("asset-db", "query-asset-depends", args.uuid);
} catch {
deps = [];
}
}
return ok({ success: true, uuid: args.uuid, dependencies: deps });
}
case "asset_open_external":
await (Editor.Message.request as any)("asset-db", "open-asset", args.path);
return ok({ success: true, path: args.path });
case "asset_import":
await (Editor.Message.request as any)("asset-db", "import-asset", args.source, args.target);
return ok({ success: true, source: args.source, target: args.target });
case "asset_save_meta":
await (Editor.Message.request as any)("asset-db", "save-asset-meta", args.uuid, args.meta);
return ok({ success: true, uuid: args.uuid });
case "asset_generate_available_url": {
const url = await (Editor.Message.request as any)("asset-db", "generate-available-url", args.url);
return ok({ success: true, url });
}
case "asset_query_ready": {
const ready = await (Editor.Message.request as any)("asset-db", "query-ready");
return ok({ success: true, ready });
}
case "asset_query_users": {
const users = await (Editor.Message.request as any)("asset-db", "query-asset-users", args.uuid).catch(() => []);
return ok({ success: true, uuid: args.uuid, users });
}
case "asset_query_missing": {
const missing = await (Editor.Message.request as any)("asset-db", "query-missing-asset-info", args.uuid).catch(() => null);
return ok({ success: true, uuid: args.uuid, missing, hasMissing: !!missing });
}
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,64 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
export class BuilderTools implements ToolCategory {
readonly categoryName = "builder";
getTools(): ToolDefinition[] {
return [
{
name: "builder_open_panel",
description: "Open the Build panel in the editor.",
inputSchema: { type: "object", properties: {} },
},
{
name: "builder_get_settings",
description: "Get the current build settings/configuration.",
inputSchema: { type: "object", properties: {} },
},
{
name: "builder_query_tasks",
description: "Query active build tasks.",
inputSchema: { type: "object", properties: {} },
},
{
name: "builder_run_preview",
description: "Start the preview server for browser testing.",
inputSchema: { type: "object", properties: {} },
},
{
name: "builder_stop_preview",
description: "Stop the preview server.",
inputSchema: { type: "object", properties: {} },
},
];
}
async execute(toolName: string, _args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "builder_open_panel":
Editor.Panel.open("builder");
return ok({ success: true });
case "builder_get_settings": {
const settings = await (Editor.Message.request as any)("builder", "query-build-options").catch(() => null);
return ok({ success: true, settings });
}
case "builder_query_tasks": {
const tasks = await (Editor.Message.request as any)("builder", "query-tasks").catch(() => []);
return ok({ success: true, tasks });
}
case "builder_run_preview":
await (Editor.Message.request as any)("preview", "start");
return ok({ success: true, message: "Preview started" });
case "builder_stop_preview":
await (Editor.Message.request as any)("preview", "stop");
return ok({ success: true, message: "Preview stopped" });
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,687 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
import { parseMaybeJson } from "../utils";
import { resolveNodeUuid } from "../node-resolve";
import { takeEditorScreenshot } from "../screenshot";
const EXT_NAME = "cocos-creator-mcp";
export class ComponentTools implements ToolCategory {
readonly categoryName = "component";
getTools(): ToolDefinition[] {
return [
{
name: "component_add",
description: "Add a component to a node. Use cc.XXX format (e.g. 'cc.Label', 'cc.Sprite', 'cc.Button').",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
componentType: { type: "string", description: "Component class name (e.g. 'cc.Label')" },
},
required: ["uuid", "componentType"],
},
},
{
name: "component_remove",
description: "Remove a component from a node.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
componentType: { type: "string", description: "Component class name to remove" },
},
required: ["uuid", "componentType"],
},
},
{
name: "component_get_components",
description: "Get all components on a node with their properties.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
nodeName: { type: "string", description: "Node name to find (alternative to uuid)" },
},
},
},
{
name: "component_set_property",
description: "Set one or more properties on a component. For single: use property+value. For batch: use properties array. Use nodeName instead of uuid to find node by name. Set screenshot=true to capture editor screenshot after changes. Examples: Label.string, Label.fontSize, Sprite.color, UITransform.contentSize.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
nodeName: { type: "string", description: "Node name to find (alternative to uuid — avoids UUID lookup)" },
componentType: { type: "string", description: "Component class name (e.g. 'cc.Label')" },
property: { type: "string", description: "Property name (single mode)" },
value: { description: "Value to set (single mode)" },
properties: {
type: "array",
description: "Batch mode: array of {property, value} objects to set multiple properties at once",
items: {
type: "object",
properties: {
property: { type: "string", description: "Property name" },
value: { description: "Value to set" },
},
required: ["property", "value"],
},
},
screenshot: { type: "boolean", description: "If true, capture editor screenshot after setting properties and return the file path (default: false)" },
},
required: ["componentType"],
},
},
{
name: "component_get_info",
description: "Get detailed dump of a specific component by its UUID.",
inputSchema: {
type: "object",
properties: {
componentUuid: { type: "string", description: "Component UUID (not node UUID)" },
},
required: ["componentUuid"],
},
},
{
name: "component_get_available",
description: "List all available component classes that can be added to nodes.",
inputSchema: { type: "object", properties: {} },
},
{
name: "component_auto_bind",
description: "Automatically bind @property references by matching property names to descendant node names. Searches only descendants of the target node. Validates component type existence. Supports array properties (Slot_0, Slot_1...). Mode: 'fuzzy' (default) tries exact match first, then case-insensitive; 'strict' requires exact match only.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
nodeName: { type: "string", description: "Node name to find (alternative to uuid)" },
componentType: { type: "string", description: "Script component class name (e.g. 'QuestReadyPageView')" },
force: { type: "boolean", description: "If true, rebind even already-bound properties (default: false)" },
mode: { type: "string", enum: ["fuzzy", "strict"], description: "Matching mode: 'fuzzy' (default) or 'strict'" },
},
required: ["componentType"],
},
},
{
name: "component_query_enum",
description: "Get enum values for a component property. Useful for knowing what values Layout.type, Layout.resizeMode, etc. accept.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
componentType: { type: "string", description: "Component class (e.g. 'cc.Layout')" },
property: { type: "string", description: "Property name (e.g. 'type', 'resizeMode')" },
},
required: ["uuid", "componentType", "property"],
},
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
// パラメータエイリアス: component → componentType
const compType = args.componentType || args.component;
// nodeName → uuid 解決(対応ツールのみ)
const needsResolve = ["component_set_property", "component_get_components", "component_auto_bind"];
if (needsResolve.includes(toolName) && !args.uuid && args.nodeName) {
try {
const resolved = await resolveNodeUuid({ nodeName: args.nodeName });
args.uuid = resolved.uuid;
} catch (e: any) {
return err(e.message || String(e));
}
}
switch (toolName) {
case "component_add":
return this.addComponent(args.uuid, compType);
case "component_remove":
return this.removeComponent(args.uuid, compType);
case "component_get_components":
return this.getComponents(args.uuid);
case "component_set_property": {
const properties = parseMaybeJson(args.properties);
let result: ToolResult;
if (properties && Array.isArray(properties)) {
const parsed = properties.map((p: any) => ({ ...p, value: parseMaybeJson(p.value) }));
result = await this.setProperties(args.uuid, compType, parsed);
} else {
result = await this.setProperty(args.uuid, compType, args.property, parseMaybeJson(args.value));
}
// screenshot オプション
if (args.screenshot) {
try {
const ss = await takeEditorScreenshot();
const data = JSON.parse(result.content[0].text);
data.screenshot = { path: ss.path, size: ss.savedSize };
return ok(data);
} catch (ssErr: any) {
// スクショ失敗してもプロパティ設定結果は返す
const data = JSON.parse(result.content[0].text);
data.screenshotError = ssErr.message || String(ssErr);
return ok(data);
}
}
return result;
}
case "component_get_info": {
try {
const dump = await (Editor.Message.request as any)("scene", "query-component", args.componentUuid);
return ok({ success: true, component: dump });
} catch (e: any) { return err(e.message || String(e)); }
}
case "component_get_available": {
try {
const classes = await (Editor.Message.request as any)("scene", "query-classes");
return ok({ success: true, classes });
} catch (e: any) { return err(e.message || String(e)); }
}
case "component_auto_bind":
return this.autoBind(args.uuid, compType, args.force ?? false, args.mode ?? "fuzzy");
case "component_query_enum":
return this.queryEnum(args.uuid, compType, args.property);
default:
return err(`Unknown tool: ${toolName}`);
}
}
private async addComponent(uuid: string, componentType: string): Promise<ToolResult> {
try {
const result = await this.sceneScript("addComponentToNode", [uuid, componentType]);
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async queryEnum(nodeUuid: string, componentType: string, property: string): Promise<ToolResult> {
try {
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
if (!nodeDump) return err("Node not found");
const comps = nodeDump.__comps__ || [];
for (const comp of comps) {
const compType = comp.type;
if (!compType) continue;
// Match by cc.XXX format
const normalizedType = componentType.startsWith("cc.") ? componentType.substring(3) : componentType;
if (compType !== `cc.${normalizedType}` && compType !== componentType) continue;
const propDump = comp.value?.[property];
if (!propDump) return err(`Property '${property}' not found on ${componentType}`);
if (propDump.type !== "Enum") {
return ok({ success: true, property, type: propDump.type, note: "Not an enum property", currentValue: propDump.value });
}
return ok({
success: true,
property,
currentValue: propDump.value,
enumList: propDump.enumList,
});
}
return err(`Component ${componentType} not found on node`);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async removeComponent(uuid: string, componentType: string): Promise<ToolResult> {
try {
const result = await this.sceneScript("removeComponentFromNode", [uuid, componentType]);
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getComponents(uuid: string): Promise<ToolResult> {
try {
const result = await this.sceneScript("getNodeInfo", [uuid]);
if (!result.success) return ok(result);
return ok({
success: true,
uuid,
name: result.data?.name,
components: result.data?.components || [],
});
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* @property 名とノード名を自動マッチングしてバインドする。
*
* - 検索スコープ: 対象ノードの子孫のみ
* - 複数ヒット時: 階層の浅いノード(直接の子)を優先
* - 型検証: Component 参照型の場合、該当コンポーネントの存在を確認
* - 配列対応: @property([Node]) → 連番ノード名 (Slots_0, Slots_1...)
* - mode:
* - "fuzzy" (default): 完全一致 → case-insensitive → not_found+候補
* - "strict": 完全一致のみ → not_found+候補
*/
private async autoBind(nodeUuid: string, componentType: string, force: boolean, mode: string): Promise<ToolResult> {
try {
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
if (!nodeDump) return err("Node not found");
const comps = nodeDump.__comps__ || [];
const compName = componentType.replace("cc.", "");
const compIndex = comps.findIndex((c: any) => {
const t = c.type || "";
return t === compName || t === `cc.${compName}`;
});
if (compIndex < 0) return err(`Component ${componentType} not found on node`);
// 子孫ノード一覧を一括取得(検索効率化)
const allDescendants = await this.sceneScript("getAllDescendants", [nodeUuid]);
const descendantList: Array<{uuid: string, name: string, depth: number}> =
allDescendants?.success ? allDescendants.data : [];
const compDump = comps[compIndex];
const properties = compDump.value || {};
const skipKeys = new Set(["uuid", "name", "enabled", "node", "__scriptAsset", "__prefab", "_name", "_objFlags", "_enabled"]);
const results: any[] = [];
for (const [propName, propDumpRaw] of Object.entries(properties)) {
if (skipKeys.has(propName) || propName.startsWith("_")) continue;
const propDump = propDumpRaw as any;
const propType = propDump.type as string;
if (!propType) continue;
const extendsArr = (propDump.extends || []) as string[];
// 配列型の判定
const isArray = propType === "Array" || Array.isArray(propDump.value);
if (isArray) {
const arrayResult = await this.autoBindArray(nodeUuid, compIndex, propName, propDump, descendantList, mode);
results.push(arrayResult);
continue;
}
const isNodeRef = propType === "cc.Node";
const isComponentRef = extendsArr.includes("cc.Component");
if (!isNodeRef && !isComponentRef) continue;
// 既にバインド済みならスキップ
const currentValue = propDump.value;
if (!force && currentValue?.uuid) {
results.push({ property: propName, status: "already_bound" });
continue;
}
// 名前マッチ: 完全一致 → fuzzy時は case-insensitive
const matchResult = this.findMatchingNode(propName, descendantList, mode);
if (matchResult && isComponentRef) {
// 型検証: コンポーネントが存在するか
const hasComp = await this.nodeHasComponent(matchResult.uuid, propType);
if (!hasComp) {
results.push({ property: propName, type: propType, status: "type_mismatch",
nodeName: matchResult.name, message: `Node "${matchResult.name}" has no ${propType} component` });
continue;
}
}
if (!matchResult) {
// 候補サジェスト
const suggestions = this.getSuggestions(propName, descendantList);
results.push({ property: propName, type: propType, status: "not_found", suggestions });
continue;
}
const path = `__comps__.${compIndex}.${propName}`;
const dump = await this.buildDumpWithTypeInfo(nodeUuid, path, matchResult.uuid);
const setResult = await this.sceneScript("setPropertyViaEditor", [nodeUuid, path, dump]);
const status = matchResult.exact ? "bound" : "fuzzy_bound";
results.push({ property: propName, status, nodeName: matchResult.name, success: setResult?.success !== false });
}
const boundCount = results.filter(r => r.status === "bound" || r.status === "fuzzy_bound").length;
const fuzzyCount = results.filter(r => r.status === "fuzzy_bound").length;
const notFoundCount = results.filter(r => r.status === "not_found").length;
return ok({ success: true, boundCount, fuzzyCount, notFoundCount, results });
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* 子孫リストからプロパティ名にマッチするノードを検索。
* 完全一致を優先、fuzzy モードでは case-insensitive もフォールバック。
* 複数ヒット時は階層の浅い(depth が小さい)ものを優先。
*/
private findMatchingNode(
propName: string, descendants: Array<{uuid: string, name: string, depth: number}>, mode: string
): { uuid: string; name: string; exact: boolean } | null {
const candidates = this.propertyNameToNodeNames(propName);
// 1. 完全一致
for (const candidate of candidates) {
const matches = descendants
.filter(d => d.name === candidate)
.sort((a, b) => a.depth - b.depth);
if (matches.length > 0) {
return { uuid: matches[0].uuid, name: matches[0].name, exact: true };
}
}
// 2. fuzzy: case-insensitive
if (mode === "fuzzy") {
const lowerCandidates = candidates.map(c => c.toLowerCase());
const matches = descendants
.filter(d => lowerCandidates.includes(d.name.toLowerCase()))
.sort((a, b) => a.depth - b.depth);
if (matches.length > 0) {
return { uuid: matches[0].uuid, name: matches[0].name, exact: false };
}
}
return null;
}
/**
* not_found 時に似た名前のノードをサジェストする。
*/
private getSuggestions(propName: string, descendants: Array<{uuid: string, name: string, depth: number}>): string[] {
const lower = propName.toLowerCase();
return descendants
.filter(d => d.name.toLowerCase().includes(lower) || lower.includes(d.name.toLowerCase()))
.map(d => d.name)
.slice(0, 5);
}
/**
* ノードに指定型のコンポーネントが存在するか確認。
*/
private async nodeHasComponent(nodeUuid: string, propType: string): Promise<boolean> {
const typeName = propType.replace("cc.", "");
const info = await this.sceneScript("getNodeInfo", [nodeUuid]);
if (!info?.success || !info?.data?.components) return false;
return info.data.components.some((c: any) => c.type === typeName);
}
/**
* 配列 @property の自動バインド。
* プロパティ名 "slots" → "Slots_0", "Slots_1", ... の連番ノードを検索。
*/
private async autoBindArray(
nodeUuid: string, compIndex: number, propName: string, propDump: any,
descendants: Array<{uuid: string, name: string, depth: number}>, mode: string
): Promise<any> {
const elementType = propDump.value?.[0]?.type as string | undefined;
if (!elementType) {
return { property: propName, status: "skip", reason: "empty array or unknown element type" };
}
const pascal = propName.charAt(0).toUpperCase() + propName.slice(1);
const foundElements: any[] = [];
let index = 0;
while (true) {
const candidateName = `${pascal}_${index}`;
// 完全一致 or case-insensitive
let match = descendants.find(d => d.name === candidateName);
if (!match && mode === "fuzzy") {
const lower = candidateName.toLowerCase();
match = descendants.find(d => d.name.toLowerCase() === lower);
}
if (!match) break;
const elementPath = `__comps__.${compIndex}.${propName}.${index}`;
const dump = await this.buildDumpWithTypeInfo(nodeUuid, elementPath, match.uuid);
const setResult = await this.sceneScript("setPropertyViaEditor", [nodeUuid, elementPath, dump]);
const exact = match.name === candidateName;
foundElements.push({ index, nodeName: match.name, exact, success: setResult?.success !== false });
index++;
}
if (foundElements.length === 0) {
return { property: propName, status: "not_found", type: "Array", candidates: [`${pascal}_0`, `${pascal}_1`, "..."] };
}
const hasFuzzy = foundElements.some(e => !e.exact);
return { property: propName, status: hasFuzzy ? "fuzzy_bound" : "bound", type: "Array", count: foundElements.length, elements: foundElements };
}
/**
* camelCase プロパティ名からノード名の候補を生成。
* closeButton → ["CloseButton", "closeButton"]
*/
private propertyNameToNodeNames(propName: string): string[] {
const pascal = propName.charAt(0).toUpperCase() + propName.slice(1);
const names = [pascal];
if (pascal !== propName) names.push(propName);
return names;
}
private async setProperty(uuid: string, componentType: string, property: string, value: any): Promise<ToolResult> {
try {
// コンポーネントのインデックスを取得
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
if (!nodeInfo?.success || !nodeInfo?.data?.components) {
return err(`Node ${uuid} not found or has no components`);
}
const compName = componentType.replace("cc.", "");
const compIndex = nodeInfo.data.components.findIndex((c: any) => c.type === compName);
if (compIndex < 0) {
return err(`Component ${componentType} not found on node ${uuid}`);
}
// scene:set-property でプロパティ変更(Prefab保存時にも反映される)
// パス形式: __comps__.{index}.{property}
const path = `__comps__.${compIndex}.${property}`;
// プロパティの型情報をquery-nodeから取得して、適切なdump形式を構築
const dump = await this.buildDumpWithTypeInfo(uuid, path, value);
const result = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
// cc.Widget の isAlign* 設定後は _alignFlags を再計算する
// (Editor が isAlign* 変更時に _alignFlags を自動更新しないバグの対処)
if (componentType === "cc.Widget" && property.startsWith("isAlign")) {
await this.recalcWidgetAlignFlags(uuid, compIndex);
}
return ok({ success: true, path, dump, result });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async setProperties(uuid: string, componentType: string, properties: Array<{property: string, value: any}>): Promise<ToolResult> {
try {
if (!componentType) return err("componentType is required");
if (!properties.length) return err("properties array is empty");
// コンポーネントのインデックスを取得(1回だけ)
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
if (!nodeInfo?.success || !nodeInfo?.data?.components) {
return err(`Node ${uuid} not found or has no components`);
}
const compName = componentType.replace("cc.", "");
const compIndex = nodeInfo.data.components.findIndex((c: any) => c.type === compName);
if (compIndex < 0) {
return err(`Component ${componentType} not found on node ${uuid}`);
}
const results: any[] = [];
for (const { property, value } of properties) {
const path = `__comps__.${compIndex}.${property}`;
const dump = await this.buildDumpWithTypeInfo(uuid, path, value);
const result = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
results.push({ property, success: result?.success !== false, path });
}
const allOk = results.every(r => r.success);
// cc.Widget の isAlign* 設定後は _alignFlags を再計算する
// (Editor が isAlign* 変更時に _alignFlags を自動更新しないバグの対処)
if (componentType === "cc.Widget" && properties.some(p => p.property.startsWith("isAlign"))) {
await this.recalcWidgetAlignFlags(uuid, compIndex);
}
return ok({ success: allOk, results });
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* cc.Widget の isAlign* プロパティ現在値から _alignFlags ビットマスクを再計算して設定する。
*
* CocosCreator Editor は isAlign* を setPropertyViaEditor で変更しても
* _alignFlags を自動更新しないバグがある。このヘルパーで明示的に同期する。
*
* _alignFlags ビット定義:
* isAlignLeft=1, isAlignRight=2, isAlignTop=4, isAlignBottom=8,
* isAlignHorizontalCenter=16, isAlignVerticalCenter=32
*/
private async recalcWidgetAlignFlags(uuid: string, wIdx: number): Promise<void> {
const ALIGN_BITS: Record<string, number> = {
isAlignLeft: 1, isAlignRight: 2, isAlignTop: 4, isAlignBottom: 8,
isAlignHorizontalCenter: 16, isAlignVerticalCenter: 32,
};
try {
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", uuid);
if (!nodeDump) return;
const wCompDump = nodeDump.__comps__?.[wIdx];
if (!wCompDump) return;
let alignFlags = 0;
for (const [key, bit] of Object.entries(ALIGN_BITS)) {
if (wCompDump.value?.[key]?.value === true) alignFlags |= bit;
}
const path = `__comps__.${wIdx}._alignFlags`;
await this.sceneScript("setPropertyViaEditor", [uuid, path, { value: alignFlags, type: "Number" }]);
} catch (_e) {
// _alignFlags 再計算の失敗は致命的でないため無視
}
}
/**
* プロパティの型情報をEditor APIから取得し、適切なdump形式を構築する。
*
* UUID文字列が渡された場合、プロパティの型に応じて:
* - Node/Component参照型 → {type: propType, value: {uuid: nodeUuid}}
* - Asset参照型(cc.Prefab等) → {type: propType, value: {uuid: assetUuid}}
* - String型 → {value, type: "String"}
*/
private async buildDumpWithTypeInfo(nodeUuid: string, path: string, value: any): Promise<any> {
// プリミティブ型はそのまま
if (typeof value === "number") return { value, type: "Number" };
if (typeof value === "boolean") return { value, type: "Boolean" };
// オブジェクト形式 {uuid: "xxx", type: "cc.Node"} はそのまま
// type 指定なしの {uuid: "xxx"} はプロパティの実際の型を解決するため文字列扱いに変換する
if (value !== null && typeof value === "object" && typeof value.uuid === "string") {
if (typeof value.type === "string") {
return { type: value.type, value: { uuid: value.uuid } };
}
// type 未指定: 文字列として処理してプロパティ型から解決
value = value.uuid;
}
// @path: プレフィックスの場合: パスからノードUUIDを解決
if (typeof value === "string" && value.startsWith("@path:")) {
const nodePath = value.slice(6);
const result = await this.sceneScript("findNodeByPath", [nodePath]);
if (result?.success && result.data?.uuid) {
value = result.data.uuid;
} else {
throw new Error(`Node not found at path: ${nodePath}`);
}
}
// 文字列の場合: プロパティの型情報を取得して判定
if (typeof value === "string") {
try {
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
if (nodeDump) {
const propDump = this.resolveDumpPath(nodeDump, path);
if (propDump?.type) {
const propType = propDump.type as string;
const extendsArr = (propDump.extends || []) as string[];
const isComponentRef = extendsArr.includes("cc.Component");
const isNodeRef = propType === "cc.Node";
const isAssetRef = extendsArr.includes("cc.Asset");
if (isComponentRef) {
// コンポーネント参照: ノードUUIDからコンポーネントUUIDを解決
const compUuid = await this.resolveComponentUuid(value, propType);
return { type: propType, value: { uuid: compUuid || value } };
}
if (isNodeRef) {
return { type: propType, value: { uuid: value } };
}
if (isAssetRef) {
return { type: propType, value: { uuid: value } };
}
}
}
} catch (_e) {
// query-node失敗時はフォールバック
}
return { value, type: "String" };
}
// その他のオブジェクト(contentSize, color等の構造体)
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
const wrapped: any = {};
for (const [k, v] of Object.entries(value)) {
wrapped[k] = { value: v };
}
return { value: wrapped };
}
return { value };
}
/**
* query-nodeのdumpからドットパスでプロパティを解決する。
* 例: "__comps__.2.scrollView" → nodeDump.__comps__[2].value.scrollView
*/
private resolveDumpPath(nodeDump: any, path: string): any {
const parts = path.split(".");
let current = nodeDump;
for (const part of parts) {
if (!current) return null;
if (part === "__comps__") {
current = current.__comps__;
} else if (/^\d+$/.test(part)) {
current = current[parseInt(part)]?.value;
} else {
current = current?.[part];
}
}
return current;
}
/**
* ノードUUIDからコンポーネントUUIDを解決する。
* propType(例: "cc.ScrollView", "MissionListPanel")に一致するコンポーネントを探す。
*/
private async resolveComponentUuid(nodeUuid: string, propType: string): Promise<string | null> {
try {
const nodeInfo = await this.sceneScript("getNodeInfo", [nodeUuid]);
if (!nodeInfo?.success || !nodeInfo?.data?.components) return null;
const typeName = propType.replace("cc.", "");
const comp = nodeInfo.data.components.find((c: any) => c.type === typeName);
return comp?.uuid || null;
} catch (_e) {
return null;
}
}
private async sceneScript(method: string, args: any[]): Promise<any> {
return Editor.Message.request("scene", "execute-scene-script", {
name: EXT_NAME,
method,
args,
});
}
}
@@ -1,804 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
import { getGameLogs, clearGameLogs, queueGameCommand, getCommandResult } from "../mcp-server";
import { parseMaybeJson } from "../utils";
import { ensureSceneSafeToSwitch } from "./scene-tools";
import { processImage, takeEditorScreenshot } from "../screenshot";
export class DebugTools implements ToolCategory {
readonly categoryName = "debug";
getTools(): ToolDefinition[] {
return [
{
name: "debug_get_editor_info",
description: "Get Cocos Creator editor information (version, platform, language).",
inputSchema: { type: "object", properties: {} },
},
{
name: "debug_list_messages",
description: "List available Editor messages for a given extension or built-in module.",
inputSchema: {
type: "object",
properties: {
target: { type: "string", description: "Message target (e.g. 'scene', 'asset-db', 'extension')" },
},
required: ["target"],
},
},
{
name: "debug_execute_script",
description: "Execute a custom scene script method. The method must be registered in scene.ts.",
inputSchema: {
type: "object",
properties: {
method: { type: "string", description: "Method name from scene.ts" },
args: { type: "array", description: "Arguments to pass", items: {} },
},
required: ["method"],
},
},
{
name: "debug_get_console_logs",
description: "Get recent console log entries. Automatically captures scene process logs (console.log/warn/error in scene scripts). Game preview logs can also be captured by sending POST requests to /log endpoint — see README for setup.",
inputSchema: {
type: "object",
properties: {
count: { type: "number", description: "Max number of entries (default 50)" },
level: { type: "string", description: "Filter by level: 'log', 'warn', or 'error'" },
source: { type: "string", description: "Filter by source: 'scene' or 'game'. Returns both if omitted." },
},
},
},
{
name: "debug_clear_console",
description: "Clear the editor console.",
inputSchema: { type: "object", properties: {} },
},
{
name: "debug_list_extensions",
description: "List all installed extensions.",
inputSchema: { type: "object", properties: {} },
},
{
name: "debug_get_project_logs",
description: "Read recent project log entries from the log file.",
inputSchema: {
type: "object",
properties: {
lines: { type: "number", description: "Number of lines to read (default 100)" },
},
},
},
{
name: "debug_search_project_logs",
description: "Search for a pattern in project logs.",
inputSchema: {
type: "object",
properties: {
pattern: { type: "string", description: "Search pattern (regex supported)" },
},
required: ["pattern"],
},
},
{
name: "debug_get_log_file_info",
description: "Get information about the project log file (size, path, last modified).",
inputSchema: { type: "object", properties: {} },
},
// ── 以下、既存MCP未対応のEditor API ──
{
name: "debug_query_devices",
description: "List connected devices (for native debugging).",
inputSchema: { type: "object", properties: {} },
},
{
name: "debug_open_url",
description: "Open a URL in the system browser from the editor.",
inputSchema: {
type: "object",
properties: { url: { type: "string", description: "URL to open" } },
required: ["url"],
},
},
{
name: "debug_validate_scene",
description: "Validate the current scene for common issues.",
inputSchema: { type: "object", properties: {} },
},
{
name: "debug_game_command",
description: "Send a command to the running game preview. Requires GameDebugClient in the game. Commands: 'screenshot' (capture game canvas), 'state' (dump GameDb), 'navigate' (go to a page), 'click' (click a node by name), 'inspect' (get runtime node info: UITransform sizes, Widget, Layout, position). Returns the result from the game.",
inputSchema: {
type: "object",
properties: {
type: { type: "string", description: "Command type: 'screenshot', 'state', 'navigate', 'click', 'inspect'" },
args: { type: "object", description: "Command arguments (e.g. {page: 'HomePageView'} for navigate, {name: 'ButtonName'} for click)" },
timeout: { type: "number", description: "Max wait time in ms (default 5000)" },
maxWidth: { type: "number", description: "Max width for screenshot resize (default: 960, 0 = no resize)" },
imageFormat: { type: "string", description: "Screenshot output format: 'webp' (default, Q=85) or 'png' (lossless)" },
},
required: ["type"],
},
},
{
name: "debug_screenshot",
description: "Take a screenshot of the editor window and save to a file. Returns the file path of the saved PNG.",
inputSchema: {
type: "object",
properties: {
savePath: { type: "string", description: "File path to save the PNG (default: temp/screenshots/screenshot_<timestamp>.png)" },
maxWidth: { type: "number", description: "Max width in pixels for resize (default: 960, 0 = no resize). Aspect ratio is preserved." },
},
},
},
{
name: "debug_preview",
description: "Start or stop the game preview. Uses Preview in Editor (auto-opens MainScene if needed). Falls back to browser preview if editor preview fails.",
inputSchema: {
type: "object",
properties: {
action: { type: "string", description: "'start' (default) or 'stop'" },
waitForReady: { type: "boolean", description: "If true, wait until GameDebugClient connects after start (default: false)" },
waitTimeout: { type: "number", description: "Max wait time in ms for waitForReady (default: 15000)" },
},
},
},
{
name: "debug_clear_code_cache",
description: "Clear the code cache (equivalent to Developer > Cache > Clear code cache) and soft-reload the scene.",
inputSchema: { type: "object", properties: {} },
},
{
name: "debug_reload_extension",
description: "Reload the MCP extension itself. Use after npm run build to apply code changes without restarting CocosCreator. Response is sent before reload starts.",
inputSchema: { type: "object", properties: {} },
},
{
name: "debug_get_extension_info",
description: "Get detailed information about a specific extension.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Extension name" },
},
required: ["name"],
},
},
{
name: "debug_record_start",
description: "Start recording the game preview canvas to a video file. Uses MediaRecorder on the game side. Bitrate is auto-calculated from canvas resolution × fps × quality coefficient unless videoBitsPerSecond is set explicitly.",
inputSchema: {
type: "object",
properties: {
fps: { type: "number", description: "Frames per second (default: 30)" },
quality: { type: "string", description: "'low'/'medium'/'high'/'ultra' (default: medium). Coefficients: 0.15/0.25/0.40/0.60" },
coefficient: { type: "number", description: "Custom bitrate coefficient (width × height × fps × coefficient). Overrides quality." },
videoBitsPerSecond: { type: "number", description: "Explicit bitrate in bps. Overrides quality-based calculation." },
format: { type: "string", description: "'mp4' (default) or 'webm'. mp4 falls back to webm if not supported." },
savePath: { type: "string", description: "Save directory (project-relative or absolute). Default: temp/recordings" },
},
},
},
{
name: "debug_record_stop",
description: "Stop recording started by debug_record_start. Returns the saved WebM file path and size. Video is saved to project's temp/recordings/rec_<datetime>.webm.",
inputSchema: {
type: "object",
properties: {
timeout: { type: "number", description: "Max wait time in ms for file upload (default: 30000)" },
},
},
},
{
name: "debug_batch_screenshot",
description: "Navigate to multiple pages and take a screenshot of each. Requires game preview running with GameDebugClient. Returns an array of screenshot file paths.",
inputSchema: {
type: "object",
properties: {
pages: {
type: "array",
items: { type: "string" },
description: "List of page names to screenshot (e.g. ['HomePageView', 'ShopPageView'])",
},
delay: { type: "number", description: "Delay in ms between navigate and screenshot (default: 1000)" },
maxWidth: { type: "number", description: "Max width for screenshot resize (default: 960)" },
},
required: ["pages"],
},
},
{
name: "debug_wait_compile",
description: "Wait for TypeScript compilation to complete. Monitors the packer-driver debug log for 'Target(editor) ends' message. Use after modifying .ts files to ensure changes are compiled before operating on Prefabs. With clean=true, deletes compiled output first to force a fresh recompile (slower but guaranteed).",
inputSchema: {
type: "object",
properties: {
timeout: { type: "number", description: "Max wait time in ms (default: 15000)" },
clean: { type: "boolean", description: "If true, delete compiled output first to force fresh recompile (default: false)" },
},
},
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "debug_get_editor_info":
return this.getEditorInfo();
case "debug_list_messages":
return this.listMessages(args.target);
case "debug_execute_script":
return this.executeScript(args.method, args.args || []);
case "debug_get_console_logs":
return this.getConsoleLogs(args.count || 50, args.level, args.source);
case "debug_clear_console":
Editor.Message.send("console", "clear");
// Clear scene process log buffer
await Editor.Message.request("scene", "execute-scene-script", {
name: "cocos-creator-mcp",
method: "clearConsoleLogs",
args: [],
}).catch(() => {});
// Clear game preview log buffer
clearGameLogs();
return ok({ success: true });
case "debug_list_extensions":
return this.listExtensions();
case "debug_get_project_logs":
return this.getProjectLogs(args.lines || 100);
case "debug_search_project_logs":
return this.searchProjectLogs(args.pattern);
case "debug_get_log_file_info":
return this.getLogFileInfo();
case "debug_query_devices": {
const devices = await (Editor.Message.request as any)("device", "query").catch(() => []);
return ok({ success: true, devices });
}
case "debug_open_url":
await (Editor.Message.request as any)("program", "open-url", args.url);
return ok({ success: true, url: args.url });
case "debug_game_command":
return this.gameCommand(args.type || args.command, parseMaybeJson(args.args), args.timeout || 5000, args.maxWidth, args.imageFormat);
case "debug_screenshot":
return this.takeScreenshot(args.savePath, args.maxWidth);
case "debug_preview":
return this.handlePreview(args.action || "start", args.waitForReady, args.waitTimeout || 15000);
case "debug_clear_code_cache":
return this.clearCodeCache();
case "debug_reload_extension":
return this.reloadExtension();
case "debug_validate_scene":
return this.validateScene();
case "debug_get_extension_info":
return this.getExtensionInfo(args.name);
case "debug_batch_screenshot":
return this.batchScreenshot(args.pages, args.delay || 1000, args.maxWidth);
case "debug_record_start":
return this.gameCommand("record_start", { fps: args.fps, quality: args.quality, coefficient: args.coefficient, videoBitsPerSecond: args.videoBitsPerSecond, format: args.format, savePath: args.savePath }, 5000);
case "debug_record_stop":
return this.gameCommand("record_stop", undefined, args.timeout || 30000);
case "debug_wait_compile":
return this.waitCompile(args.timeout || 15000, args.clean ?? false);
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getEditorInfo(): Promise<ToolResult> {
return ok({
success: true,
version: Editor.App.version,
path: Editor.App.path,
home: Editor.App.home,
language: Editor.I18n?.getLanguage?.() || "unknown",
});
}
private async listMessages(target: string): Promise<ToolResult> {
try {
const info = await (Editor.Message.request as any)("extension", "query-info", target);
return ok({ success: true, target, info });
} catch (e: any) {
const knownMessages: Record<string, string[]> = {
"scene": [
"query-node-tree", "create-node", "remove-node", "duplicate-node",
"set-property", "create-prefab", "save-scene", "execute-scene-script",
"query-is-dirty", "query-classes", "soft-reload", "snapshot",
"change-gizmo-tool", "query-gizmo-tool-name", "focus-camera-on-nodes",
],
"asset-db": [
"query-assets", "query-asset-info", "query-asset-meta",
"refresh-asset", "save-asset", "create-asset", "delete-asset",
"move-asset", "copy-asset", "open-asset", "reimport-asset",
"query-path", "query-uuid", "query-url", "query-asset-depends",
],
};
const messages = knownMessages[target];
if (messages) {
return ok({ success: true, target, messages, note: "Static list (query failed)" });
}
return err(e.message || String(e));
}
}
private async executeScript(method: string, args: any[]): Promise<ToolResult> {
const result = await Editor.Message.request("scene", "execute-scene-script", {
name: "cocos-creator-mcp",
method,
args,
});
return ok(result);
}
private async getConsoleLogs(count: number, level?: string, source?: string): Promise<ToolResult> {
// 1. Try Editor's native console API first (may be supported in future CocosCreator versions)
if (!source) {
try {
const logs = await (Editor.Message.request as any)("console", "query-last-logs", count);
if (Array.isArray(logs) && logs.length > 0) {
return ok({ success: true, logs, source: "editor-api", note: "Using native Editor console API" });
}
} catch { /* Not supported in this version — use fallback */ }
}
// 2. Fallback: collect from scene process buffer + game preview buffer
let sceneLogs: any[] = [];
let gameLogs: any[] = [];
// 2a. Scene process logs (console wrapper in scene.ts)
if (!source || source === "scene") {
try {
const result = await Editor.Message.request("scene", "execute-scene-script", {
name: "cocos-creator-mcp",
method: "getConsoleLogs",
args: [count * 2, level], // request more, will trim after merge
});
if (result?.logs) {
sceneLogs = result.logs.map((l: any) => ({ ...l, source: "scene" }));
}
} catch { /* scene not available */ }
}
// 2b. Game preview logs (received via POST /log endpoint)
if (!source || source === "game") {
const gameResult = getGameLogs(count * 2, level);
gameLogs = gameResult.logs.map((l: any) => ({ ...l, source: "game" }));
}
// Merge and sort by timestamp, take last `count`
const merged = [...sceneLogs, ...gameLogs]
.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
.slice(-count);
return ok({
success: true,
logs: merged,
total: { scene: sceneLogs.length, game: (source === "scene" ? 0 : gameLogs.length) },
});
}
private async listExtensions(): Promise<ToolResult> {
try {
const list = await (Editor.Message.request as any)("extension", "query-all");
return ok({ success: true, extensions: list });
} catch {
return ok({ success: true, extensions: [], note: "Extension query not supported" });
}
}
private async getExtensionInfo(name: string): Promise<ToolResult> {
try {
const info = await (Editor.Message.request as any)("extension", "query-info", name);
return ok({ success: true, name, info });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getProjectLogs(lines: number): Promise<ToolResult> {
try {
const fs = require("fs");
const path = require("path");
const logPath = path.join(Editor.Project.tmpDir, "logs", "project.log");
if (!fs.existsSync(logPath)) return ok({ success: true, logs: [], note: "Log file not found" });
const content = fs.readFileSync(logPath, "utf-8");
const allLines = content.split("\n");
const recent = allLines.slice(-lines);
return ok({ success: true, lines: recent.length, logs: recent });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async searchProjectLogs(pattern: string): Promise<ToolResult> {
try {
const fs = require("fs");
const path = require("path");
const logPath = path.join(Editor.Project.tmpDir, "logs", "project.log");
if (!fs.existsSync(logPath)) return ok({ success: true, matches: [] });
const content = fs.readFileSync(logPath, "utf-8");
const regex = new RegExp(pattern, "gi");
const matches = content.split("\n").filter((line: string) => regex.test(line));
return ok({ success: true, pattern, count: matches.length, matches: matches.slice(0, 100) });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getLogFileInfo(): Promise<ToolResult> {
try {
const fs = require("fs");
const path = require("path");
const logPath = path.join(Editor.Project.tmpDir, "logs", "project.log");
if (!fs.existsSync(logPath)) return ok({ success: true, exists: false });
const stat = fs.statSync(logPath);
return ok({ success: true, exists: true, path: logPath, size: stat.size, modified: stat.mtime });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async handlePreview(action: string, waitForReady?: boolean, waitTimeout?: number): Promise<ToolResult> {
if (action === "stop") {
return this.stopPreview();
}
const result = await this.startPreview();
if (waitForReady) {
const resultData = JSON.parse(result.content[0].text);
if (resultData.success) {
const ready = await this.waitForGameReady(waitTimeout || 15000);
resultData.gameReady = ready;
if (!ready) {
resultData.note = (resultData.note || "") + " GameDebugClient did not connect within timeout.";
}
return ok(resultData);
}
}
return result;
}
private async waitForGameReady(timeout: number): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeout) {
// Check if game has sent any log or command result recently
const gameResult = getGameLogs(1);
if (gameResult.total > 0) return true;
await new Promise(r => setTimeout(r, 500));
}
return false;
}
private async startPreview(): Promise<ToolResult> {
try {
await this.ensureMainSceneOpen();
// ツールバーのVueインスタンス経由でplay()を呼ぶ(UI状態も同期される)
const played = await this.executeOnToolbar("start");
if (played) {
return ok({ success: true, action: "start", mode: "editor" });
}
// フォールバック: 直接API
const isPlaying = await (Editor.Message.request as any)("scene", "editor-preview-set-play", true);
return ok({ success: true, isPlaying, action: "start", mode: "editor", note: "direct API (toolbar UI may not sync)" });
} catch (e: any) {
try {
const electron = require("electron");
await electron.shell.openExternal("http://127.0.0.1:7456");
return ok({ success: true, action: "start", mode: "browser" });
} catch (e2: any) {
return err(e2.message || String(e2));
}
}
}
private async stopPreview(): Promise<ToolResult> {
try {
// ツールバー経由で停止(UI同期)
const stopped = await this.executeOnToolbar("stop");
if (!stopped) {
// フォールバック: 直接API
await (Editor.Message.request as any)("scene", "editor-preview-set-play", false);
}
// scene:preview-stop ブロードキャストでツールバーUI状態をリセット
Editor.Message.broadcast("scene:preview-stop");
// シーンビューに戻す
await new Promise(r => setTimeout(r, 500));
await this.ensureMainSceneOpen();
return ok({ success: true, action: "stop" });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async executeOnToolbar(action: "start" | "stop"): Promise<boolean> {
try {
const electron = require("electron");
const allContents = electron.webContents.getAllWebContents();
for (const wc of allContents) {
try {
// play()をawaitしない — プレビュー完了を待つとタイムアウトするため
if (action === "start") {
const result = await wc.executeJavaScript(
`(function() { if (window.xxx && window.xxx.play && !window.xxx.gameView.isPlay) { window.xxx.play(); return true; } return false; })()`
);
if (result) return true;
} else {
const result = await wc.executeJavaScript(
`(function() { if (window.xxx && window.xxx.gameView.isPlay) { window.xxx.play(); return true; } return false; })()`
);
if (result) return true;
}
} catch { /* not the toolbar webContents */ }
}
} catch { /* electron API not available */ }
return false;
}
private async ensureMainSceneOpen(): Promise<void> {
const hierarchy = await Editor.Message.request("scene", "execute-scene-script", {
name: "cocos-creator-mcp",
method: "getSceneHierarchy",
args: [false],
}).catch(() => null);
if (!hierarchy?.sceneName || hierarchy.sceneName === "scene-2d") {
// プロジェクト設定のStart Sceneを参照
let sceneUuid: string | null = null;
try {
sceneUuid = await (Editor as any).Profile.getConfig("preview", "general.start_scene", "local");
} catch { /* ignore */ }
// Start Sceneが未設定 or "current_scene" の場合、最初のシーンを使う
if (!sceneUuid || sceneUuid === "current_scene") {
const scenes = await Editor.Message.request("asset-db", "query-assets", {
ccType: "cc.SceneAsset",
pattern: "db://assets/**/*",
});
if (Array.isArray(scenes) && scenes.length > 0) {
sceneUuid = scenes[0].uuid;
}
}
if (sceneUuid) {
// debug_preview 内部の自動遷移は preview を優先して force=true
// (dialog 出るより preview 開始を優先する運用)
await ensureSceneSafeToSwitch(true);
await (Editor.Message.request as any)("scene", "open-scene", sceneUuid);
await new Promise(r => setTimeout(r, 1500));
}
}
}
private async clearCodeCache(): Promise<ToolResult> {
try {
const electron = require("electron");
const menu = electron.Menu.getApplicationMenu();
if (!menu) return err("Application menu not found");
const findMenuItem = (items: any[], path: string[]): any => {
for (const item of items) {
if (item.label === path[0]) {
if (path.length === 1) return item;
if (item.submenu?.items) return findMenuItem(item.submenu.items, path.slice(1));
}
}
return null;
};
const cacheItem = findMenuItem(menu.items, ["Developer", "Cache", "Clear code cache"]);
if (!cacheItem) return err("Menu item 'Developer > Cache > Clear code cache' not found");
cacheItem.click();
await new Promise(r => setTimeout(r, 1000));
return ok({ success: true, note: "Code cache cleared via menu" });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async gameCommand(type: string, args: any, timeout: number, maxWidth?: number, imageFormat?: string): Promise<ToolResult> {
const cmdId = queueGameCommand(type, args);
// Poll for result
const start = Date.now();
while (Date.now() - start < timeout) {
const result = getCommandResult();
if (result && result.id === cmdId) {
// If screenshot, save to file and return path
if (type === "screenshot" && result.success && result.data?.dataUrl) {
try {
const fs = require("fs");
const path = require("path");
const dir = path.join(Editor.Project.tmpDir, "screenshots");
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const base64 = result.data.dataUrl.replace(/^data:image\/png;base64,/, "");
const pngBuffer = Buffer.from(base64, "base64");
const effectiveMaxWidth = maxWidth !== undefined ? maxWidth : 960;
const electron = require("electron");
const origImage = electron.nativeImage.createFromBuffer(pngBuffer);
const originalSize = origImage.getSize();
const { buffer, width, height, format } = await processImage(pngBuffer, effectiveMaxWidth, imageFormat);
const ext = format === "webp" ? "webp" : format === "jpeg" ? "jpg" : "png";
const filePath = path.join(dir, `game_${timestamp}.${ext}`);
fs.writeFileSync(filePath, buffer);
return ok({
success: true, path: filePath, size: buffer.length, format,
originalSize: `${originalSize.width}x${originalSize.height}`,
savedSize: `${width}x${height}`,
});
} catch (e: any) {
return ok({ success: true, note: "Screenshot captured but file save failed", error: e.message });
}
}
return ok(result);
}
await new Promise(r => setTimeout(r, 200));
}
return err(`Game did not respond within ${timeout}ms. Is GameDebugClient running in the preview?`);
}
private async takeScreenshot(savePath?: string, maxWidth?: number): Promise<ToolResult> {
try {
const result = await takeEditorScreenshot(savePath, maxWidth);
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async reloadExtension(): Promise<ToolResult> {
// Schedule reload after response is sent
setTimeout(async () => {
try {
await (Editor.Message.request as any)("extension", "reload", "cocos-creator-mcp");
} catch (e: any) {
console.error("[MCP] Extension reload failed:", e.message);
}
}, 500);
return ok({ success: true, note: "Extension reload scheduled. MCP server will restart in ~1s. NOTE: Adding new tool definitions or modifying scene.ts requires a full CocosCreator restart (reload is not sufficient)." });
}
private async batchScreenshot(pages: string[], delay: number, maxWidth?: number): Promise<ToolResult> {
const results: any[] = [];
const timeout = 10000;
for (const page of pages) {
// Navigate
const navResult = await this.gameCommand("navigate", { page }, timeout, maxWidth);
const navData = JSON.parse(navResult.content[0].text);
if (!navData.success) {
results.push({ page, success: false, error: "navigate failed" });
continue;
}
// Wait for page to render
await new Promise(r => setTimeout(r, delay));
// Screenshot
const ssResult = await this.gameCommand("screenshot", {}, timeout, maxWidth);
const ssData = JSON.parse(ssResult.content[0].text);
results.push({
page,
success: ssData.success || false,
path: ssData.path,
error: ssData.success ? undefined : (ssData.error || ssData.message),
});
}
const succeeded = results.filter(r => r.success).length;
return ok({
success: true,
total: pages.length,
succeeded,
failed: pages.length - succeeded,
results,
});
}
private async validateScene(): Promise<ToolResult> {
try {
const tree = await Editor.Message.request("scene", "query-node-tree");
const issues: string[] = [];
const checkNodes = (nodes: any[]) => {
if (!nodes) return;
for (const node of nodes) {
if (!node.name) issues.push(`Node ${node.uuid} has no name`);
if (node.children) checkNodes(node.children);
}
};
if (Array.isArray(tree)) checkNodes(tree);
return ok({ success: true, issueCount: issues.length, issues });
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* TypeScript コンパイル完了を待つ。
* packer-driver の debug.log に "Target(editor) ends" が現れるのを監視する。
* 既にコンパイル済み(直近数秒以内に完了ログあり)なら即座に返す。
*/
private async waitCompile(timeout: number, clean: boolean): Promise<ToolResult> {
try {
const fs = require("fs");
const path = require("path");
const logPath = path.join(Editor.Project.path, "temp", "programming", "packer-driver", "logs", "debug.log");
const chunksDir = path.join(Editor.Project.path, "temp", "programming", "packer-driver", "targets", "editor", "chunks");
if (!fs.existsSync(logPath)) {
return err(`Compile log not found: ${logPath}`);
}
const MARKER = "Target(editor) ends";
// clean モード: コードキャッシュクリア + soft-reload で再コンパイルを強制
if (clean) {
// Developer > Cache > Clear code cache をクリック
try {
const electron = require("electron");
const menu = electron.Menu.getApplicationMenu();
const findMenuItem = (items: any[], labels: string[]): any => {
for (const item of items) {
if (item.label === labels[0]) {
if (labels.length === 1) return item;
if (item.submenu?.items) return findMenuItem(item.submenu.items, labels.slice(1));
}
}
return null;
};
const cacheItem = menu ? findMenuItem(menu.items, ["Developer", "Cache", "Clear code cache"]) : null;
if (cacheItem) cacheItem.click();
} catch (_e) { /* ignore */ }
await new Promise(r => setTimeout(r, 500));
// soft-reload でシーンを再読み込み → コンパイルトリガー
await (Editor.Message.request as any)("scene", "soft-reload").catch(() => {});
}
// refresh-asset でファイル変更を CC に通知してコンパイルをトリガー
await (Editor.Message.request as any)("asset-db", "refresh-asset", "db://assets").catch(() => {});
const initialSize = fs.statSync(logPath).size;
const startTime = Date.now();
const POLL_INTERVAL = 200;
const DETECT_GRACE_MS = 2000; // CC がファイル変更を検知するまでの猶予
while (Date.now() - startTime < timeout) {
await new Promise(r => setTimeout(r, POLL_INTERVAL));
const currentSize = fs.statSync(logPath).size;
// ログが成長していない
if (currentSize <= initialSize) {
// clean モードでは必ずコンパイルが走るので猶予判定しない
if (clean) continue;
// 猶予期間内はまだ待つ (CC の検知が遅い可能性)
if (Date.now() - startTime < DETECT_GRACE_MS) continue;
// 猶予期間を過ぎてもログが成長しない → コンパイル不要
return ok({ success: true, compiled: true, waitedMs: Date.now() - startTime, note: "No compilation triggered (no changes detected)" });
}
// ログが成長した → 新しい部分にマーカーがあるか確認
const fd = fs.openSync(logPath, "r");
const newBytes = currentSize - initialSize;
const buffer = Buffer.alloc(newBytes);
fs.readSync(fd, buffer, 0, newBytes, initialSize);
fs.closeSync(fd);
const newContent = buffer.toString("utf8");
if (newContent.includes(MARKER)) {
return ok({ success: true, compiled: true, waitedMs: Date.now() - startTime });
}
}
return ok({ success: true, compiled: false, timeout: true, waitedMs: timeout });
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,624 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
import { parseMaybeJson } from "../utils";
import { resolveNodeUuid } from "../node-resolve";
import { takeEditorScreenshot } from "../screenshot";
const EXT_NAME = "cocos-creator-mcp";
export class NodeTools implements ToolCategory {
readonly categoryName = "node";
getTools(): ToolDefinition[] {
return [
{
name: "node_create",
description: "Create a new node in the scene.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Node name" },
parent: { type: "string", description: "Parent node UUID (optional, defaults to scene root)" },
components: {
type: "array",
items: { type: "string" },
description: "Component class names to add (e.g. ['cc.Label', 'cc.Sprite'])",
},
},
required: ["name"],
},
},
{
name: "node_get_info",
description: "Get detailed information about a node by UUID, including components.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
},
required: ["uuid"],
},
},
{
name: "node_find_by_name",
description: "Find all nodes matching a given name.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Node name to search" },
},
required: ["name"],
},
},
{
name: "node_set_property",
description: "Set a property on a node (name, active, position, rotation, scale, etc.).",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
property: { type: "string", description: "Property name (e.g. 'name', 'active', 'position')" },
value: { description: "Value to set. For position/rotation/scale use {x,y,z}." },
},
required: ["uuid", "property", "value"],
},
},
{
name: "node_set_transform",
description: "Set position, rotation, and/or scale of a node at once.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
position: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
description: "Position {x,y,z}",
},
rotation: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
description: "Euler rotation {x,y,z}",
},
scale: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
description: "Scale {x,y,z}",
},
},
required: ["uuid"],
},
},
{
name: "node_delete",
description: "Delete a node by UUID.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
},
required: ["uuid"],
},
},
{
name: "node_move",
description: "Move a node to a new parent.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
parentUuid: { type: "string", description: "New parent node UUID" },
},
required: ["uuid", "parentUuid"],
},
},
{
name: "node_duplicate",
description: "Duplicate a node.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID to duplicate" },
},
required: ["uuid"],
},
},
{
name: "node_get_all",
description: "Get a flat list of all nodes in the current scene.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "node_set_active",
description: "Set a node's active (visible) state.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
active: { type: "boolean", description: "Whether the node is active" },
},
required: ["uuid", "active"],
},
},
{
name: "node_set_layer",
description: "Set a node's layer.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
layer: { type: "number", description: "Layer value" },
},
required: ["uuid", "layer"],
},
},
{
name: "node_detect_type",
description: "Detect node type (2D, 3D, or regular Node) based on its components.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
},
required: ["uuid"],
},
},
{
name: "node_create_tree",
description: "Create a full node tree from a JSON spec in one call. Much faster than creating nodes one by one. Spec format: { name, components?: ['cc.UITransform'], properties?: {'cc.UITransform.contentSize': {width:720,height:1280}}, widget?: {top:0, bottom:0, left:0, right:0}, active?: bool, position?: {x,y,z}, children?: [...] }",
inputSchema: {
type: "object",
properties: {
parent: { type: "string", description: "Parent node UUID" },
spec: { description: "Node tree specification (JSON object with name, components, properties, children)" },
},
required: ["parent", "spec"],
},
},
{
name: "node_set_layout",
description: "Set UITransform (contentSize, anchorPoint) and Widget (margins) on a node in one call. Much faster than calling component_set_property multiple times for layout adjustments. Set screenshot=true to capture the editor after changes.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID (either uuid or nodeName required)" },
nodeName: { type: "string", description: "Node name to find (alternative to uuid)" },
contentSize: {
type: "object",
properties: { width: { type: "number" }, height: { type: "number" } },
description: "UITransform contentSize {width, height}",
},
anchorPoint: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" } },
description: "UITransform anchorPoint {x, y} (0-1)",
},
widget: {
type: "object",
properties: {
top: { type: "number" }, bottom: { type: "number" },
left: { type: "number" }, right: { type: "number" },
horizontalCenter: { type: "number" }, verticalCenter: { type: "number" },
isAlignTop: { type: "boolean" }, isAlignBottom: { type: "boolean" },
isAlignLeft: { type: "boolean" }, isAlignRight: { type: "boolean" },
isAlignHorizontalCenter: { type: "boolean" }, isAlignVerticalCenter: { type: "boolean" },
},
description: "Widget alignment margins. Setting a value (e.g. top:0) automatically enables the corresponding alignment (isAlignTop:true).",
},
color: {
type: "object",
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
description: "Node color {r,g,b,a} (0-255)",
},
opacity: { type: "number", description: "Node opacity (0-255)" },
screenshot: { type: "boolean", description: "If true, capture editor screenshot after changes (default: false)" },
},
},
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
const rejected = await this.rejectIfPreviewRunning(toolName);
if (rejected) return rejected;
switch (toolName) {
case "node_create":
return this.createNode(args.name, args.parent, args.components);
case "node_get_info":
return this.getNodeInfo(args.uuid);
case "node_find_by_name":
return this.findByName(args.name);
case "node_set_property":
return this.setProperty(args.uuid, args.property, parseMaybeJson(args.value));
case "node_set_transform":
return this.setTransform(args.uuid, args.position, args.rotation, args.scale);
case "node_delete":
return this.deleteNode(args.uuid);
case "node_move":
return this.moveNode(args.uuid, args.parentUuid);
case "node_duplicate":
return this.duplicateNode(args.uuid);
case "node_get_all":
return this.getAllNodes();
case "node_set_active":
return this.setProperty(args.uuid, "active", args.active);
case "node_set_layer":
return this.setProperty(args.uuid, "layer", args.layer);
case "node_create_tree":
return this.createNodeTree(args.parent, parseMaybeJson(args.spec));
case "node_set_layout":
return this.setLayout(args);
case "node_detect_type": {
try {
const info = await this.sceneScript("getNodeInfo", [args.uuid]);
if (!info.success) return ok(info);
const comps = info.data?.components || [];
const compTypes = comps.map((c: any) => c.type);
let nodeType = "Node";
if (compTypes.includes("UITransform")) nodeType = "2D";
else if (compTypes.includes("MeshRenderer") || compTypes.includes("Camera")) nodeType = "3D";
return ok({ success: true, uuid: args.uuid, nodeType, components: compTypes });
} catch (e: any) { return err(e.message || String(e)); }
}
default:
return err(`Unknown tool: ${toolName}`);
}
}
/** Scene editing tools that must not run during preview */
private static readonly SCENE_EDIT_TOOLS = new Set([
"node_create", "node_delete", "node_move", "node_duplicate",
"node_set_property", "node_set_transform", "node_set_active", "node_set_layer",
"node_create_tree", "node_set_layout",
]);
private async rejectIfPreviewRunning(toolName: string): Promise<ToolResult | null> {
if (!NodeTools.SCENE_EDIT_TOOLS.has(toolName)) return null;
try {
const state = await Editor.Message.request("preview", "query-info");
if (state && (state as any).running) {
return err(`"${toolName}" はプレビュー中に実行できません。先にプレビューを停止してください。`);
}
} catch { /* query failed — allow execution */ }
return null;
}
private async createNode(name: string, parent?: string, components?: string[]): Promise<ToolResult> {
try {
// Use Editor API to create node
const uuid = await Editor.Message.request("scene", "create-node", {
parent: parent || undefined,
name,
assetUuid: undefined,
});
// Wait until the node is queryable in the scene process
await this.waitForNode(uuid);
// Add components if specified
if (components && components.length > 0) {
for (const comp of components) {
await this.sceneScript("addComponentToNode", [uuid, comp]);
// Wait until the component is reflected in query-node
await this.waitForComponent(uuid, comp);
}
}
return ok({ success: true, uuid, name });
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* Wait until a node becomes queryable in the scene process.
* Editor.Message.request("scene", "create-node") returns before the node
* is fully registered in the scene hierarchy, so subsequent scene script
* calls (findNode) may fail without this wait.
*/
private async waitForNode(uuid: string, maxRetries = 10, intervalMs = 100): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
const result = await this.sceneScript("getNodeInfo", [uuid]);
if (result?.success) return;
} catch { /* not ready yet */ }
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
// Don't throw — let the caller proceed and get a more specific error if needed
}
/**
* Wait until a component added via addComponentToNode is reflected in query-node.
* sceneScript returns before the Editor API (query-node) reflects the change,
* so polling is needed to avoid race conditions in subsequent tool calls.
*/
private async waitForComponent(nodeUuid: string, componentType: string, maxRetries = 10, intervalMs = 100): Promise<void> {
const normalizedType = componentType.startsWith("cc.") ? componentType.substring(3) : componentType;
for (let i = 0; i < maxRetries; i++) {
try {
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", nodeUuid);
const comps: any[] = nodeDump?.__comps__ || [];
const found = comps.some((c) =>
c.type === componentType || c.type === `cc.${normalizedType}` || c.type === normalizedType
);
if (found) return;
} catch { /* not ready yet */ }
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
// Don't throw — component may still work; let caller get a specific error if needed
}
private async createNodeTree(parentUuid: string, spec: any): Promise<ToolResult> {
try {
const result = await this.sceneScript("buildNodeTree", [parentUuid, spec]);
if (!result?.success) return err(result?.error || "buildNodeTree failed");
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getNodeInfo(uuid: string): Promise<ToolResult> {
try {
const result = await this.sceneScript("getNodeInfo", [uuid]);
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async findByName(name: string): Promise<ToolResult> {
try {
const result = await this.sceneScript("findNodesByName", [name]);
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async setProperty(uuid: string, property: string, value: any): Promise<ToolResult> {
try {
const result = await this.sceneScript("setNodeProperty", [uuid, property, value]);
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
private async setTransform(uuid: string, position?: any, rotation?: any, scale?: any): Promise<ToolResult> {
try {
const results: any[] = [];
if (position) {
results.push(await this.sceneScript("setNodeProperty", [uuid, "position", position]));
}
if (rotation) {
results.push(await this.sceneScript("setNodeProperty", [uuid, "rotation", rotation]));
}
if (scale) {
results.push(await this.sceneScript("setNodeProperty", [uuid, "scale", scale]));
}
const anyFailed = results.find((r) => !r.success);
if (anyFailed) return ok(anyFailed);
return ok({ success: true, uuid });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async deleteNode(uuid: string): Promise<ToolResult> {
try {
await Editor.Message.request("scene", "remove-node", { uuid });
return ok({ success: true, uuid });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async moveNode(uuid: string, parentUuid: string): Promise<ToolResult> {
try {
await (Editor.Message.request as any)("scene", "set-property", {
uuid,
path: "parent",
dump: { type: "cc.Node", value: { uuid: parentUuid } },
});
return ok({ success: true, uuid, parentUuid });
} catch (e: any) {
// Fallback: try scene script
try {
const result = await this.sceneScript("moveNode", [uuid, parentUuid]);
return ok(result);
} catch (e2: any) {
return err(e.message || String(e));
}
}
}
private async duplicateNode(uuid: string): Promise<ToolResult> {
try {
const result = await Editor.Message.request("scene", "duplicate-node", uuid);
// duplicate-node returns an array of UUIDs
const newUuid = Array.isArray(result) ? result[0] : result;
return ok({ success: true, sourceUuid: uuid, newUuid });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getAllNodes(): Promise<ToolResult> {
try {
const result = await this.sceneScript("getAllNodes", []);
return ok(result);
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* UITransform + Widget + color/opacity をまとめて設定する。
* Widget の値を指定すると、対応する isAlign* フラグを自動で true にする。
*/
private async setLayout(args: Record<string, any>): Promise<ToolResult> {
try {
// nodeName → uuid 解決
let uuid = args.uuid;
if (!uuid && args.nodeName) {
const resolved = await resolveNodeUuid({ nodeName: args.nodeName });
uuid = resolved.uuid;
}
if (!uuid) return err("Either 'uuid' or 'nodeName' is required");
const results: any[] = [];
// UITransform の設定
const contentSize = parseMaybeJson(args.contentSize);
const anchorPoint = parseMaybeJson(args.anchorPoint);
if (contentSize || anchorPoint) {
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
if (!nodeInfo?.success) return err(`Node ${uuid} not found`);
const comps = nodeInfo.data?.components || [];
const utIdx = comps.findIndex((c: any) => c.type === "UITransform");
if (utIdx < 0) return err("Node has no UITransform component");
if (contentSize) {
const path = `__comps__.${utIdx}.contentSize`;
const dump = { value: { width: { value: contentSize.width }, height: { value: contentSize.height } } };
const r = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
results.push({ property: "contentSize", success: r?.success !== false });
}
if (anchorPoint) {
const path = `__comps__.${utIdx}.anchorPoint`;
const dump = { value: { x: { value: anchorPoint.x }, y: { value: anchorPoint.y } } };
const r = await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
results.push({ property: "anchorPoint", success: r?.success !== false });
}
}
// Widget の設定
const widget = parseMaybeJson(args.widget);
if (widget) {
// Widget コンポーネントを探す(なければ追加)
let nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
if (!nodeInfo?.success) return err(`Node ${uuid} not found`);
let comps = nodeInfo.data?.components || [];
let wIdx = comps.findIndex((c: any) => c.type === "Widget");
if (wIdx < 0) {
await this.sceneScript("addComponentToNode", [uuid, "cc.Widget"]);
// 再取得
nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
comps = nodeInfo.data?.components || [];
wIdx = comps.findIndex((c: any) => c.type === "Widget");
if (wIdx < 0) return err("Failed to add Widget component");
results.push({ property: "Widget", action: "added" });
}
// isAlign* を自動設定(値があれば true にする)
const alignMap: Record<string, string> = {
top: "isAlignTop", bottom: "isAlignBottom",
left: "isAlignLeft", right: "isAlignRight",
horizontalCenter: "isAlignHorizontalCenter",
verticalCenter: "isAlignVerticalCenter",
};
for (const [key, value] of Object.entries(widget)) {
// isAlign* を明示指定した場合はそのまま設定
const path = `__comps__.${wIdx}.${key}`;
if (typeof value === "boolean") {
const dump = { value, type: "Boolean" };
await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
results.push({ property: `Widget.${key}`, success: true });
} else if (typeof value === "number") {
// まず対応する isAlign* を true にする
const alignKey = alignMap[key];
if (alignKey && widget[alignKey] === undefined) {
const alignPath = `__comps__.${wIdx}.${alignKey}`;
await this.sceneScript("setPropertyViaEditor", [uuid, alignPath, { value: true, type: "Boolean" }]);
}
const dump = { value, type: "Number" };
await this.sceneScript("setPropertyViaEditor", [uuid, path, dump]);
results.push({ property: `Widget.${key}`, success: true });
}
}
// _alignFlags を isAlign* 現在値から再計算して設定
// (Editor が isAlign* 変更時に _alignFlags を自動更新しないバグの対処)
try {
const ALIGN_BITS: Record<string, number> = {
isAlignLeft: 1, isAlignRight: 2, isAlignTop: 4, isAlignBottom: 8,
isAlignHorizontalCenter: 16, isAlignVerticalCenter: 32,
};
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", uuid);
if (nodeDump) {
const wCompDump = nodeDump.__comps__?.[wIdx];
if (wCompDump) {
let alignFlags = 0;
for (const [key, bit] of Object.entries(ALIGN_BITS)) {
if (wCompDump.value?.[key]?.value === true) alignFlags |= bit;
}
const flagPath = `__comps__.${wIdx}._alignFlags`;
await this.sceneScript("setPropertyViaEditor", [uuid, flagPath, { value: alignFlags, type: "Number" }]);
results.push({ property: "Widget._alignFlags", value: alignFlags });
}
}
} catch (_e) {
// _alignFlags 再計算の失敗は致命的でないため無視
}
}
// color
const color = parseMaybeJson(args.color);
if (color) {
const r = await this.sceneScript("setNodeProperty", [uuid, "color", color]);
results.push({ property: "color", success: r?.success !== false });
}
// opacity
if (args.opacity !== undefined) {
// cc.UIOpacity を使う(なければ color.a で設定)
const nodeInfo = await this.sceneScript("getNodeInfo", [uuid]);
const comps = nodeInfo?.data?.components || [];
const opIdx = comps.findIndex((c: any) => c.type === "UIOpacity");
if (opIdx >= 0) {
const path = `__comps__.${opIdx}.opacity`;
await this.sceneScript("setPropertyViaEditor", [uuid, path, { value: args.opacity, type: "Number" }]);
results.push({ property: "UIOpacity.opacity", success: true });
} else {
// UIOpacity がない場合は color.a を直接設定
const currentColor = nodeInfo?.data?.color || { r: 255, g: 255, b: 255, a: 255 };
currentColor.a = args.opacity;
const r = await this.sceneScript("setNodeProperty", [uuid, "color", currentColor]);
results.push({ property: "color.a", success: r?.success !== false });
}
}
const allOk = results.every(r => r.success !== false);
let response: any = { success: allOk, uuid, results };
// screenshot
if (args.screenshot) {
try {
const ss = await takeEditorScreenshot();
response.screenshot = { path: ss.path, size: ss.savedSize };
} catch (ssErr: any) {
response.screenshotError = ssErr.message || String(ssErr);
}
}
return ok(response);
} catch (e: any) {
return err(e.message || String(e));
}
}
/** Call a scene script method */
private async sceneScript(method: string, args: any[]): Promise<any> {
return Editor.Message.request("scene", "execute-scene-script", {
name: EXT_NAME,
method,
args,
});
}
}
@@ -1,680 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
import { ensureSceneSafeToSwitch, safeSaveScene } from "./scene-tools";
import { parseMaybeJson } from "../utils";
import type { ComponentTools } from "./component-tools";
import fs from "fs";
import path from "path";
import crypto from "crypto";
const EXT_NAME = "cocos-creator-mcp";
/** prefab_instantiate で配置したネスト Prefab 情報を記憶 */
interface NestedPrefabEntry {
nodeUuid: string;
prefabAssetUuid: string;
parentUuid: string;
}
export class PrefabTools implements ToolCategory {
readonly categoryName = "prefab";
private _pendingNestedPrefabs: NestedPrefabEntry[] = [];
/** prefab_open で開いた Prefab アセット UUID */
private _currentPrefabUuid: string | null = null;
private _componentTools: ComponentTools | null;
constructor(componentTools?: ComponentTools) {
this._componentTools = componentTools ?? null;
}
getTools(): ToolDefinition[] {
return [
{
name: "prefab_list",
description: "List all prefab files in the project.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "prefab_create",
description: "Create a prefab from an existing node in the scene. The node remains in the scene.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID to create prefab from" },
path: { type: "string", description: "db:// path for the prefab (e.g. 'db://assets/prefabs/MyPrefab.prefab')" },
},
required: ["uuid", "path"],
},
},
{
name: "prefab_instantiate",
description: "Instantiate a prefab into the scene.",
inputSchema: {
type: "object",
properties: {
prefabUuid: { type: "string", description: "Prefab asset UUID" },
parent: { type: "string", description: "Parent node UUID (optional, defaults to scene root)" },
},
required: ["prefabUuid"],
},
},
{
name: "prefab_get_info",
description: "Get information about a prefab asset (name, path, UUID).",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Prefab asset UUID" },
},
required: ["uuid"],
},
},
{
name: "prefab_update",
description: "Update (re-save) a prefab from its instance node in the scene.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID of the prefab instance in the scene" },
},
required: ["uuid"],
},
},
{
name: "prefab_duplicate",
description: "Duplicate a prefab asset to a new path.",
inputSchema: {
type: "object",
properties: {
source: { type: "string", description: "Source prefab db:// path" },
destination: { type: "string", description: "Destination db:// path" },
},
required: ["source", "destination"],
},
},
{
name: "prefab_validate",
description: "Validate a prefab for missing references or broken links.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Prefab asset UUID" },
},
required: ["uuid"],
},
},
{
name: "prefab_revert",
description: "Revert a prefab instance node to its original prefab state.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID of the prefab instance" },
},
required: ["uuid"],
},
},
{
name: "prefab_create_and_replace",
description: "Create a prefab from a node AND replace the original node with a prefab instance. This is the recommended way to extract a nested prefab — one command instead of create → delete → instantiate.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID to create prefab from" },
path: { type: "string", description: "db:// path for the prefab (e.g. 'db://assets/prefabs/MyPrefab.prefab')" },
},
required: ["uuid", "path"],
},
},
{
name: "prefab_open",
description: "Open a prefab in editing mode. Equivalent to double-clicking the prefab in CocosCreator. Returns an error if the current scene is dirty and untitled (to avoid modal save dialog); pass force=true to bypass.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Prefab asset UUID" },
path: { type: "string", description: "Prefab db:// path (alternative to uuid)" },
force: { type: "boolean", description: "Skip dirty-scene preflight check (may trigger modal save dialog)" },
},
},
},
{
name: "prefab_close",
description: "Save and close the current prefab editing mode, then return to the main scene. Returns an error if the current prefab is dirty and untitled (to avoid modal save dialog); pass force=true to bypass.",
inputSchema: {
type: "object",
properties: {
save: { type: "boolean", description: "Save prefab before closing (default true)" },
sceneUuid: { type: "string", description: "Scene UUID to return to (default: project's start scene or first scene)" },
force: { type: "boolean", description: "Skip dirty-scene preflight check (may trigger modal save dialog)" },
},
},
},
{
name: "prefab_create_from_spec",
description: "Create a prefab from a JSON spec in one call. Combines node_create_tree + component_auto_bind + prefab_create into a single operation. Spec extends node_create_tree format with optional autoBind field. Example: { name: 'MyPopup', components: ['cc.UITransform', 'MyPopupView'], autoBind: 'MyPopupView', children: [{ name: 'CloseButton', components: ['cc.Button'] }] }",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "db:// path for the prefab (e.g. 'db://assets/prefabs/MyPrefab.prefab')" },
spec: { description: "Node tree specification with optional autoBind field (string for component type to auto-bind)" },
autoBindMode: { type: "string", enum: ["fuzzy", "strict"], description: "Auto-bind matching mode (default: fuzzy)" },
},
required: ["path", "spec"],
},
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
switch (toolName) {
case "prefab_list":
return this.listPrefabs();
case "prefab_create":
return this.createPrefab(args.uuid, args.path);
case "prefab_instantiate":
return this.instantiatePrefab(args.prefabUuid, args.parent);
case "prefab_get_info":
return this.getPrefabInfo(args.uuid);
case "prefab_update":
return this.updatePrefab(args.uuid);
case "prefab_duplicate": {
try {
await (Editor.Message.request as any)("asset-db", "copy-asset", args.source, args.destination);
return ok({ success: true, source: args.source, destination: args.destination });
} catch (e: any) { return err(e.message || String(e)); }
}
case "prefab_validate": {
try {
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", args.uuid);
const deps = await (Editor.Message.request as any)("asset-db", "query-depends", args.uuid).catch(() => []);
return ok({ success: true, uuid: args.uuid, info, dependencies: deps, valid: !!info });
} catch (e: any) { return err(e.message || String(e)); }
}
case "prefab_revert":
return this.revertPrefab(args.uuid);
case "prefab_create_and_replace":
return this.createAndReplace(args.uuid, args.path);
case "prefab_open":
return this.openPrefab(args.uuid, args.path, !!args.force);
case "prefab_close":
return this.closePrefab(args.save !== false, args.sceneUuid, !!args.force);
case "prefab_create_from_spec":
return this.createFromSpec(args.path, parseMaybeJson(args.spec), args.autoBindMode ?? "fuzzy");
default:
return err(`Unknown tool: ${toolName}`);
}
}
private async listPrefabs(): Promise<ToolResult> {
try {
const results = await Editor.Message.request("asset-db", "query-assets", {
pattern: "db://assets/**/*.prefab",
});
const prefabs = (results || []).map((a: any) => ({
uuid: a.uuid,
path: a.path || a.url,
name: a.name,
}));
return ok({ success: true, prefabs });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async createPrefab(nodeUuid: string, path: string): Promise<ToolResult> {
try {
// 既存Prefabがある場合は警告を返す(上書きダイアログでタイムアウトするため)
const existing = await this.assetExists(path);
if (existing) {
return err(
`Prefab already exists at "${path}". Use prefab_update instead to update an existing prefab. ` +
`Workflow: 1) prefab_instantiate to place in scene, 2) modify properties, 3) prefab_update to save.`
);
}
const result = await (Editor.Message.request as any)("scene", "create-prefab", nodeUuid, path);
return ok({ success: true, nodeUuid, path, result });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async assetExists(path: string): Promise<boolean> {
try {
const pattern = path.replace(/\.prefab$/, "") + ".*";
const results = await Editor.Message.request("asset-db", "query-assets", { pattern });
return (results || []).length > 0;
} catch {
try {
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", path);
return !!info;
} catch {
return false;
}
}
}
private async instantiatePrefab(prefabUuid: string, parent?: string): Promise<ToolResult> {
try {
const nodeUuid = await Editor.Message.request("scene", "create-node", {
parent: parent || undefined,
assetUuid: prefabUuid,
});
// Prefab 編集モード中の場合、ネスト Prefab 情報を記憶
// prefab_update 時に JSON 後処理で asset/instance/nestedPrefabInstanceRoots を設定
if (parent) {
this._pendingNestedPrefabs.push({
nodeUuid,
prefabAssetUuid: prefabUuid,
parentUuid: parent,
});
}
return ok({ success: true, nodeUuid, prefabUuid });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async updatePrefab(nodeUuid: string): Promise<ToolResult> {
try {
const result = await (Editor.Message.request as any)("scene", "apply-prefab", nodeUuid);
// ネスト Prefab の JSON 後処理
if (this._pendingNestedPrefabs.length > 0) {
await this._fixNestedPrefabJson(nodeUuid);
}
return ok({ success: true, nodeUuid, result });
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* prefab_update 後に Prefab JSON を後処理して、ネスト Prefab 参照を正しく設定する.
*/
private async _fixNestedPrefabJson(_rootNodeUuid: string): Promise<void> {
if (this._pendingNestedPrefabs.length === 0) return;
try {
// シーンを保存して Prefab JSON を書き出す
// 現在シーンが untitled (scene-2d) の場合、save-scene はダイアログを出すので
// safeSaveScene でスキップする。untitled でシーンにインスタンスが居るケースは
// 本来 prefab_open モードでのみ発生するので save が効く想定だが、
// テスト等で直接呼ばれた場合の保護として skip する。
const saved = await safeSaveScene();
if (!saved) {
console.warn(
"[PrefabTools] _fixNestedPrefabJson: save-scene skipped (untitled scene). " +
"Nested prefab JSON post-processing may be incomplete."
);
return;
}
await new Promise(r => setTimeout(r, 1500));
// prefab_open で記憶した UUID からファイルパスを取得
if (!this._currentPrefabUuid) return;
const prefabPath = await (Editor.Message.request as any)(
"asset-db", "query-path", this._currentPrefabUuid
);
if (!prefabPath) return;
if (!fs.existsSync(prefabPath)) return;
const data = JSON.parse(fs.readFileSync(prefabPath, "utf-8"));
// 各ネスト Prefab エントリを処理
for (const entry of this._pendingNestedPrefabs) {
// fileId でノードを検索(nodeUuid はシーン内 UUID、Prefab JSON 内では fileId)
let flpNodeIdx = -1;
for (let i = 0; i < data.length; i++) {
if (data[i].__type__ === "cc.PrefabInfo" && data[i].fileId === entry.nodeUuid) {
// この PrefabInfo を持つノードを探す
for (let j = 0; j < data.length; j++) {
if (data[j]._prefab?.__id__ === i) {
flpNodeIdx = j;
break;
}
}
break;
}
}
// fileId で見つからない場合、ノード名で検索
if (flpNodeIdx < 0) {
// Prefab アセット名を取得
const assetInfo = await (Editor.Message.request as any)("asset-db", "query-asset-info", entry.prefabAssetUuid);
const assetName = assetInfo?.name?.replace(".prefab", "") || "";
for (let i = 0; i < data.length; i++) {
if (data[i].__type__ === "cc.Node" && (data[i]._name === assetName || data[i]._name === undefined)) {
const prefabIdx = data[i]._prefab?.__id__;
if (prefabIdx != null && data[prefabIdx]?.asset?.__id__ === 0 && !data[prefabIdx]?.instance) {
flpNodeIdx = i;
break;
}
}
}
}
if (flpNodeIdx < 0) continue;
const prefabInfoIdx = data[flpNodeIdx]._prefab?.__id__;
if (prefabInfoIdx == null) continue;
// PrefabInfo を修正
const prefabInfo = data[prefabInfoIdx];
prefabInfo.root = { __id__: flpNodeIdx };
prefabInfo.asset = {
__uuid__: entry.prefabAssetUuid,
__expectedType__: "cc.Prefab",
};
// PrefabInstance を追加
if (!prefabInfo.instance) {
const instanceIdx = data.length;
data.push({
__type__: "cc.PrefabInstance",
fileId: crypto.randomBytes(16).toString("base64").replace(/[+/=]/g, "").substring(0, 22),
prefabRootNode: { __id__: 1 }, // Prefab 編集モードのルート
mountedChildren: [],
mountedComponents: [],
propertyOverrides: [],
removedComponents: [],
});
prefabInfo.instance = { __id__: instanceIdx };
}
// 子ノード・コンポーネントをクリア(Prefab アセットから復元される)
data[flpNodeIdx]._children = [];
data[flpNodeIdx]._components = [];
// ルートの nestedPrefabInstanceRoots に追加
const rootPrefabIdx = data[1]._prefab?.__id__;
if (rootPrefabIdx != null) {
const rootPrefab = data[rootPrefabIdx];
if (!rootPrefab.nestedPrefabInstanceRoots) {
rootPrefab.nestedPrefabInstanceRoots = [];
}
const alreadyNested = rootPrefab.nestedPrefabInstanceRoots.some(
(r: any) => r?.__id__ === flpNodeIdx
);
if (!alreadyNested) {
rootPrefab.nestedPrefabInstanceRoots.push({ __id__: flpNodeIdx });
}
}
}
fs.writeFileSync(prefabPath, JSON.stringify(data, null, 2), "utf-8");
this._pendingNestedPrefabs = [];
} catch (e: any) {
console.warn("[PrefabTools] _fixNestedPrefabJson failed:", e.message);
}
}
private async revertPrefab(nodeUuid: string): Promise<ToolResult> {
try {
const result = await (Editor.Message.request as any)("scene", "revert-prefab", nodeUuid);
return ok({ success: true, nodeUuid, result });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getPrefabInfo(uuid: string): Promise<ToolResult> {
try {
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", uuid);
return ok({ success: true, info });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async createAndReplace(nodeUuid: string, path: string): Promise<ToolResult> {
try {
// 1. Check if prefab already exists
const existing = await this.assetExists(path);
if (existing) {
return err(
`Prefab already exists at "${path}". Delete it first or use a different path.`
);
}
// 2. Get node info (parent, sibling index, transform) before creating prefab
const nodeInfo = await this.sceneScript("getNodeInfo", [nodeUuid]);
if (!nodeInfo?.success) {
return err(`Node ${nodeUuid} not found`);
}
const parentUuid = nodeInfo.data?.parent;
// 3. Create prefab from the node
const prefabAssetUuid = await (Editor.Message.request as any)("scene", "create-prefab", nodeUuid, path);
if (!prefabAssetUuid) {
return err("create-prefab returned no asset UUID");
}
// 4. Delete the original node
await (Editor.Message.request as any)("scene", "remove-node", { uuid: nodeUuid });
// 5. Instantiate the prefab at the same parent
const newNodeUuid = await Editor.Message.request("scene", "create-node", {
parent: parentUuid || undefined,
assetUuid: prefabAssetUuid,
});
return ok({
success: true,
prefabAssetUuid,
prefabPath: path,
originalNodeUuid: nodeUuid,
newInstanceUuid: newNodeUuid,
});
} catch (e: any) {
return err(e.message || String(e));
}
}
private async openPrefab(uuid?: string, path?: string, force: boolean = false): Promise<ToolResult> {
try {
// Prefab を開くときも内部的にシーン切替が発生するので dirty untitled チェック
await ensureSceneSafeToSwitch(force);
// Resolve UUID from path if needed
let assetUuid = uuid;
if (!assetUuid && path) {
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", path);
assetUuid = info?.uuid;
}
if (!assetUuid) {
return err("Either uuid or path is required");
}
// Open prefab in editing mode (equivalent to double-click)
await (Editor.Message.request as any)("asset-db", "open-asset", assetUuid);
// Wait for prefab editing mode to initialize
await new Promise(r => setTimeout(r, 1000));
this._currentPrefabUuid = assetUuid;
this._pendingNestedPrefabs = [];
return ok({ success: true, uuid: assetUuid, mode: "prefab-edit" });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async closePrefab(save: boolean, sceneUuid?: string, force: boolean = false): Promise<ToolResult> {
try {
// 1. Save prefab if requested
// prefab edit モード中は "current scene" = 編集中 Prefab なので save-scene は Prefab を保存する。
// ただし untitled フォールバックに当たった場合はダイアログ回避のためスキップする。
if (save) {
await safeSaveScene();
await new Promise(r => setTimeout(r, 500));
}
// 2. Determine which scene to return to
let targetScene = sceneUuid;
if (!targetScene) {
// Try project's start scene
try {
targetScene = await (Editor as any).Profile.getConfig("preview", "general.start_scene", "local");
} catch { /* ignore */ }
// Fallback to first scene
if (!targetScene || targetScene === "current_scene") {
const scenes = await Editor.Message.request("asset-db", "query-assets", {
ccType: "cc.SceneAsset",
pattern: "db://assets/**/*",
});
if (Array.isArray(scenes) && scenes.length > 0) {
targetScene = scenes[0].uuid;
}
}
}
// 3. Open the scene
if (targetScene) {
// prefab edit モードから戻る遷移もダイアログが出うる
await ensureSceneSafeToSwitch(force);
await (Editor.Message.request as any)("scene", "open-scene", targetScene);
await new Promise(r => setTimeout(r, 1000));
}
return ok({ success: true, returnedToScene: targetScene });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async createFromSpec(prefabPath: string, spec: any, autoBindMode: string): Promise<ToolResult> {
try {
// 1. 既存 Prefab チェック
const existing = await this.assetExists(prefabPath);
if (existing) {
return err(
`Prefab already exists at "${prefabPath}". Delete it first or use a different path.`
);
}
// 2. シーンの最初の cc.Node UUID を取得(Scene UUID ではなく Canvas 等)
const hier = await this.sceneScript("getSceneHierarchy", [false]);
const hierarchy = hier?.hierarchy || [];
const firstNode = hierarchy[0];
if (!firstNode?.uuid) return err("Could not find a node in the current scene to use as parent");
const parentUuid = firstNode.uuid;
// 3. ノードツリーを構築
const autoBind = spec.autoBind;
const cleanSpec = { ...spec };
delete cleanSpec.autoBind;
const treeResult = await this.sceneScript("buildNodeTree", [parentUuid, cleanSpec]);
if (!treeResult?.success) return err(treeResult?.error || "buildNodeTree failed");
const nodeUuid = treeResult.data?.uuid;
if (!nodeUuid) return err("buildNodeTree returned no root node UUID");
// 4. フォント・SpriteFrame を Editor API 経由で設定(アセット依存追跡のため)
await this._applyDefaultAssets(nodeUuid);
// 5. autoBind 実行 (旧4)
let autoBindResult: any = null;
if (autoBind) {
if (!this._componentTools) {
return err("autoBind requires ComponentTools dependency (internal configuration error)");
}
const bindToolResult = await this._componentTools.execute("component_auto_bind", {
uuid: nodeUuid,
componentType: autoBind,
force: false,
mode: autoBindMode,
});
try {
autoBindResult = JSON.parse(bindToolResult.content[0].text);
} catch { autoBindResult = bindToolResult; }
}
// 6. Prefab 作成
const prefabAssetUuid = await (Editor.Message.request as any)(
"scene", "create-prefab", nodeUuid, prefabPath
);
if (!prefabAssetUuid) {
await (Editor.Message.request as any)("scene", "remove-node", { uuid: nodeUuid });
return err("create-prefab returned no asset UUID");
}
// 7. 一時ノードを削除
await (Editor.Message.request as any)("scene", "remove-node", { uuid: nodeUuid });
return ok({
success: true,
prefabAssetUuid,
path: prefabPath,
nodeTree: treeResult.data,
autoBind: autoBindResult,
});
} catch (e: any) {
return err(e.message || String(e));
}
}
/**
* buildNodeTree で作成したノードツリーの Label にプロジェクトフォントを設定する。
* Editor API (scene:set-property) 経由で設定することでアセット依存が正しく追跡される。
*/
private async _applyDefaultAssets(rootUuid: string): Promise<void> {
// プロジェクトのデフォルトフォントを検索(resources/fonts/ 配下の TTFFont)
let fontUuid: string | null = null;
try {
const assets = await Editor.Message.request("asset-db", "query-assets", {
pattern: "db://assets/resources/fonts/**",
ccType: "cc.TTFFont",
});
if (Array.isArray(assets) && assets.length > 0) {
fontUuid = assets[0].uuid;
}
} catch { /* ignore */ }
if (!fontUuid) return;
// 全子孫ノードを取得
const descendants = await this.sceneScript("getAllDescendants", [rootUuid]);
if (!descendants?.success) return;
const allNodes = [{ uuid: rootUuid, name: "root" }, ...descendants.data];
for (const node of allNodes) {
try {
const nodeDump = await (Editor.Message.request as any)("scene", "query-node", node.uuid);
if (!nodeDump) continue;
const comps = nodeDump.__comps__ || [];
for (let i = 0; i < comps.length; i++) {
const compType = comps[i].type || "";
// Label にフォント設定
if (compType === "cc.Label") {
const fontDump = comps[i].value?.font;
if (!fontDump?.value?.uuid) {
await (Editor.Message.request as any)("scene", "set-property", {
uuid: node.uuid,
path: `__comps__.${i}.font`,
dump: { type: "cc.TTFFont", value: { uuid: fontUuid } },
});
}
}
}
} catch { /* skip nodes that can't be queried */ }
}
}
private async sceneScript(method: string, args: any[]): Promise<any> {
return Editor.Message.request("scene", "execute-scene-script", {
name: "cocos-creator-mcp",
method,
args,
});
}
}
@@ -1,84 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
export class PreferencesTools implements ToolCategory {
readonly categoryName = "preferences";
getTools(): ToolDefinition[] {
return [
{
name: "preferences_get",
description: "Get a preference value by key.",
inputSchema: {
type: "object",
properties: {
protocol: { type: "string", description: "Protocol name (e.g. 'general', 'builder', 'engine')" },
key: { type: "string", description: "Preference key" },
},
required: ["protocol", "key"],
},
},
{
name: "preferences_set",
description: "Set a preference value.",
inputSchema: {
type: "object",
properties: {
protocol: { type: "string", description: "Protocol name" },
key: { type: "string", description: "Preference key" },
value: { description: "Value to set" },
},
required: ["protocol", "key", "value"],
},
},
{
name: "preferences_get_all",
description: "Get all preferences for a given protocol.",
inputSchema: {
type: "object",
properties: {
protocol: { type: "string", description: "Protocol name (e.g. 'general')" },
},
required: ["protocol"],
},
},
{
name: "preferences_reset",
description: "Reset a preference to its default value.",
inputSchema: {
type: "object",
properties: {
protocol: { type: "string", description: "Protocol name" },
key: { type: "string", description: "Preference key to reset" },
},
required: ["protocol", "key"],
},
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "preferences_get": {
const value = Editor.Profile.getConfig(args.protocol, args.key);
return ok({ success: true, protocol: args.protocol, key: args.key, value });
}
case "preferences_set":
Editor.Profile.setConfig(args.protocol, args.key, args.value);
return ok({ success: true, protocol: args.protocol, key: args.key });
case "preferences_get_all": {
const config = Editor.Profile.getConfig(args.protocol);
return ok({ success: true, protocol: args.protocol, config });
}
case "preferences_reset":
Editor.Profile.removeConfig(args.protocol, args.key);
return ok({ success: true, protocol: args.protocol, key: args.key });
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,167 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
export class ProjectTools implements ToolCategory {
readonly categoryName = "project";
getTools(): ToolDefinition[] {
return [
{
name: "project_get_info",
description: "Get project information (name, path, engine version).",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "project_refresh_assets",
description: "Refresh the asset database to detect file changes.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "project_get_asset_info",
description: "Get information about an asset by UUID.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Asset UUID" },
},
required: ["uuid"],
},
},
{
name: "project_find_asset",
description: "Find assets by name pattern (glob). Returns matching asset paths and UUIDs.",
inputSchema: {
type: "object",
properties: {
pattern: { type: "string", description: "Glob pattern (e.g. 'db://assets/**/*.ts', 'db://assets/**/Button*')" },
},
required: ["pattern"],
},
},
{
name: "project_get_settings",
description: "Get project settings for a given protocol.",
inputSchema: {
type: "object",
properties: {
protocol: { type: "string", description: "Settings protocol (e.g. 'general', 'engine')" },
},
},
},
{
name: "project_set_settings",
description: "Set a project setting.",
inputSchema: {
type: "object",
properties: {
protocol: { type: "string" },
key: { type: "string" },
value: {},
},
required: ["protocol", "key", "value"],
},
},
{
name: "project_get_engine_info",
description: "Get engine version and path information.",
inputSchema: { type: "object", properties: {} },
},
{
name: "project_query_scripts",
description: "Query all script plugins in the project.",
inputSchema: { type: "object", properties: {} },
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
switch (toolName) {
case "project_get_info":
return this.getInfo();
case "project_refresh_assets":
return this.refreshAssets();
case "project_get_asset_info":
return this.getAssetInfo(args.uuid);
case "project_find_asset":
return this.findAsset(args.pattern);
case "project_get_settings": {
try {
const config = await (Editor.Message.request as any)("project", "query-config", args.protocol || "general");
return ok({ success: true, config });
} catch (e: any) { return err(e.message || String(e)); }
}
case "project_set_settings": {
try {
await (Editor.Message.request as any)("project", "set-config", args.protocol, args.key, args.value);
return ok({ success: true });
} catch (e: any) { return err(e.message || String(e)); }
}
case "project_get_engine_info": {
try {
const info = await (Editor.Message.request as any)("engine", "query-info");
return ok({ success: true, info });
} catch (e: any) { return err(e.message || String(e)); }
}
case "project_query_scripts": {
try {
const scripts = await (Editor.Message.request as any)("programming", "query-sorted-plugins");
return ok({ success: true, scripts });
} catch (e: any) { return err(e.message || String(e)); }
}
default:
return err(`Unknown tool: ${toolName}`);
}
}
private async getInfo(): Promise<ToolResult> {
try {
return ok({
success: true,
name: Editor.Project.name,
path: Editor.Project.path,
tmpDir: Editor.Project.tmpDir,
});
} catch (e: any) {
return err(e.message || String(e));
}
}
private async refreshAssets(): Promise<ToolResult> {
try {
await Editor.Message.request("asset-db", "refresh-asset", "db://assets");
return ok({ success: true });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getAssetInfo(uuid: string): Promise<ToolResult> {
try {
const info = await (Editor.Message.request as any)("asset-db", "query-asset-info", uuid);
return ok({ success: true, info });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async findAsset(pattern: string): Promise<ToolResult> {
try {
const results = await Editor.Message.request("asset-db", "query-assets", { pattern });
const assets = (results || []).map((a: any) => ({
uuid: a.uuid,
path: a.path || a.url,
name: a.name,
type: a.type,
}));
return ok({ success: true, assets });
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,150 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
export class ReferenceImageTools implements ToolCategory {
readonly categoryName = "referenceImage";
getTools(): ToolDefinition[] {
return [
{
name: "refimage_add",
description: "Add a reference image to the scene view.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "Image file path or db:// path" },
},
required: ["path"],
},
},
{
name: "refimage_remove",
description: "Remove a reference image by index.",
inputSchema: {
type: "object",
properties: {
index: { type: "number", description: "Image index" },
},
required: ["index"],
},
},
{
name: "refimage_list",
description: "List all reference images in the scene view.",
inputSchema: { type: "object", properties: {} },
},
{
name: "refimage_clear_all",
description: "Remove all reference images.",
inputSchema: { type: "object", properties: {} },
},
{
name: "refimage_switch",
description: "Switch to a specific reference image by index.",
inputSchema: {
type: "object",
properties: {
index: { type: "number", description: "Image index to switch to" },
},
required: ["index"],
},
},
{
name: "refimage_set_position",
description: "Set the position of the current reference image.",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
},
required: ["x", "y"],
},
},
{
name: "refimage_set_scale",
description: "Set the scale of the current reference image.",
inputSchema: {
type: "object",
properties: {
scale: { type: "number", description: "Scale factor" },
},
required: ["scale"],
},
},
{
name: "refimage_set_opacity",
description: "Set the opacity of the current reference image.",
inputSchema: {
type: "object",
properties: {
opacity: { type: "number", description: "Opacity (0-255)" },
},
required: ["opacity"],
},
},
{
name: "refimage_query_config",
description: "Get the current reference image configuration.",
inputSchema: { type: "object", properties: {} },
},
{
name: "refimage_query_current",
description: "Get info about the currently active reference image.",
inputSchema: { type: "object", properties: {} },
},
{
name: "refimage_refresh",
description: "Refresh the reference image display.",
inputSchema: { type: "object", properties: {} },
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "refimage_add":
await (Editor.Message.request as any)("scene", "add-reference-image", args.path);
return ok({ success: true, path: args.path });
case "refimage_remove":
await (Editor.Message.request as any)("scene", "remove-reference-image", args.index);
return ok({ success: true, index: args.index });
case "refimage_list": {
const config = await (Editor.Message.request as any)("scene", "query-reference-image-config").catch(() => null);
return ok({ success: true, config });
}
case "refimage_clear_all":
await (Editor.Message.request as any)("scene", "clear-all-reference-images");
return ok({ success: true });
case "refimage_switch":
await (Editor.Message.request as any)("scene", "switch-reference-image", args.index);
return ok({ success: true, index: args.index });
case "refimage_set_position":
await (Editor.Message.request as any)("scene", "set-reference-image-position", args.x, args.y);
return ok({ success: true, x: args.x, y: args.y });
case "refimage_set_scale":
await (Editor.Message.request as any)("scene", "set-reference-image-scale", args.scale);
return ok({ success: true, scale: args.scale });
case "refimage_set_opacity":
await (Editor.Message.request as any)("scene", "set-reference-image-opacity", args.opacity);
return ok({ success: true, opacity: args.opacity });
case "refimage_query_config": {
const config = await (Editor.Message.request as any)("scene", "query-reference-image-config").catch(() => null);
return ok({ success: true, config });
}
case "refimage_query_current": {
const current = await (Editor.Message.request as any)("scene", "query-current-reference-image").catch(() => null);
return ok({ success: true, current });
}
case "refimage_refresh":
await (Editor.Message.request as any)("scene", "refresh-reference-image");
return ok({ success: true });
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,635 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
import { ensureSceneSafeToSwitch } from "./scene-tools";
const EXT_NAME = "cocos-creator-mcp";
export class SceneAdvancedTools implements ToolCategory {
readonly categoryName = "sceneAdvanced";
getTools(): ToolDefinition[] {
return [
{
name: "scene_execute_script",
description: "Execute a scene script method by name with arguments.",
inputSchema: {
type: "object",
properties: {
method: { type: "string", description: "Scene script method name" },
args: { type: "array", description: "Arguments to pass", items: {} },
},
required: ["method"],
},
},
{
name: "scene_snapshot",
description: "Take a snapshot of the current scene state (for undo).",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_query_dirty",
description: "Check if the current scene has unsaved changes.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_query_classes",
description: "Query all available component classes in the scene.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_query_components",
description: "Query available components for a given node.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
},
required: ["uuid"],
},
},
{
name: "scene_query_node_tree",
description: "Query the raw node tree from the editor (alternative to scene_get_hierarchy).",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_query_nodes_by_asset",
description: "Find all nodes that reference a given asset UUID.",
inputSchema: {
type: "object",
properties: {
assetUuid: { type: "string", description: "Asset UUID to search for" },
},
required: ["assetUuid"],
},
},
{
name: "scene_soft_reload",
description: "Soft reload the current scene without losing state.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_reset_node_transform",
description: "Reset a node's transform to default (position 0,0,0 / rotation 0,0,0 / scale 1,1,1).",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
},
required: ["uuid"],
},
},
{
name: "scene_copy_node",
description: "Copy a node to clipboard.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node UUID" },
},
required: ["uuid"],
},
},
{
name: "scene_paste_node",
description: "Paste node from clipboard under a parent.",
inputSchema: {
type: "object",
properties: {
parentUuid: { type: "string", description: "Parent node UUID" },
},
required: ["parentUuid"],
},
},
{
name: "scene_create",
description: "Create a new empty 2D scene. If path is omitted, uses the editor's built-in new-scene command (may not work on CC 3.8.x). If path is specified, creates a .scene file via asset-db as a fallback. Returns an error if the current scene is dirty and untitled (to avoid modal save dialog); pass force=true to bypass.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "Scene asset path (e.g. 'db://assets/scenes/NewScene.scene'). If omitted, uses editor's new-scene command." },
force: { type: "boolean", description: "Skip dirty-scene preflight check (may trigger modal save dialog)" },
},
},
},
{
name: "scene_cut_node",
description: "Cut a node to clipboard (removes from scene).",
inputSchema: {
type: "object",
properties: { uuid: { type: "string", description: "Node UUID" } },
required: ["uuid"],
},
},
{
name: "scene_reset_property",
description: "Reset a specific property on a node or component to its default value.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node or component UUID" },
path: { type: "string", description: "Property path (e.g. 'position', 'color')" },
},
required: ["uuid", "path"],
},
},
{
name: "scene_reset_component",
description: "Reset a component to its default state.",
inputSchema: {
type: "object",
properties: { uuid: { type: "string", description: "Component UUID" } },
required: ["uuid"],
},
},
{
name: "scene_execute_component_method",
description: "Call a method on a component at edit-time.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Component UUID" },
method: { type: "string", description: "Method name" },
args: { type: "array", description: "Method arguments", items: {} },
},
required: ["uuid", "method"],
},
},
{
name: "scene_move_array_element",
description: "Move an array element to a new position in a property.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node or component UUID" },
path: { type: "string", description: "Array property path" },
target: { type: "number", description: "Current index" },
offset: { type: "number", description: "Move offset (+1 = down, -1 = up)" },
},
required: ["uuid", "path", "target", "offset"],
},
},
{
name: "scene_remove_array_element",
description: "Remove an element from an array property by index.",
inputSchema: {
type: "object",
properties: {
uuid: { type: "string", description: "Node or component UUID" },
path: { type: "string", description: "Array property path" },
index: { type: "number", description: "Index to remove" },
},
required: ["uuid", "path", "index"],
},
},
{
name: "scene_snapshot_abort",
description: "Abort the current undo snapshot.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_query_ready",
description: "Check if the scene is fully loaded and ready.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_query_component_has_script",
description: "Check if a component has an associated script file.",
inputSchema: {
type: "object",
properties: { name: { type: "string", description: "Component class name" } },
required: ["name"],
},
},
{
name: "scene_restore_prefab",
description: "Restore a prefab node to its original prefab state.",
inputSchema: {
type: "object",
properties: { uuid: { type: "string", description: "Node UUID" } },
required: ["uuid"],
},
},
{
name: "scene_begin_undo",
description: "Begin recording undo operations.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_end_undo",
description: "End undo recording and save the undo step.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_cancel_undo",
description: "Cancel the current undo recording.",
inputSchema: { type: "object", properties: {} },
},
{
name: "scene_save_as",
description: "Save the current scene to a new file (shows save dialog).",
inputSchema: { type: "object", properties: {} },
},
// ── 以下、既存MCP未対応のEditor API ──
{
name: "scene_set_parent",
description: "Reparent node(s) using the official Editor API (alternative to node_move).",
inputSchema: {
type: "object",
properties: {
uuids: { type: "array", items: { type: "string" }, description: "Node UUID(s) to move" },
parent: { type: "string", description: "New parent node UUID" },
keepWorldTransform: { type: "boolean", description: "Keep world position (default false)" },
},
required: ["uuids", "parent"],
},
},
{
name: "scene_query_node",
description: "Get a full property dump of a node (all serialized data).",
inputSchema: {
type: "object",
properties: { uuid: { type: "string", description: "Node UUID" } },
required: ["uuid"],
},
},
{
name: "scene_query_component",
description: "Get a full property dump of a component by its UUID.",
inputSchema: {
type: "object",
properties: { uuid: { type: "string", description: "Component UUID" } },
required: ["uuid"],
},
},
{
name: "scene_query_scene_bounds",
description: "Get the bounding rect of the current scene.",
inputSchema: { type: "object", properties: {} },
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "scene_execute_script":
return ok(await this.sceneScript(args.method, args.args || []));
case "scene_snapshot":
return ok(await (Editor.Message.request as any)("scene", "snapshot"));
case "scene_query_dirty": {
const dirty = await (Editor.Message.request as any)("scene", "query-dirty");
return ok({ success: true, dirty });
}
case "scene_query_classes": {
const classes = await (Editor.Message.request as any)("scene", "query-classes");
return ok({ success: true, classes });
}
case "scene_query_components": {
const comps = await (Editor.Message.request as any)("scene", "query-components", args.uuid);
return ok({ success: true, components: comps });
}
case "scene_query_node_tree": {
const tree = await Editor.Message.request("scene", "query-node-tree");
return ok({ success: true, tree });
}
case "scene_query_nodes_by_asset": {
const nodes = await (Editor.Message.request as any)("scene", "query-nodes-by-asset-uuid", args.assetUuid);
return ok({ success: true, nodes });
}
case "scene_soft_reload":
await (Editor.Message.request as any)("scene", "soft-reload");
return ok({ success: true });
case "scene_reset_node_transform":
return await this.resetTransform(args.uuid);
case "scene_copy_node":
await (Editor.Message.request as any)("scene", "copy-node", args.uuid);
return ok({ success: true, uuid: args.uuid });
case "scene_paste_node": {
const result = await (Editor.Message.request as any)("scene", "paste-node", args.parentUuid);
return ok({ success: true, result });
}
case "scene_create":
return this.createScene(args.path, !!args.force);
case "scene_cut_node":
await (Editor.Message.request as any)("scene", "cut-node", args.uuid);
return ok({ success: true, uuid: args.uuid });
case "scene_reset_property":
await (Editor.Message.request as any)("scene", "reset-property", { uuid: args.uuid, path: args.path });
return ok({ success: true });
case "scene_reset_component":
await (Editor.Message.request as any)("scene", "reset-component", { uuid: args.uuid });
return ok({ success: true });
case "scene_execute_component_method": {
const result = await (Editor.Message.request as any)("scene", "execute-component-method", { uuid: args.uuid, name: args.method, args: args.args || [] });
return ok({ success: true, result });
}
case "scene_move_array_element":
await (Editor.Message.request as any)("scene", "move-array-element", { uuid: args.uuid, path: args.path, target: args.target, offset: args.offset });
return ok({ success: true });
case "scene_remove_array_element":
await (Editor.Message.request as any)("scene", "remove-array-element", { uuid: args.uuid, path: args.path, index: args.index });
return ok({ success: true });
case "scene_snapshot_abort":
await (Editor.Message.request as any)("scene", "snapshot-abort");
return ok({ success: true });
case "scene_query_ready": {
const ready = await (Editor.Message.request as any)("scene", "query-is-ready");
return ok({ success: true, ready });
}
case "scene_query_component_has_script": {
const hasScript = await (Editor.Message.request as any)("scene", "query-component-has-script", args.name);
return ok({ success: true, name: args.name, hasScript });
}
case "scene_restore_prefab":
await (Editor.Message.request as any)("scene", "restore-prefab", { uuid: args.uuid });
return ok({ success: true, uuid: args.uuid });
case "scene_begin_undo":
await (Editor.Message.request as any)("scene", "begin-recording");
return ok({ success: true });
case "scene_end_undo":
await (Editor.Message.request as any)("scene", "end-recording");
return ok({ success: true });
case "scene_cancel_undo":
await (Editor.Message.request as any)("scene", "cancel-recording");
return ok({ success: true });
case "scene_save_as": {
const result = await (Editor.Message.request as any)("scene", "save-as-scene");
return ok({ success: true, result });
}
case "scene_set_parent":
await (Editor.Message.request as any)("scene", "set-parent", {
parent: args.parent,
uuids: args.uuids,
keepWorldTransform: args.keepWorldTransform || false,
});
return ok({ success: true });
case "scene_query_node": {
const dump = await (Editor.Message.request as any)("scene", "query-node", args.uuid);
return ok({ success: true, node: dump });
}
case "scene_query_component": {
const dump = await (Editor.Message.request as any)("scene", "query-component", args.uuid);
return ok({ success: true, component: dump });
}
case "scene_query_scene_bounds": {
const bounds = await (Editor.Message.request as any)("scene", "query-scene-bounds");
return ok({ success: true, bounds });
}
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
private async resetTransform(uuid: string): Promise<ToolResult> {
const result = await this.sceneScript("setNodeProperty", [uuid, "position", { x: 0, y: 0, z: 0 }]);
await this.sceneScript("setNodeProperty", [uuid, "rotation", { x: 0, y: 0, z: 0 }]);
await this.sceneScript("setNodeProperty", [uuid, "scale", { x: 1, y: 1, z: 1 }]);
return ok({ success: true, uuid });
}
private async createScene(path?: string, force: boolean = false): Promise<ToolResult> {
// ダイアログ割り込み防止: 現在シーンが dirty かつ untitled の場合は事前エラー
try { await ensureSceneSafeToSwitch(force); }
catch (e: any) { return err(e.message || String(e)); }
// まず scene:new-scene を試行(path 未指定時のみ)
if (!path) {
try {
await (Editor.Message.request as any)("scene", "new-scene");
return ok({ success: true });
} catch (e: any) {
const msg = e?.message || String(e);
if (msg.includes("Message does not exist") || msg.includes("scene - new-scene")) {
// CC 3.8.x → asset-db fallback にフォール
const fallbackPath = await this.generateAvailableScenePath();
return this.createSceneViaAssetDb(fallbackPath);
}
return err(msg);
}
}
// path 指定 → asset-db fallback
return this.createSceneViaAssetDb(path);
}
private async generateAvailableScenePath(): Promise<string> {
const basePath = "db://assets/NewScene.scene";
try {
const result = await (Editor.Message.request as any)("asset-db", "generate-available-url", basePath);
if (result) return result;
} catch { /* fallback */ }
return `db://assets/NewScene_${Date.now()}.scene`;
}
private async createSceneViaAssetDb(path: string): Promise<ToolResult> {
try {
if (!path.endsWith(".scene")) path += ".scene";
const sceneName = path.split("/").pop()!.replace(".scene", "");
const uid = () => crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const sid = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let s = "";
for (let i = 0; i < 21; i++) s += chars[Math.floor(Math.random() * chars.length)];
return s;
};
const sceneJson = this.buildMinimalSceneJson(sceneName, uid, sid);
const content = JSON.stringify(sceneJson, null, 2);
await (Editor.Message.request as any)("asset-db", "create-asset", path, content);
// シーンを開く
try {
// ensureSceneSafeToSwitch は createScene 入口で既に通過済みなのでここでは再チェックしない
const queryResult = await (Editor.Message.request as any)("asset-db", "query-uuid", path);
if (queryResult) {
await (Editor.Message.request as any)("scene", "open-scene", queryResult);
}
} catch { /* open failure is not critical */ }
return ok({ success: true, path, method: "asset-db-fallback" });
} catch (e: any) {
return err(e.message || String(e));
}
}
private buildMinimalSceneJson(name: string, uid: () => string, sid: () => string): any[] {
const sceneId = uid();
const canvasNodeId = sid();
const cameraNodeId = sid();
const vec3 = (x: number, y: number, z: number) => ({ __type__: "cc.Vec3", x, y, z });
const quat = () => ({ __type__: "cc.Quat", x: 0, y: 0, z: 0, w: 1 });
return [
// [0] SceneAsset
{
__type__: "cc.SceneAsset",
_name: name,
_objFlags: 0,
__editorExtras__: {},
_native: "",
scene: { __id__: 1 },
},
// [1] Scene
{
__type__: "cc.Scene",
_name: name,
_objFlags: 0,
__editorExtras__: {},
_parent: null,
_children: [{ __id__: 2 }],
_active: true,
_components: [],
_prefab: null,
_lpos: vec3(0, 0, 0),
_lrot: quat(),
_lscale: vec3(1, 1, 1),
_mobility: 0,
_layer: 1073741824,
_euler: vec3(0, 0, 0),
autoReleaseAssets: false,
_globals: { __id__: 10 },
_id: sceneId,
},
// [2] Canvas node
{
__type__: "cc.Node",
_name: "Canvas",
_objFlags: 0,
__editorExtras__: {},
_parent: { __id__: 1 },
_children: [{ __id__: 3 }],
_active: true,
_components: [{ __id__: 5 }, { __id__: 6 }, { __id__: 7 }],
_prefab: null,
_lpos: vec3(0, 0, 0),
_lrot: quat(),
_lscale: vec3(1, 1, 1),
_mobility: 0,
_layer: 33554432,
_euler: vec3(0, 0, 0),
_id: canvasNodeId,
},
// [3] Camera node
{
__type__: "cc.Node",
_name: "Camera",
_objFlags: 0,
__editorExtras__: {},
_parent: { __id__: 2 },
_children: [],
_active: true,
_components: [{ __id__: 4 }],
_prefab: null,
_lpos: vec3(0, 0, 1000),
_lrot: quat(),
_lscale: vec3(1, 1, 1),
_mobility: 0,
_layer: 1073741824,
_euler: vec3(0, 0, 0),
_id: cameraNodeId,
},
// [4] Camera component
{
__type__: "cc.Camera",
_name: "",
_objFlags: 0,
__editorExtras__: {},
node: { __id__: 3 },
_enabled: true,
_projection: 1,
_priority: 0,
_fov: 45,
_fovAxis: 0,
_orthoHeight: 10,
_near: 1,
_far: 2000,
_color: { __type__: "cc.Color", r: 0, g: 0, b: 0, a: 255 },
_depth: 1,
_stencil: 0,
_clearFlags: 6,
_rect: { __type__: "cc.Rect", x: 0, y: 0, width: 1, height: 1 },
_visibility: 1108344832,
_id: "",
},
// [5] UITransform on Canvas
{
__type__: "cc.UITransform",
_name: "",
_objFlags: 0,
__editorExtras__: {},
node: { __id__: 2 },
_enabled: true,
_contentSize: { __type__: "cc.Size", width: 720, height: 1280 },
_anchorPoint: { __type__: "cc.Vec2", x: 0.5, y: 0.5 },
_id: "",
},
// [6] Canvas component
{
__type__: "cc.Canvas",
_name: "",
_objFlags: 0,
__editorExtras__: {},
node: { __id__: 2 },
_enabled: true,
_cameraComponent: { __id__: 4 },
_alignCanvasWithScreen: true,
_id: "",
},
// [7] Widget on Canvas (fullscreen)
{
__type__: "cc.Widget",
_name: "",
_objFlags: 0,
__editorExtras__: {},
node: { __id__: 2 },
_enabled: true,
_alignFlags: 15,
_target: null,
_left: 0,
_right: 0,
_top: 0,
_bottom: 0,
_isAbsLeft: true,
_isAbsRight: true,
_isAbsTop: true,
_isAbsBottom: true,
_originalWidth: 0,
_originalHeight: 0,
_id: "",
},
// [8] cc.PrefabInfo for scene
// [9] (reserved)
// [10] SceneGlobals
{
__type__: "cc.SceneGlobals",
ambient: { __id__: 11 },
shadows: { __id__: 12 },
_skybox: { __id__: 13 },
fog: { __id__: 14 },
},
// [11] AmbientInfo
{ __type__: "cc.AmbientInfo", _skyLightingColor: { __type__: "cc.Vec4", x: 0.2, y: 0.2, z: 0.2, w: 1 } },
// [12] ShadowsInfo
{ __type__: "cc.ShadowsInfo" },
// [13] SkyboxInfo
{ __type__: "cc.SkyboxInfo" },
// [14] FogInfo
{ __type__: "cc.FogInfo" },
];
}
private async sceneScript(method: string, args: any[]): Promise<any> {
return Editor.Message.request("scene", "execute-scene-script", {
name: EXT_NAME,
method,
args,
});
}
}
@@ -1,361 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
const EXT_NAME = "cocos-creator-mcp";
/** シーン名から「untitled (保存先なし)」かどうかを判定 */
const UNTITLED_SCENE_NAMES = new Set(["scene-2d", "scene-3d", "Untitled", "NewScene", ""]);
/**
* 現在のシーンの dirty 状態を取得。
* CC バージョンによって query-dirty / query-is-dirty のどちらか(または両方)が存在するので両方試行。
*/
export async function queryCurrentSceneDirty(): Promise<boolean> {
try {
const r = await (Editor.Message.request as any)("scene", "query-dirty");
return !!r;
} catch { /* try alternate */ }
try {
const r = await (Editor.Message.request as any)("scene", "query-is-dirty");
return !!r;
} catch { /* assume clean if both fail */ }
return false;
}
/**
* 現在のシーン情報(名前・UUID)を取得。
*/
export async function queryCurrentSceneInfo(): Promise<{ sceneName: string; sceneUuid: string }> {
try {
const result: any = await Editor.Message.request(
"scene",
"execute-scene-script",
{ name: EXT_NAME, method: "getSceneHierarchy", args: [false] }
);
return { sceneName: result?.sceneName || "", sceneUuid: result?.sceneUuid || "" };
} catch {
return { sceneName: "", sceneUuid: "" };
}
}
/**
* 現在のシーンが untitled (scene-2d 等) かを判定。
*/
export async function isCurrentSceneUntitled(): Promise<boolean> {
const { sceneName } = await queryCurrentSceneInfo();
return UNTITLED_SCENE_NAMES.has(sceneName);
}
/**
* save-scene をダイアログ安全に呼び出す。
*
* - 現在のシーンが untitled (scene-2d 等) → no-op(ダイアログ防止)
* - それ以外 → save-scene を実行
*
* 任意の MCP ツール内部で save-scene を呼ぶ場合は必ずこれ経由にする。
* 直接 `Editor.Message.request("scene", "save-scene")` を呼ぶと、
* 現在シーンが untitled のときにモーダルダイアログが出て MCP が固まる。
*
* @returns 実際に保存を実行したか
*/
export async function safeSaveScene(): Promise<boolean> {
if (await isCurrentSceneUntitled()) {
console.warn(
"[cocos-creator-mcp] safeSaveScene: current scene is untitled, " +
"skipping save-scene to avoid modal dialog."
);
return false;
}
await (Editor.Message.request as any)("scene", "save-scene");
return true;
}
/**
* シーン切替系ツール(scene_open/close/new など)の前処理。
*
* - clean → OK
* - dirty + 保存先ありのシーン → save-scene で自動保存
* - dirty + untitled シーン → ダイアログを自動応答("Don't Save")して変更を破棄
*
* 目的: "Save changes?" ダイアログで MCP がブロックされるのを防ぐ。
*/
export async function ensureSceneSafeToSwitch(force: boolean = false): Promise<void> {
const isDirty = await queryCurrentSceneDirty();
if (!isDirty) return;
const { sceneName } = await queryCurrentSceneInfo();
const isUntitled = UNTITLED_SCENE_NAMES.has(sceneName);
if (isUntitled) {
// untitled + dirty: ダイアログが出るとMCPがブロックされるので、
// Electron dialog をパッチして「保存しない」を自動応答する。
// パッチは次回のシーン切替(呼び出し元の scene_open 等)でダイアログが出る瞬間に効く。
patchDialogForDiscard();
console.warn(
`[cocos-creator-mcp] ensureSceneSafeToSwitch: untitled scene "${sceneName}" ` +
`is dirty — dialog will auto-respond "Don't Save" to avoid blocking MCP.`
);
return;
}
try {
// ここに来る時点で untitled ではない(上で判定済み)ので直接 save-scene OK
await (Editor.Message.request as any)("scene", "save-scene");
} catch (e: any) {
if (force) {
console.warn(
`[cocos-creator-mcp] ensureSceneSafeToSwitch: save-scene failed but force=true — proceeding. ` +
`Error: ${e.message || e}`
);
return;
}
throw new Error(
`Failed to auto-save dirty scene "${sceneName}" before switch: ${e.message || e}. ` +
`Save manually and retry, or pass force=true to bypass.`
);
}
}
/**
* Electron の dialog.showMessageBox(Sync) を一時的にパッチして、
* 次回のダイアログで「保存しない (Don't Save)」を自動選択する。
*
* CocosCreator の "Save changes?" ダイアログは通常:
* buttons: ["Save", "Cancel", "Don't Save"] → index 2 = Don't Save
* または:
* buttons: ["Save", "Don't Save", "Cancel"] → index 1 = Don't Save
*
* ボタンテキストから "Don't Save" / "not" / "discard" を探し、
* 見つからなければ最後のボタン(通常 Don't Save)を選択する。
*/
function patchDialogForDiscard(): void {
try {
const electron = require("electron");
const dialog = electron.dialog;
const origSync = dialog.showMessageBoxSync;
const origAsync = dialog.showMessageBox;
function findDiscardIndex(buttons: string[]): number {
if (!buttons || buttons.length === 0) return 0;
const idx = buttons.findIndex((b: string) => {
const lower = b.toLowerCase();
return lower.includes("don't save") || lower.includes("not")
|| lower.includes("discard") || lower.includes("保存しない");
});
return idx >= 0 ? idx : buttons.length - 1;
}
// Sync version
dialog.showMessageBoxSync = function (...args: any[]) {
dialog.showMessageBoxSync = origSync; // 1回で復元
const options = args.length > 1 ? args[1] : args[0];
const buttons = options?.buttons || [];
const result = findDiscardIndex(buttons);
console.warn(`[cocos-creator-mcp] dialog auto-responded: button[${result}]="${buttons[result] || "?"}" (buttons: ${JSON.stringify(buttons)})`);
return result;
};
// Async version
dialog.showMessageBox = function (...args: any[]) {
dialog.showMessageBox = origAsync; // 1回で復元
const options = args.length > 1 ? args[1] : args[0];
const buttons = options?.buttons || [];
const result = findDiscardIndex(buttons);
console.warn(`[cocos-creator-mcp] dialog auto-responded (async): button[${result}]="${buttons[result] || "?"}" (buttons: ${JSON.stringify(buttons)})`);
return Promise.resolve({ response: result, checkboxChecked: false });
};
// 安全策: 5秒後に未使用なら復元(次のダイアログが出なかったケース)
setTimeout(() => {
if (dialog.showMessageBoxSync !== origSync) {
dialog.showMessageBoxSync = origSync;
}
if (dialog.showMessageBox !== origAsync) {
dialog.showMessageBox = origAsync;
}
}, 5000);
} catch (e: any) {
console.error(`[cocos-creator-mcp] patchDialogForDiscard failed: ${e.message || e}`);
}
}
export class SceneTools implements ToolCategory {
readonly categoryName = "scene";
getTools(): ToolDefinition[] {
return [
{
name: "scene_get_hierarchy",
description: "Get the node hierarchy of the current scene. Returns tree structure with uuid, name, active, and optionally components.",
inputSchema: {
type: "object",
properties: {
includeComponents: {
type: "boolean",
description: "Include component info for each node",
},
},
},
},
{
name: "scene_open",
description: "Open a scene by its asset UUID or database path (e.g. 'db://assets/scenes/Main.scene'). If the current scene is dirty and untitled, returns an error instead of triggering a modal save dialog that would block MCP. Pass force=true to bypass this guard.",
inputSchema: {
type: "object",
properties: {
scene: {
type: "string",
description: "Scene UUID or db:// path",
},
force: {
type: "boolean",
description: "Skip dirty-scene preflight check (may trigger modal save dialog)",
},
},
required: ["scene"],
},
},
{
name: "scene_save",
description: "Save the currently open scene.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "scene_get_list",
description: "List all scene files in the project.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "scene_close",
description: "Close the current scene. If the current scene is dirty and untitled, returns an error instead of triggering a modal save dialog. Pass force=true to bypass.",
inputSchema: {
type: "object",
properties: {
force: {
type: "boolean",
description: "Skip dirty-scene preflight check (may trigger modal save dialog)",
},
},
},
},
{
name: "scene_get_current",
description: "Get the name and UUID of the currently open scene.",
inputSchema: { type: "object", properties: {} },
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
switch (toolName) {
case "scene_get_hierarchy":
return this.getHierarchy(args.includeComponents ?? false);
case "scene_open":
return this.openScene(args.scene, !!args.force);
case "scene_save":
return this.saveScene();
case "scene_get_list":
return this.getSceneList();
case "scene_close":
try {
await ensureSceneSafeToSwitch(!!args.force);
await (Editor.Message.request as any)("scene", "close-scene");
return ok({ success: true });
} catch (e: any) { return err(e.message || String(e)); }
case "scene_get_current":
return this.getHierarchy(false).then((r) => {
const parsed = JSON.parse(r.content[0].text);
return ok({ success: true, sceneName: parsed.sceneName, sceneUuid: parsed.sceneUuid });
}).catch((e) => err(String(e)));
default:
return err(`Unknown tool: ${toolName}`);
}
}
private async getHierarchy(includeComponents: boolean): Promise<ToolResult> {
try {
const result = await Editor.Message.request(
"scene",
"execute-scene-script",
{
name: EXT_NAME,
method: "getSceneHierarchy",
args: [includeComponents],
}
);
return ok(result);
} catch (e: any) {
// Fallback: use query-node-tree
try {
const tree = await Editor.Message.request("scene", "query-node-tree");
return ok({ success: true, hierarchy: tree });
} catch (e2: any) {
return err(e2.message || String(e2));
}
}
}
private async openScene(scene: string, force: boolean): Promise<ToolResult> {
try {
await ensureSceneSafeToSwitch(force);
await Editor.Message.request("asset-db", "open-asset", scene);
return ok({ success: true, scene });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async saveScene(): Promise<ToolResult> {
try {
// 現在のシーンが保存済みファイルか確認(新規シーンの場合はダイアログが出るのでスキップ)
const scenes = await Editor.Message.request("asset-db", "query-assets", {
pattern: "db://assets/**/*.scene",
}).catch(() => []);
// シーン名を取得して既存シーンか判定
const hierarchy = await Editor.Message.request(
"scene", "execute-scene-script",
{ name: "cocos-creator-mcp", method: "getSceneHierarchy", args: [false] }
).catch(() => null);
const sceneName = hierarchy?.sceneName;
const isNewScene = !sceneName || sceneName === "scene-2d" || sceneName === "Untitled";
if (isNewScene) {
return ok({ success: true, note: "New/untitled scene, skip save to avoid dialog" });
}
// シーンがdirtyでない場合は保存不要
const isDirty = await (Editor.Message.request as any)("scene", "query-is-dirty").catch(() => true);
if (!isDirty) {
return ok({ success: true, note: "Scene not dirty, skip save" });
}
const result = await (Editor.Message.request as any)("scene", "save-scene", false);
return ok({ success: true, result });
} catch (e: any) {
return err(e.message || String(e));
}
}
private async getSceneList(): Promise<ToolResult> {
try {
const results = await Editor.Message.request("asset-db", "query-assets", {
pattern: "db://assets/**/*.scene",
});
const scenes = (results || []).map((a: any) => ({
uuid: a.uuid,
path: a.path || a.url,
name: a.name,
}));
return ok({ success: true, scenes });
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,232 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
export class SceneViewTools implements ToolCategory {
readonly categoryName = "sceneView";
getTools(): ToolDefinition[] {
return [
{
name: "view_change_gizmo_tool",
description: "Change the gizmo tool (move, rotate, scale, rect).",
inputSchema: {
type: "object",
properties: {
tool: { type: "string", enum: ["move", "rotate", "scale", "rect"], description: "Gizmo tool name" },
},
required: ["tool"],
},
},
{
name: "view_query_gizmo_tool",
description: "Get the currently active gizmo tool.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_change_gizmo_pivot",
description: "Change gizmo pivot mode (center or pivot).",
inputSchema: {
type: "object",
properties: {
pivot: { type: "string", enum: ["center", "pivot"], description: "Pivot mode" },
},
required: ["pivot"],
},
},
{
name: "view_query_gizmo_pivot",
description: "Get the current gizmo pivot mode.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_change_gizmo_coordinate",
description: "Change gizmo coordinate system (local or global).",
inputSchema: {
type: "object",
properties: {
coordinate: { type: "string", enum: ["local", "global"], description: "Coordinate system" },
},
required: ["coordinate"],
},
},
{
name: "view_query_gizmo_coordinate",
description: "Get the current gizmo coordinate system.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_change_mode_2d_3d",
description: "Switch between 2D and 3D view mode.",
inputSchema: {
type: "object",
properties: {
mode: { type: "string", enum: ["2d", "3d"], description: "View mode" },
},
required: ["mode"],
},
},
{
name: "view_query_mode_2d_3d",
description: "Get the current 2D/3D view mode.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_set_grid_visible",
description: "Show or hide the scene grid.",
inputSchema: {
type: "object",
properties: {
visible: { type: "boolean", description: "Whether to show the grid" },
},
required: ["visible"],
},
},
{
name: "view_query_grid_visible",
description: "Check if the scene grid is visible.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_focus_on_node",
description: "Focus the scene camera on specific node(s).",
inputSchema: {
type: "object",
properties: {
uuids: { type: "array", items: { type: "string" }, description: "Node UUIDs to focus on" },
},
required: ["uuids"],
},
},
{
name: "view_get_status",
description: "Get the current scene view status (gizmo tool, pivot, coordinate, grid, etc.).",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_set_icon_gizmo_3d",
description: "Toggle 3D icon gizmos on/off.",
inputSchema: {
type: "object",
properties: { enabled: { type: "boolean" } },
required: ["enabled"],
},
},
{
name: "view_query_icon_gizmo_3d",
description: "Check if 3D icon gizmos are enabled.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_set_icon_gizmo_size",
description: "Set the icon gizmo size.",
inputSchema: {
type: "object",
properties: { size: { type: "number" } },
required: ["size"],
},
},
{
name: "view_query_icon_gizmo_size",
description: "Get the current icon gizmo size.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_align_with_view",
description: "Align the selected node with the current camera view.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_align_view_with_node",
description: "Align the camera view with the selected node.",
inputSchema: { type: "object", properties: {} },
},
{
name: "view_reset",
description: "Reset the scene view to default state.",
inputSchema: { type: "object", properties: {} },
},
];
}
async execute(toolName: string, args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "view_change_gizmo_tool":
await (Editor.Message.request as any)("scene", "change-gizmo-tool", args.tool);
return ok({ success: true, tool: args.tool });
case "view_query_gizmo_tool": {
const tool = await (Editor.Message.request as any)("scene", "query-gizmo-tool-name");
return ok({ success: true, tool });
}
case "view_change_gizmo_pivot":
await (Editor.Message.request as any)("scene", "change-gizmo-pivot", args.pivot);
return ok({ success: true, pivot: args.pivot });
case "view_query_gizmo_pivot": {
const pivot = await (Editor.Message.request as any)("scene", "query-gizmo-pivot");
return ok({ success: true, pivot });
}
case "view_change_gizmo_coordinate":
await (Editor.Message.request as any)("scene", "change-gizmo-coordinate", args.coordinate);
return ok({ success: true, coordinate: args.coordinate });
case "view_query_gizmo_coordinate": {
const coord = await (Editor.Message.request as any)("scene", "query-gizmo-coordinate");
return ok({ success: true, coordinate: coord });
}
case "view_change_mode_2d_3d":
await (Editor.Message.request as any)("scene", "change-view-mode-2d-3d", args.mode);
return ok({ success: true, mode: args.mode });
case "view_query_mode_2d_3d": {
const mode = await (Editor.Message.request as any)("scene", "query-view-mode-2d-3d");
return ok({ success: true, mode });
}
case "view_set_grid_visible":
await (Editor.Message.request as any)("scene", "set-grid-visible", args.visible);
return ok({ success: true, visible: args.visible });
case "view_query_grid_visible": {
const visible = await (Editor.Message.request as any)("scene", "query-grid-visible");
return ok({ success: true, visible });
}
case "view_focus_on_node":
await (Editor.Message.request as any)("scene", "focus-camera-on-nodes", args.uuids);
return ok({ success: true, uuids: args.uuids });
case "view_get_status": {
const [tool, pivot, coord, mode, grid] = await Promise.all([
(Editor.Message.request as any)("scene", "query-gizmo-tool-name").catch(() => null),
(Editor.Message.request as any)("scene", "query-gizmo-pivot").catch(() => null),
(Editor.Message.request as any)("scene", "query-gizmo-coordinate").catch(() => null),
(Editor.Message.request as any)("scene", "query-view-mode-2d-3d").catch(() => null),
(Editor.Message.request as any)("scene", "query-grid-visible").catch(() => null),
]);
return ok({ success: true, tool, pivot, coordinate: coord, mode, gridVisible: grid });
}
case "view_set_icon_gizmo_3d":
await (Editor.Message.request as any)("scene", "set-icon-gizmo-3d", args.enabled);
return ok({ success: true, enabled: args.enabled });
case "view_query_icon_gizmo_3d": {
const enabled = await (Editor.Message.request as any)("scene", "query-is-icon-gizmo-3d");
return ok({ success: true, enabled });
}
case "view_set_icon_gizmo_size":
await (Editor.Message.request as any)("scene", "set-icon-gizmo-size", args.size);
return ok({ success: true, size: args.size });
case "view_query_icon_gizmo_size": {
const size = await (Editor.Message.request as any)("scene", "query-icon-gizmo-size");
return ok({ success: true, size });
}
case "view_align_with_view":
await (Editor.Message.request as any)("scene", "align-with-view");
return ok({ success: true });
case "view_align_view_with_node":
await (Editor.Message.request as any)("scene", "align-view-with-node");
return ok({ success: true });
case "view_reset":
await (Editor.Message.request as any)("scene", "reset-scene-view");
return ok({ success: true });
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,130 +0,0 @@
import { ToolCategory, ToolDefinition, ToolResult } from "../types";
import { ok, err } from "../tool-base";
import { BUILD_HASH } from "../mcp-server";
export class ServerTools implements ToolCategory {
readonly categoryName = "server";
getTools(): ToolDefinition[] {
return [
{
name: "server_query_ip_list",
description: "Get the list of IP addresses the editor server is listening on.",
inputSchema: { type: "object", properties: {} },
},
{
name: "server_query_port",
description: "Get the port number of the editor's built-in server.",
inputSchema: { type: "object", properties: {} },
},
{
name: "server_get_status",
description: "Get the editor server status (IP, port, connectivity).",
inputSchema: { type: "object", properties: {} },
},
{
name: "server_get_build_hash",
description: "Get the build hash of the MCP server. The hash is derived from the code content, so identical code always produces the same hash.",
inputSchema: { type: "object", properties: {} },
},
{
name: "server_check_connectivity",
description: "Check if the editor server is reachable.",
inputSchema: { type: "object", properties: {} },
},
{
name: "server_get_network_interfaces",
description: "Get detailed network interface information.",
inputSchema: { type: "object", properties: {} },
},
{
name: "server_check_code_sync",
description: "Check if the running MCP extension code matches the latest build. Compares the runtime BUILD_HASH with the hash of dist/ files. Returns whether extension reload or CC restart is needed.",
inputSchema: { type: "object", properties: {} },
},
];
}
async execute(toolName: string, _args: Record<string, any>): Promise<ToolResult> {
try {
switch (toolName) {
case "server_query_ip_list": {
const ips = await (Editor.Message.request as any)("server", "query-ip-list");
return ok({ success: true, ips });
}
case "server_query_port": {
const port = await (Editor.Message.request as any)("server", "query-port");
return ok({ success: true, port });
}
case "server_get_status": {
const [ips, port] = await Promise.all([
(Editor.Message.request as any)("server", "query-ip-list").catch(() => []),
(Editor.Message.request as any)("server", "query-port").catch(() => null),
]);
return ok({ success: true, ips, port, buildId: BUILD_HASH });
}
case "server_get_build_hash": {
return ok({ success: true, buildHash: BUILD_HASH });
}
case "server_check_connectivity": {
try {
const port = await (Editor.Message.request as any)("server", "query-port");
return ok({ success: true, reachable: true, port });
} catch {
return ok({ success: true, reachable: false });
}
}
case "server_get_network_interfaces": {
const os = require("os");
const interfaces = os.networkInterfaces();
return ok({ success: true, interfaces });
}
case "server_check_code_sync": {
try {
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const extDir = path.join(Editor.Project.path, "extensions", "cocos-creator-mcp", "dist");
if (!fs.existsSync(extDir)) {
return ok({ success: true, synced: false, note: "Extension dist/ not found", runtimeHash: BUILD_HASH });
}
const hash = crypto.createHash("sha256");
const collectJs = (dir: string, prefix = ""): string[] => {
let files: string[] = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) files = files.concat(collectJs(path.join(dir, entry.name), rel));
else if (entry.name.endsWith(".js")) files.push(rel);
}
return files;
};
for (const file of collectJs(extDir).sort()) {
let content = fs.readFileSync(path.join(extDir, file), "utf8");
// mcp-server.js の baked hash を postbuild 前と同じプレースホルダに逆置換
// (postbuild は __BUILD_HASH__ が残った状態でハッシュ計算しているため、同じ入力にする)
if (file === "mcp-server.js") {
content = content.replace(/exports\.BUILD_HASH = "[a-f0-9]{12}"/, 'exports.BUILD_HASH = "__BUILD_HASH__"');
}
hash.update(content.replace(/__BUILD_HASH__/g, ""));
}
const diskHash = hash.digest("hex").substring(0, 12);
const synced = diskHash === BUILD_HASH;
return ok({
success: true,
synced,
runtimeHash: BUILD_HASH,
diskHash,
action: synced ? "none" : "Extension reload or CC restart needed",
});
} catch (e: any) {
return ok({ success: true, synced: false, error: e.message, runtimeHash: BUILD_HASH });
}
}
default:
return err(`Unknown tool: ${toolName}`);
}
} catch (e: any) {
return err(e.message || String(e));
}
}
}
@@ -1,56 +0,0 @@
/** MCP Tool definition (JSON Schema based) */
export interface ToolDefinition {
name: string;
description: string;
inputSchema: {
type: "object";
properties: Record<string, any>;
required?: string[];
};
}
/** Result returned from tool execution */
export interface ToolResult {
content: Array<{ type: "text"; text: string }>;
isError?: boolean;
}
/** Interface that all tool categories must implement */
export interface ToolCategory {
readonly categoryName: string;
getTools(): ToolDefinition[];
execute(toolName: string, args: Record<string, any>): Promise<ToolResult>;
}
/** JSON-RPC 2.0 request */
export interface JsonRpcRequest {
jsonrpc: "2.0";
id?: string | number;
method: string;
params?: any;
}
/** JSON-RPC 2.0 response */
export interface JsonRpcResponse {
jsonrpc: "2.0";
id?: string | number | null;
result?: any;
error?: {
code: number;
message: string;
data?: any;
};
}
/** Server configuration */
export interface ServerConfig {
port: number;
autoStart: boolean;
autoArchiveRecordings: boolean;
}
export const DEFAULT_CONFIG: ServerConfig = {
port: 3000,
autoStart: false,
autoArchiveRecordings: false,
};
@@ -1,10 +0,0 @@
/**
* MCP クライアントが JSON オブジェクトを文字列化して送信する問題への共通対策.
* スキーマに type が未宣言のオブジェクト引数は文字列で届く場合がある.
*/
export function parseMaybeJson<T = any>(value: any): T {
if (typeof value === "string") {
try { return JSON.parse(value); } catch { /* string のまま */ }
}
return value;
}
@@ -1,128 +0,0 @@
/**
* Prefab保存テスト — プロパティがPrefabに保持されるか検証
*
* 前提: CocosCreatorでcocos-creator-mcpサーバーが起動中、MainScene開いてる
*/
const PORT = process.argv[2] || 3001;
const BASE = `http://127.0.0.1:${PORT}`;
let rpcId = 0;
async function rpc(method, params = {}) {
const res = await fetch(`${BASE}/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: ++rpcId, method, params }),
});
return res.json();
}
async function callTool(name, args = {}) {
const res = await rpc("tools/call", { name, arguments: args });
if (res.error) return { _rpcError: res.error };
const text = res.result?.content?.[0]?.text;
return text ? JSON.parse(text) : res.result;
}
async function sceneScript(method, args) {
// Call execute-scene-script through a raw JSON-RPC (not via tool)
// Actually we need to go through the MCP server... let's use node_set_property hack
// Or better: add a scene_script tool call
// For now, use the existing setComponentProperty that IS registered in scene.ts
// We can't call execute-scene-script directly from outside.
// But we CAN call the node_set_property tool which internally calls setNodeProperty scene script.
// For component property, we need a dedicated tool.
// Workaround: Use curl to call the Editor's Messages API if available
return null;
}
async function main() {
console.log("\n🧪 Prefab Property Persistence Test\n");
// Step 1: Create test node with Label
console.log("1. Creating test node with Label...");
const created = await callTool("node_create", {
name: "PrefabPropertyTest",
parent: "83A39G7PtBqLLEoVjvUo4h", // Canvas
components: ["cc.Label"],
});
console.log(` Node UUID: ${created.uuid}`);
const nodeUuid = created.uuid;
// Step 2: Set position (this should persist)
console.log("2. Setting position to (100, 200, 0)...");
await callTool("node_set_transform", {
uuid: nodeUuid,
position: { x: 100, y: 200, z: 0 },
});
// Step 3: Verify position was set
const info1 = await callTool("node_get_info", { uuid: nodeUuid });
console.log(` Position: (${info1.data?.position?.x}, ${info1.data?.position?.y})`);
console.log(` Components: ${info1.data?.components?.map(c => c.type).join(", ")}`);
// Step 4: Try to create prefab via Editor API
console.log("3. Attempting to create prefab via Editor.Message...");
// We'll try the create-prefab scene message
// This requires calling: Editor.Message.request("scene", "create-prefab", nodeUuid, url)
// Since we don't have a tool for this, let's try different approaches:
// Approach A: Try "create-prefab" message (3 args: nodeUuid, url, ?)
const prefabUrl = "db://assets/test/PrefabPropertyTest.prefab";
// We need to add this capability. For now, let's check if we can use
// asset-db to create an asset
console.log(` Target: ${prefabUrl}`);
// Actually, let me just check the scene script createPrefabFromNode
// which IS registered in package.json scene methods
// But wait - our own MCP's scene.ts doesn't have createPrefabFromNode
console.log("\n⚠️ Prefab creation tool not yet implemented in our MCP.");
console.log(" This is what needs to be built for v0.5.\n");
// Step 5: Test what the Editor API returns for prefab-related messages
console.log("4. Probing available prefab-related Editor Messages...");
const messages = [
["scene", "create-prefab"],
["scene", "save-prefab"],
["asset-db", "create-asset"],
];
for (const [target, msg] of messages) {
try {
const res = await fetch(`${BASE}/mcp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: ++rpcId, method: "tools/call",
params: { name: "node_get_info", arguments: { uuid: "test" } }
}),
});
console.log(` ${target}:${msg} — needs testing`);
} catch (e) {
console.log(` ${target}:${msg} — error: ${e.message}`);
}
}
// Cleanup
console.log("\n5. Cleaning up test node...");
await callTool("node_delete", { uuid: nodeUuid });
console.log(" Deleted.\n");
console.log("═".repeat(50));
console.log(" Summary:");
console.log(" - Node creation with components: ✅");
console.log(" - Position/transform setting: ✅");
console.log(" - Component property setting: ❓ (needs component tool)");
console.log(" - Prefab creation: ❓ (needs prefab tool)");
console.log(" - Property persistence in prefab: ❓ (blocked by above)");
console.log("═".repeat(50));
console.log("\n→ v0.5 で component + prefab ツール実装後に再検証");
}
main().catch(e => { console.error("Fatal:", e); process.exit(1); });
@@ -1,128 +0,0 @@
#!/usr/bin/env node
/**
* 独立测试服务器 - 模拟Cocos Creator MCP服务器的最小化实现
* 用于测试stdio-bridge.js的基本功能
*/
const http = require('http');
const PORT = 3000;
const MCP_PROTOCOL_VERSION = "2024-11-05";
// 模拟的工具列表(从README中提取的164个工具中的几个示例)
const MOCK_TOOLS = [
{
name: "scene_get_hierarchy",
description: "Get the node tree (with optional component info)",
inputSchema: {
type: "object",
properties: {
includeComponents: { type: "boolean", description: "Include component info in hierarchy" }
}
}
},
{
name: "node_create",
description: "Create node (with optional components)",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Node name" },
parentUuid: { type: "string", description: "Parent node UUID" }
},
required: ["name"]
}
},
{
name: "scene_save",
description: "Save the current scene",
inputSchema: {
type: "object",
properties: {}
}
}
];
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/mcp') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const request = JSON.parse(body);
console.log('[TEST SERVER] Received:', request.method);
const response = {
jsonrpc: "2.0",
id: request.id
};
switch (request.method) {
case 'initialize':
response.result = {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: {
tools: {}
},
serverInfo: {
name: "cocos-creator-mcp",
version: "1.14.0"
}
};
break;
case 'tools/list':
response.result = {
tools: MOCK_TOOLS
};
break;
case 'tools/call':
const toolName = request.params.name;
response.result = {
content: [{
type: "text",
text: JSON.stringify({
success: true,
tool: toolName,
message: `Called ${toolName} successfully`
})
}]
};
break;
default:
response.error = {
code: -32601,
message: "Method not found"
};
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(response));
} catch (e) {
res.statusCode = 400;
res.end(JSON.stringify({ error: e.message }));
}
});
} else if (req.method === 'GET' && req.url === '/health') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 'ok', tools: MOCK_TOOLS.length }));
} else {
res.statusCode = 404;
res.end('Not found');
}
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`[TEST SERVER] Started on http://127.0.0.1:${PORT}/mcp`);
console.log(`[TEST SERVER] Health check: http://127.0.0.1:${PORT}/health`);
});
// 优雅关闭
process.on('SIGINT', () => {
console.log('[TEST SERVER] Shutting down...');
server.close(() => {
process.exit(0);
});
});
@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "CommonJS",
"moduleResolution": "node",
"inlineSourceMap": true,
"inlineSources": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./source",
"types": [
"node",
"@cocos/creator-types/editor"
]
},
"exclude": ["client", "test", "dist"]
}
@@ -0,0 +1,44 @@
# Cocos Creator MCP Server v1.7.9
## 安装说明
1. 将整个文件夹复制到 Cocos Creator 项目的 `extensions` 目录
2. 重启 Cocos Creator 或刷新扩展 (Ctrl+R / Cmd+R)
3. 在扩展菜单中启用 "Cocos MCP Server"
4. 打开扩展面板配置服务器设置
## 使用配置
### Claude Desktop
```json
{
"mcpServers": {
"cocos-creator": {
"type": "http",
"url": "http://127.0.0.1:3000/mcp"
}
}
}
```
### Cursor
```json
{
"mcpServers": {
"cocos-creator": {
"url": "http://127.0.0.1:3000/mcp"
}
}
}
```
## 技术支持
- 📧 邮箱: a549623165@gmail.com
- 💬 微信: lidaxian-AI | 客服微信: vberai-support
- 🐙 GitHub: https://github.com/DaxianLee
- 🛒 购买/续费: https://www.vberai.com/game-engines/cocos
---
构建信息: v1.7.9 (production) - 2026-07-18 16:06:14
作者: 李大仙 (LiDaxian)
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "خادم Cocos MCP",
"description": "خادم AI MCP لـ Cocos Creator 3.8",
"panel_title": "خادم MCP",
"open_panel": "فتح لوحة MCP",
"start_server": "بدء الخادم",
"stop_server": "إيقاف الخادم",
"server_status": "حالة الخادم",
"port": "المنفذ",
"settings": "الإعدادات",
"connected": "متصل",
"disconnected": "غير متصل",
"server_running": "الخادم يعمل على المنفذ {0}",
"server_stopped": "تم إيقاف الخادم",
"auto_start": "بدء تلقائي",
"debug_log": "سجل التصحيح",
"max_connections": "الحد الأقصى للاتصالات",
"connection_info": "معلومات الاتصال",
"http_url": "رابط HTTP",
"copy": "نسخ",
"save_settings": "حفظ",
"settings_saved": "تم حفظ الإعدادات بنجاح",
"server_started": "تم بدء خادم MCP",
"server_stopped_msg": "تم إيقاف خادم MCP",
"failed_to_start": "فشل في بدء الخادم",
"failed_to_stop": "فشل في إيقاف الخادم",
"failed_to_save": "فشل في حفظ الإعدادات",
"url_copied": "تم نسخ رابط HTTP إلى الحافظة",
"tool_manager": "مدير الأدوات",
"open_tool_manager": "فتح مدير الأدوات",
"create_config": "إنشاء تكوين",
"edit_config": "تعديل التكوين",
"delete_config": "حذف التكوين",
"import_config": "استيراد التكوين",
"export_config": "تصدير التكوين",
"apply_config": "تطبيق التكوين",
"select_all": "تحديد الكل",
"deselect_all": "إلغاء تحديد الكل",
"save_changes": "حفظ التغييرات",
"config_name": "اسم التكوين",
"config_description": "وصف التكوين",
"current_config": "التكوين الحالي",
"tool_management": "إدارة الأدوات",
"total_tools": "إجمالي الأدوات",
"enabled_tools": "مفعّلة",
"disabled_tools": "معطّلة",
"no_config_selected": "لم يتم تحديد تكوين",
"select_config_first": "يرجى تحديد أو إنشاء تكوين أولاً",
"config_created": "تم إنشاء التكوين بنجاح",
"config_updated": "تم تحديث التكوين بنجاح",
"config_deleted": "تم حذف التكوين بنجاح",
"config_applied": "تم تطبيق التكوين بنجاح",
"config_exported": "تم تصدير التكوين بنجاح",
"config_imported": "تم استيراد التكوين بنجاح",
"confirm_delete": "تأكيد الحذف",
"delete_config_confirm": "هل أنت متأكد من حذف التكوين \"{0}\"؟ لا يمكن التراجع عن هذا الإجراء.",
"max_config_slots_reached": "تم الوصول إلى الحد الأقصى لعدد التكوينات ({0})",
"invalid_config_format": "تنسيق التكوين غير صالح",
"invalid_json_format": "تنسيق JSON أو بنية التكوين غير صالحة",
"server_tab": "الخادم",
"tools_tab": "إدارة الأدوات",
"available_tools": "الأدوات المتاحة",
"scene_tools": "أدوات المشهد",
"node_tools": "أدوات العقد",
"component_tools": "أدوات المكونات",
"prefab_tools": "أدوات القوالب",
"project_tools": "أدوات المشروع",
"debug_tools": "أدوات التصحيح",
"preferences_tools": "التفضيلات",
"server_tools": "أدوات الخادم",
"broadcast_tools": "أدوات البث",
"scene_advanced_tools": "أدوات المشهد المتقدمة",
"scene_view_tools": "أدوات عرض المشهد",
"reference_image_tools": "أدوات الصور المرجعية",
"asset_advanced_tools": "أدوات الأصول المتقدمة",
"validation_tools": "أدوات التحقق",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "البريد الإلكتروني",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "رمز الجهاز",
"purchase_renew": "شراء / تجديد",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "اختر طريقة تسجيل الدخول",
"auth_subtitle": "أدخل بريدك الإلكتروني ورمز التفعيل",
"email_label": "عنوان البريد الإلكتروني",
"activation_code_label": "رمز التفعيل",
"email_placeholder": "أدخل البريد الإلكتروني",
"code_placeholder": "أدخل الرمز (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "تفعيل",
"deactivate_button": "إلغاء التفعيل",
"activating": "جارٍ التفعيل...",
"deactivating": "جارٍ إلغاء التفعيل...",
"license_status": "حالة الترخيص",
"license_type": "نوع الترخيص",
"expiry_date": "تاريخ الانتهاء",
"days_remaining": "الأيام المتبقية",
"days_unit": "ي",
"permanent": "دائم",
"expiry_warning_title": "الترخيص على وشك الانتهاء",
"expiry_warning_message": "سينتهي ترخيصك خلال {0} أيام. يرجى التجديد لتجنب انقطاع الخدمة.",
"renew_now": "تجديد الآن",
"i_understand": "فهمت",
"activation_success": "تم تفعيل الترخيص بنجاح!",
"activation_failed": "فشل التفعيل: {0}",
"invalid_code": "تنسيق رمز التفعيل غير صالح",
"purchase_license": "شراء ترخيص",
"renew_license": "تجديد الترخيص",
"contact_support": "الدعم",
"language_switch": "العربية",
"version_label": "الإصدار",
"license_required": "مطلوب ترخيص لبدء الخادم",
"license_check_failed": "فشل التحقق من الترخيص",
"status_stopped": "متوقف",
"status_running": "يعمل",
"status_error": "فشل الاتصال",
"config_tab": "إعداد سريع",
"status_label": "الحالة",
"connections_count": "الاتصالات",
"transport_protocol": "بروتوكول النقل",
"transport_type": "نوع النقل",
"mcp_url": "رابط MCP",
"port_fallback_title": "المنفذ {0} غير متاح — تم التبديل إلى {1}",
"port_fallback_reason": "المنفذ {0} محجوز من قبل النظام أو محظور بواسطة جدار الحماية/برنامج مكافحة الفيروسات (شائع في نظام Windows المثبت عليه Hyper-V أو WSL2 أو Docker). الخادم يعمل بشكل طبيعي على المنفذ الجديد.",
"port_fallback_resynced": "تم تحديث إعدادات العملاء التالية: {0}. أعد تشغيلها ليصبح المنفذ الجديد ساري المفعول.",
"port_fallback_no_clients": "إذا سبق أن أعددت عملاء الذكاء الاصطناعي، فأعد إعدادهم على المنفذ الجديد من خلال الإعداد بنقرة واحدة.",
"port_fallback_dismiss": "حسنًا",
"port_label": "المنفذ",
"auto_start_label": "بدء تلقائي",
"debug_log_label": "سجل التصحيح",
"max_connections_label": "الحد الأقصى للاتصالات",
"server_settings_title": "إعدادات الخادم",
"server_status_title": "حالة الخادم",
"connection_info_title": "معلومات الاتصال",
"tools_count": "{0} أدوات",
"tools_stats": "{0} أدوات ({1} مفعّلة / {2} معطّلة)",
"quick_config_title": "إعداد سريع لعملاء MCP",
"quick_config_desc": "تكوين خادم MCP تلقائيًا في محررات AI وأدوات CLI",
"server_info": "معلومات الخادم",
"server_name_label": "اسم الخادم",
"server_address": "عنوان الخادم",
"timeout_ms": "مهلة الانتظار (مللي ثانية)",
"auto_config_title": "إعداد تلقائي (IDE + Codex CLI)",
"add_all": "إضافة الكل",
"remove_all": "إزالة الكل",
"auto_config_note": "انقر لتعديل ملفات التكوين تلقائيًا (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "مُعدّ",
"not_configured": "غير مُعدّ",
"auto_config_btn": "إعداد تلقائي",
"delete_btn": "حذف",
"copy_config": "نسخ التكوين",
"open_config": "فتح التكوين",
"manual_config_title": "إعداد يدوي (Claude CLI + Gemini CLI)",
"refresh_commands": "تحديث الأوامر",
"manual_config_note": "أدوات CLI التالية تتطلب تنفيذًا يدويًا في الطرفية",
"config_scope": "نطاق التكوين:",
"scope_user": "مستوى المستخدم",
"scope_project": "مستوى المشروع",
"operation_log": "سجل العمليات",
"no_logs": "لا توجد سجلات",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "مبني على بروتوكول Streamable HTTP (MCP 2025-03-26)",
"tools_copyright_line1": "نظام الأدوات - 13 أداة intent-level، أكثر من 150 عملية محرر، توزيع أكواد الإجراءات",
"config_copyright_line1": "إعداد سريع - يدعم Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "يرجى إدخال البريد الإلكتروني ورمز التفعيل",
"permanent_label": "دائم",
"logout_button": "تسجيل الخروج",
"invite_code_label": "رمز الدعوة",
"copy_button": "نسخ",
"change_code_button": "تغيير الرمز",
"change_code_title": "تغيير رمز التفعيل",
"change_code_desc": "أدخل رمز التفعيل الجديد. سيتم إلغاء ربط الجهاز الحالي أولاً.",
"confirm_change": "تأكيد",
"cancel": "إلغاء",
"code_required": "يرجى إدخال رمز التفعيل",
"expiry_inline_warning": "ينتهي قريبًا!",
"store_purchase_hint": "إذا اشتريت هذا الإضافة من Cocos Store، يرجى الاتصال بي للحصول على رمز التفعيل.",
"license_expired_title": "انتهت صلاحية الترخيص",
"license_expired_message": "انتهت صلاحية ترخيصك ولا يمكن بدء الخادم. يرجى التجديد أو إعادة التفعيل للمتابعة.",
"go_to_login": "الذهاب لتسجيل الدخول",
"log_load_config_failed": "فشل في تحميل حالة التكوين: {0}",
"log_gen_cli_failed": "فشل في إنشاء أوامر CLI: {0}",
"log_add_client_failed": "فشل في الإضافة إلى {0}: {1}",
"log_remove_client_failed": "فشل في الإزالة من {0}: {1}",
"quick_config_dialog_title": "تكوين بنقرة واحدة",
"config_operation_failed": "فشلت عملية التكوين",
"config_file_not_found_changed_path": "فشل التكوين: لم يتم العثور على ملف التكوين. ربما تم تثبيت {0} في مسار مخصص.",
"log_batch_add_failed": "فشل الإضافة الجماعية: {0}",
"log_batch_remove_failed": "فشل الإزالة الجماعية: {0}",
"log_copied_config": "تم نسخ تكوين {0} إلى الحافظة",
"log_gen_config_failed": "فشل في إنشاء التكوين",
"log_copy_config_failed": "فشل في نسخ التكوين: {0}",
"log_copied_command": "تم نسخ أمر {0} إلى الحافظة",
"log_copy_command_failed": "فشل في نسخ الأمر: {0}",
"log_opened_config_file": "تم فتح ملف التكوين: {0}",
"log_open_config_failed": "فشل في فتح ملف التكوين",
"log_open_config_error": "فشل في فتح ملف التكوين: {0}",
"update_available_title": "تحديث متاح",
"update_new_version": "الإصدار الجديد v{0} متاح!",
"update_now": "التحديث الآن",
"update_not_now": "ليس الآن",
"update_skip_version": "تخطي v{0}",
"check_for_updates": "التحقق من التحديثات",
"update_checking": "جارٍ التحقق...",
"update_no_update": "أنت تستخدم أحدث إصدار",
"update_urgency_critical": "تحديث عاجل",
"update_urgency_high": "تحديث مهم",
"update_current_version": "الإصدار الحالي: v{0}",
"update_changelog": "سجل التغييرات",
"auth_choose_subtitle": "اختر طريقة تسجيل الدخول للمتابعة",
"update_changelog_empty": "لم يتم توفير ملاحظات إصدار لهذه النسخة.",
"subscription_login_title": "حساب اشتراك VberAI",
"subscription_password_label": "كلمة المرور",
"subscription_password_placeholder": "أدخل كلمة المرور",
"subscription_login_button": "تسجيل الدخول باستخدام VberAI",
"subscription_google_button": "تسجيل الدخول باستخدام Google",
"subscription_google_pending": "تم فتح المتصفح. أكمل تفويض Google ثم عد إلى Cocos Creator.",
"subscription_google_pending_button": "في انتظار تفويض Google...",
"subscription_register_button": "إنشاء حساب VberAI",
"subscription_logging_in": "جارٍ تسجيل الدخول...",
"subscription_credentials_required": "يرجى إدخال البريد الإلكتروني وكلمة المرور",
"subscription_login_success": "تم تسجيل الدخول إلى حساب الاشتراك",
"subscription_login_failed": "فشل تسجيل الدخول إلى الاشتراك",
"subscription_not_entitled": "اشتراكك الحالي لا يتضمن Cocos 3.x MCP. قم بالترقية للمتابعة.",
"subscription_upgrade_to": "قم بالترقية إلى «{0}» لاستخدام هذا المنتج",
"subscription_upgrade_action": "ترقية الاشتراك",
"subscription_logged_in": "الحساب المسجّل دخوله",
"subscription_choose_button": "حساب اشتراك VberAI",
"license_choose_button": "رمز التفعيل",
"subscription_method_label": "اشتراك",
"back_to_login_choices": "← اختر طريقة تسجيل دخول أخرى",
"subscription_signed_out_other_device": "تم تسجيل الخروج: تم استخدام حسابك على جهاز آخر.",
"subscription_google_cancelled": "تم إلغاء تسجيل الدخول عبر Google.",
"subscription_request_timeout": "انتهت مهلة الطلب. تحقق من شبكتك وحاول مرة أخرى.",
"subscription_network_error": "فشل الاتصال بالشبكة. تحقق من شبكتك وحاول مرة أخرى.",
"subscription_server_error": "أرجع الخادم استجابة غير صالحة. حاول مرة أخرى لاحقًا.",
"subscription_google_authorization_failed": "لم يكتمل تفويض Google. يرجى المحاولة مرة أخرى."
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP Server",
"description": "AI MCP Server für Cocos Creator 3.8",
"panel_title": "MCP Server",
"open_panel": "MCP-Panel öffnen",
"start_server": "Server starten",
"stop_server": "Server stoppen",
"server_status": "Serverstatus",
"port": "Port",
"settings": "Einstellungen",
"connected": "Verbunden",
"disconnected": "Getrennt",
"server_running": "Server läuft auf Port {0}",
"server_stopped": "Server wurde gestoppt",
"auto_start": "Autostart",
"debug_log": "Debug-Protokoll",
"max_connections": "Max. Verbindungen",
"connection_info": "Verbindungsinfo",
"http_url": "HTTP-URL",
"copy": "Kopieren",
"save_settings": "Speichern",
"settings_saved": "Einstellungen erfolgreich gespeichert",
"server_started": "MCP Server gestartet",
"server_stopped_msg": "MCP Server gestoppt",
"failed_to_start": "Server konnte nicht gestartet werden",
"failed_to_stop": "Server konnte nicht gestoppt werden",
"failed_to_save": "Einstellungen konnten nicht gespeichert werden",
"url_copied": "HTTP-URL in Zwischenablage kopiert",
"tool_manager": "Tool-Manager",
"open_tool_manager": "Tool-Manager öffnen",
"create_config": "Konfiguration erstellen",
"edit_config": "Konfiguration bearbeiten",
"delete_config": "Konfiguration löschen",
"import_config": "Konfiguration importieren",
"export_config": "Konfiguration exportieren",
"apply_config": "Konfiguration anwenden",
"select_all": "Alle auswählen",
"deselect_all": "Alle abwählen",
"save_changes": "Änderungen speichern",
"config_name": "Konfigurationsname",
"config_description": "Konfigurationsbeschreibung",
"current_config": "Aktuelle Konfiguration",
"tool_management": "Tool-Verwaltung",
"total_tools": "Gesamt",
"enabled_tools": "Aktiviert",
"disabled_tools": "Deaktiviert",
"no_config_selected": "Keine Konfiguration ausgewählt",
"select_config_first": "Bitte wählen Sie zuerst eine Konfiguration oder erstellen Sie eine neue",
"config_created": "Konfiguration erfolgreich erstellt",
"config_updated": "Konfiguration erfolgreich aktualisiert",
"config_deleted": "Konfiguration erfolgreich gelöscht",
"config_applied": "Konfiguration erfolgreich angewendet",
"config_exported": "Konfiguration erfolgreich exportiert",
"config_imported": "Konfiguration erfolgreich importiert",
"confirm_delete": "Löschen bestätigen",
"delete_config_confirm": "Möchten Sie die Konfiguration \"{0}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"max_config_slots_reached": "Maximale Anzahl von Konfigurationen erreicht ({0})",
"invalid_config_format": "Ungültiges Konfigurationsformat",
"invalid_json_format": "Ungültiges JSON-Format oder Konfigurationsstruktur",
"server_tab": "Server",
"tools_tab": "Tool-Verwaltung",
"available_tools": "Verfügbare Tools",
"scene_tools": "Szenen-Tools",
"node_tools": "Knoten-Tools",
"component_tools": "Komponenten-Tools",
"prefab_tools": "Prefab-Tools",
"project_tools": "Projekt-Tools",
"debug_tools": "Debug-Tools",
"preferences_tools": "Einstellungen",
"server_tools": "Server-Tools",
"broadcast_tools": "Broadcast-Tools",
"scene_advanced_tools": "Erweiterte Szenen-Tools",
"scene_view_tools": "Szenenansicht-Tools",
"reference_image_tools": "Referenzbild-Tools",
"asset_advanced_tools": "Erweiterte Asset-Tools",
"validation_tools": "Validierungs-Tools",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "E-Mail",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "Maschinencode",
"purchase_renew": "Kaufen / Verlängern",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "Anmeldemethode auswählen",
"auth_subtitle": "Geben Sie Ihre E-Mail und Ihren Aktivierungscode ein",
"email_label": "E-Mail-Adresse",
"activation_code_label": "Aktivierungscode",
"email_placeholder": "E-Mail eingeben",
"code_placeholder": "Code eingeben (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "Aktivieren",
"deactivate_button": "Deaktivieren",
"activating": "Aktivierung...",
"deactivating": "Deaktivierung...",
"license_status": "Lizenzstatus",
"license_type": "Lizenztyp",
"expiry_date": "Ablaufdatum",
"days_remaining": "Verbleibende Tage",
"days_unit": "T",
"permanent": "Permanent",
"expiry_warning_title": "Lizenz läuft bald ab",
"expiry_warning_message": "Ihre Lizenz läuft in {0} Tagen ab. Bitte verlängern Sie, um Unterbrechungen zu vermeiden.",
"renew_now": "Jetzt verlängern",
"i_understand": "Verstanden",
"activation_success": "Lizenz erfolgreich aktiviert!",
"activation_failed": "Aktivierung fehlgeschlagen: {0}",
"invalid_code": "Ungültiges Aktivierungscode-Format",
"purchase_license": "Lizenz kaufen",
"renew_license": "Lizenz verlängern",
"contact_support": "Support",
"language_switch": "Deutsch",
"version_label": "Version",
"license_required": "Zum Starten des Servers ist eine Lizenz erforderlich",
"license_check_failed": "Lizenzprüfung fehlgeschlagen",
"status_stopped": "Gestoppt",
"status_running": "Läuft",
"status_error": "Verbindung fehlgeschlagen",
"config_tab": "Schnellkonfig",
"status_label": "Status",
"connections_count": "Verbindungen",
"transport_protocol": "Transportprotokoll",
"transport_type": "Transporttyp",
"mcp_url": "MCP-URL",
"port_fallback_title": "Port {0} nicht verfügbar – zu {1} gewechselt",
"port_fallback_reason": "Port {0} ist vom System reserviert oder wird von einer Firewall/Antivirensoftware blockiert (häufig unter Windows mit Hyper-V, WSL2 oder Docker). Der Server läuft auf dem neuen Port normal.",
"port_fallback_resynced": "Konfiguration aktualisiert für: {0}. Starten Sie diese Clients neu, damit der neue Port wirksam wird.",
"port_fallback_no_clients": "Falls Sie bereits AI-Clients eingerichtet haben, konfigurieren Sie sie über die Ein-Klick-Konfiguration erneut auf den neuen Port.",
"port_fallback_dismiss": "Verstanden",
"port_label": "Port",
"auto_start_label": "Autostart",
"debug_log_label": "Debug-Protokoll",
"max_connections_label": "Max. Verbindungen",
"server_settings_title": "Servereinstellungen",
"server_status_title": "Serverstatus",
"connection_info_title": "Verbindungsinfo",
"tools_count": "{0} Tools",
"tools_stats": "{0} Tools ({1} aktiviert / {2} deaktiviert)",
"quick_config_title": "MCP-Clients schnell konfigurieren",
"quick_config_desc": "MCP-Server automatisch in AI-Editoren und CLI-Tools konfigurieren",
"server_info": "Serverinfo",
"server_name_label": "Servername",
"server_address": "Serveradresse",
"timeout_ms": "Zeitlimit (ms)",
"auto_config_title": "Auto-Konfig (IDE-Editoren + Codex CLI)",
"add_all": "Alle hinzufügen",
"remove_all": "Alle entfernen",
"auto_config_note": "Klicken Sie, um Konfigurationsdateien automatisch zu ändern (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "Konfiguriert",
"not_configured": "Nicht konfiguriert",
"auto_config_btn": "Auto-Konfig",
"delete_btn": "Löschen",
"copy_config": "Konfig kopieren",
"open_config": "Konfig öffnen",
"manual_config_title": "Manuelle Konfig (Claude CLI + Gemini CLI)",
"refresh_commands": "Befehle aktualisieren",
"manual_config_note": "Die folgenden CLI-Tools erfordern manuelle Ausführung im Terminal",
"config_scope": "Konfig-Bereich:",
"scope_user": "Benutzerebene",
"scope_project": "Projektebene",
"operation_log": "Operationsprotokoll",
"no_logs": "Keine Einträge",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Basierend auf dem Streamable HTTP Protokoll (MCP 2025-03-26)",
"tools_copyright_line1": "Tool-System - 13 Intent-Level-Tools, 150+ Editor-Operationen, Action-Code-Dispatch",
"config_copyright_line1": "Schnellkonfig - Unterstützt Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "Bitte geben Sie E-Mail und Aktivierungscode ein",
"permanent_label": "Permanent",
"logout_button": "Abmelden",
"invite_code_label": "Einladungscode",
"copy_button": "Kopieren",
"change_code_button": "Code ändern",
"change_code_title": "Aktivierungscode ändern",
"change_code_desc": "Geben Sie Ihren neuen Aktivierungscode ein. Die aktuelle Gerätebindung wird zuerst freigegeben.",
"confirm_change": "Bestätigen",
"cancel": "Abbrechen",
"code_required": "Bitte geben Sie einen Aktivierungscode ein",
"expiry_inline_warning": "Läuft bald ab!",
"store_purchase_hint": "Wenn Sie dieses Plugin im Cocos Store gekauft haben, kontaktieren Sie mich bitte, um Ihren Aktivierungscode zu erhalten.",
"license_expired_title": "Lizenz abgelaufen",
"license_expired_message": "Ihre Lizenz ist abgelaufen und der Server kann nicht gestartet werden. Bitte verlängern oder reaktivieren Sie, um fortzufahren.",
"go_to_login": "Zum Login",
"log_load_config_failed": "Konfigurationsstatus konnte nicht geladen werden: {0}",
"log_gen_cli_failed": "CLI-Befehle konnten nicht generiert werden: {0}",
"log_add_client_failed": "Hinzufügen zu {0} fehlgeschlagen: {1}",
"log_remove_client_failed": "Entfernen von {0} fehlgeschlagen: {1}",
"quick_config_dialog_title": "Ein-Klick-Konfiguration",
"config_operation_failed": "Konfigurationsvorgang fehlgeschlagen",
"config_file_not_found_changed_path": "Konfiguration fehlgeschlagen: Konfigurationsdatei nicht gefunden. {0} wurde möglicherweise in einem benutzerdefinierten Pfad installiert.",
"log_batch_add_failed": "Stapel-Hinzufügen fehlgeschlagen: {0}",
"log_batch_remove_failed": "Stapel-Entfernen fehlgeschlagen: {0}",
"log_copied_config": "{0}-Konfiguration in Zwischenablage kopiert",
"log_gen_config_failed": "Konfiguration konnte nicht generiert werden",
"log_copy_config_failed": "Konfiguration konnte nicht kopiert werden: {0}",
"log_copied_command": "{0}-Befehl in Zwischenablage kopiert",
"log_copy_command_failed": "Befehl konnte nicht kopiert werden: {0}",
"log_opened_config_file": "Konfigurationsdatei geöffnet: {0}",
"log_open_config_failed": "Konfigurationsdatei konnte nicht geöffnet werden",
"log_open_config_error": "Konfigurationsdatei konnte nicht geöffnet werden: {0}",
"update_available_title": "Update verfügbar",
"update_new_version": "Neue Version v{0} ist verfügbar!",
"update_now": "Jetzt aktualisieren",
"update_not_now": "Später",
"update_skip_version": "v{0} überspringen",
"check_for_updates": "Nach Updates suchen",
"update_checking": "Prüfe...",
"update_no_update": "Sie verwenden die neueste Version",
"update_urgency_critical": "Kritisches Update",
"update_urgency_high": "Wichtiges Update",
"update_current_version": "Aktuelle Version: v{0}",
"update_changelog": "Änderungsprotokoll",
"auth_choose_subtitle": "Wählen Sie eine Anmeldemethode, um fortzufahren",
"update_changelog_empty": "Für diese Version wurden keine Versionshinweise bereitgestellt.",
"subscription_login_title": "VberAI-Abonnementkonto",
"subscription_password_label": "Passwort",
"subscription_password_placeholder": "Passwort eingeben",
"subscription_login_button": "Mit VberAI anmelden",
"subscription_google_button": "Mit Google anmelden",
"subscription_google_pending": "Browser geöffnet. Schließen Sie die Google-Autorisierung ab und kehren Sie dann zu Cocos Creator zurück.",
"subscription_google_pending_button": "Warte auf Google-Autorisierung...",
"subscription_register_button": "VberAI-Konto erstellen",
"subscription_logging_in": "Anmeldung läuft...",
"subscription_credentials_required": "Bitte E-Mail und Passwort eingeben",
"subscription_login_success": "Bei Abonnementkonto angemeldet",
"subscription_login_failed": "Abonnement-Anmeldung fehlgeschlagen",
"subscription_not_entitled": "Ihr aktuelles Abonnement enthält Cocos 3.x MCP nicht. Führen Sie ein Upgrade durch, um fortzufahren.",
"subscription_upgrade_to": "Führen Sie ein Upgrade auf „{0}“ durch, um dieses Produkt zu verwenden",
"subscription_upgrade_action": "Abonnement upgraden",
"subscription_logged_in": "Angemeldetes Konto",
"subscription_choose_button": "VberAI-Abonnementkonto",
"license_choose_button": "Aktivierungscode",
"subscription_method_label": "Abonnement",
"back_to_login_choices": "← Andere Anmeldemethode wählen",
"subscription_signed_out_other_device": "Abgemeldet: Ihr Konto wurde auf einem anderen Gerät verwendet.",
"subscription_google_cancelled": "Google-Anmeldung wurde abgebrochen.",
"subscription_request_timeout": "Zeitüberschreitung der Anfrage. Überprüfen Sie Ihr Netzwerk und versuchen Sie es erneut.",
"subscription_network_error": "Netzwerkverbindung fehlgeschlagen. Überprüfen Sie Ihr Netzwerk und versuchen Sie es erneut.",
"subscription_server_error": "Der Server hat eine ungültige Antwort zurückgegeben. Versuchen Sie es später erneut.",
"subscription_google_authorization_failed": "Die Google-Autorisierung wurde nicht abgeschlossen. Bitte versuchen Sie es erneut."
};
@@ -0,0 +1,244 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP Server",
"description": "AI MCP Server for Cocos Creator 3.8",
"panel_title": "MCP Server",
"open_panel": "Open MCP Panel",
"start_server": "Start Server",
"stop_server": "Stop Server",
"server_status": "Server Status",
"port": "Port",
"settings": "Settings",
"connected": "Connected",
"disconnected": "Disconnected",
"server_running": "Server is running on port {0}",
"server_stopped": "Server has stopped",
"auto_start": "Auto Start",
"debug_log": "Debug Logging",
"max_connections": "Max Connections",
"connection_info": "Connection Info",
"http_url": "HTTP URL",
"copy": "Copy",
"save_settings": "Save Settings",
"settings_saved": "Settings saved successfully",
"server_started": "MCP Server Started",
"server_stopped_msg": "MCP Server Stopped",
"failed_to_start": "Failed to start server",
"failed_to_stop": "Failed to stop server",
"failed_to_save": "Failed to save settings",
"url_copied": "HTTP URL copied to clipboard",
"tool_manager": "Tool Manager",
"open_tool_manager": "Open Tool Manager",
"create_config": "Create Configuration",
"edit_config": "Edit Configuration",
"delete_config": "Delete Configuration",
"import_config": "Import Configuration",
"export_config": "Export Configuration",
"apply_config": "Apply Configuration",
"select_all": "Select All",
"deselect_all": "Deselect All",
"save_changes": "Save Changes",
"config_name": "Configuration Name",
"config_description": "Configuration Description",
"current_config": "Current Configuration",
"tool_management": "Tool Management",
"total_tools": "Total Tools",
"enabled_tools": "Enabled",
"disabled_tools": "Disabled",
"no_config_selected": "No Configuration Selected",
"select_config_first": "Please select a configuration or create a new one first",
"config_created": "Configuration created successfully",
"config_updated": "Configuration updated successfully",
"config_deleted": "Configuration deleted successfully",
"config_applied": "Configuration applied successfully",
"config_exported": "Configuration exported successfully",
"config_imported": "Configuration imported successfully",
"confirm_delete": "Confirm Delete",
"delete_config_confirm": "Are you sure you want to delete configuration \"{0}\"? This action cannot be undone.",
"max_config_slots_reached": "Maximum configuration slots reached ({0})",
"invalid_config_format": "Invalid configuration format",
"invalid_json_format": "Invalid JSON format or configuration structure",
"server_tab": "Server",
"tools_tab": "Tool Management",
"available_tools": "Available Tools",
"scene_tools": "Scene Tools",
"node_tools": "Node Tools",
"component_tools": "Component Tools",
"prefab_tools": "Prefab Tools",
"project_tools": "Project Tools",
"debug_tools": "Debug Tools",
"preferences_tools": "Preferences",
"server_tools": "Server Tools",
"broadcast_tools": "Broadcast Tools",
"scene_advanced_tools": "Advanced Scene Tools",
"scene_view_tools": "Scene View Tools",
"reference_image_tools": "Reference Image Tools",
"asset_advanced_tools": "Advanced Asset Tools",
"validation_tools": "Validation Tools",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "Email",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "Machine ID",
"purchase_renew": "Purchase / Renew",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "Choose a sign-in method",
"auth_subtitle": "Enter your email and activation code",
"auth_choose_subtitle": "Choose a sign-in method to continue",
"email_label": "Email Address",
"activation_code_label": "Activation Code",
"email_placeholder": "Enter email",
"code_placeholder": "Enter code (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "Activate",
"deactivate_button": "Deactivate",
"activating": "Activating...",
"deactivating": "Deactivating...",
"license_status": "License Status",
"license_type": "License Type",
"expiry_date": "Expiry Date",
"days_remaining": "Days Remaining",
"days_unit": "d",
"permanent": "Permanent",
"expiry_warning_title": "License Expiring Soon",
"expiry_warning_message": "Your license will expire in {0} days. Please renew to avoid service interruption.",
"renew_now": "Renew Now",
"i_understand": "I Understand",
"activation_success": "License activated successfully!",
"activation_failed": "Activation failed: {0}",
"invalid_code": "Invalid activation code format",
"purchase_license": "Purchase License",
"renew_license": "Renew License",
"contact_support": "Support",
"language_switch": "English",
"version_label": "Version",
"license_required": "License required to start server",
"license_check_failed": "License check failed",
"status_stopped": "Stopped",
"status_running": "Running",
"status_error": "Connection Failed",
"config_tab": "Quick Config",
"status_label": "Status",
"connections_count": "Connections",
"transport_protocol": "Transport Protocol",
"transport_type": "Transport Type",
"mcp_url": "MCP URL",
"port_fallback_title": "Port {0} unavailable — switched to {1}",
"port_fallback_reason": "Port {0} is reserved by the system or blocked by a firewall/antivirus (common on Windows with Hyper-V, WSL2 or Docker installed). The server is running normally on the new port.",
"port_fallback_resynced": "Updated the config for: {0}. Restart these clients for the new port to take effect.",
"port_fallback_no_clients": "If you have already configured AI clients, re-run the one-click setup to point them at the new port.",
"port_fallback_dismiss": "Got it",
"port_label": "Port",
"auto_start_label": "Auto Start",
"debug_log_label": "Debug Log",
"max_connections_label": "Max Connections",
"server_settings_title": "Server Settings",
"server_status_title": "Server Status",
"connection_info_title": "Connection Info",
"tools_count": "{0} tools",
"tools_stats": "{0} tools ({1} enabled / {2} disabled)",
"quick_config_title": "Quick Config MCP Clients",
"quick_config_desc": "Auto-configure the current MCP server into mainstream AI editors and CLI tools",
"server_info": "Server Info",
"server_name_label": "Server Name",
"server_address": "Server Address",
"timeout_ms": "Timeout (ms)",
"auto_config_title": "Auto Config (IDE Editors + Codex CLI)",
"add_all": "Add All",
"remove_all": "Remove All",
"auto_config_note": "Click button to auto-modify config files (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "Configured",
"not_configured": "Not Configured",
"auto_config_btn": "Auto Config",
"delete_btn": "Delete",
"copy_config": "Copy Config",
"open_config": "Open Config",
"manual_config_title": "Manual Config (Claude CLI + Gemini CLI)",
"refresh_commands": "Refresh Commands",
"manual_config_note": "The following CLI tools need manual terminal command execution",
"config_scope": "Config Scope:",
"scope_user": "User Level",
"scope_project": "Project Level",
"operation_log": "Operation Log",
"no_logs": "No operation records",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Based on Streamable HTTP Protocol (MCP 2025-03-26)",
"tools_copyright_line1": "Tool System - 13 intent-level tools, 150+ editor operations, action code dispatch",
"config_copyright_line1": "Quick Config - Supports Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "Please enter email and activation code",
"permanent_label": "Permanent",
"logout_button": "Logout",
"invite_code_label": "Invite Code",
"copy_button": "Copy",
"change_code_button": "Change Code",
"change_code_title": "Change Activation Code",
"change_code_desc": "Enter your new activation code. The current device binding will be released first.",
"confirm_change": "Confirm",
"cancel": "Cancel",
"code_required": "Please enter an activation code",
"expiry_inline_warning": "Expiring soon!",
"store_purchase_hint": "If you purchased this plugin from Cocos Store, please contact me to get your activation code.",
"license_expired_title": "License Expired",
"license_expired_message": "Your license has expired and the server cannot be started. Please renew or reactivate to continue.",
"go_to_login": "Go to Login",
"log_load_config_failed": "Failed to load config status: {0}",
"log_gen_cli_failed": "Failed to generate CLI commands: {0}",
"log_add_client_failed": "Failed to add to {0}: {1}",
"log_remove_client_failed": "Failed to remove from {0}: {1}",
"quick_config_dialog_title": "One-click Configuration",
"config_operation_failed": "Configuration operation failed",
"config_file_not_found_changed_path": "Configuration failed: could not find the config file. {0} may be installed in a custom path.",
"log_batch_add_failed": "Batch add failed: {0}",
"log_batch_remove_failed": "Batch remove failed: {0}",
"log_copied_config": "Copied {0} config to clipboard",
"log_gen_config_failed": "Failed to generate config",
"log_copy_config_failed": "Failed to copy config: {0}",
"log_copied_command": "Copied {0} command to clipboard",
"log_copy_command_failed": "Failed to copy command: {0}",
"log_opened_config_file": "Opened config file: {0}",
"log_open_config_failed": "Failed to open config file",
"log_open_config_error": "Failed to open config file: {0}",
"update_available_title": "Update Available",
"update_new_version": "New version v{0} is available!",
"update_now": "Update Now",
"update_not_now": "Not Now",
"update_skip_version": "Skip v{0}",
"check_for_updates": "Check for Updates",
"update_checking": "Checking...",
"update_no_update": "You are using the latest version",
"update_urgency_critical": "Critical Update",
"update_urgency_high": "Important Update",
"update_current_version": "Current version: v{0}",
"update_changelog": "Changelog"
,"update_changelog_empty": "No release notes were provided for this version."
,"subscription_login_title": "VberAI subscription account"
,"subscription_password_label": "Password"
,"subscription_password_placeholder": "Enter password"
,"subscription_login_button": "Sign in with VberAI"
,"subscription_google_button": "Sign in with Google"
,"subscription_google_pending": "Browser opened. Complete Google authorization, then return to Cocos Creator."
,"subscription_google_pending_button": "Waiting for Google authorization..."
,"subscription_register_button": "Create a VberAI account"
,"subscription_logging_in": "Signing in..."
,"subscription_credentials_required": "Please enter email and password"
,"subscription_login_success": "Subscription account signed in"
,"subscription_login_failed": "Subscription sign-in failed"
,"subscription_not_entitled": "Your current subscription does not include Cocos 3.x MCP. Upgrade to continue."
,"subscription_upgrade_to": "Upgrade to {0} to use this product"
,"subscription_upgrade_action": "Upgrade subscription"
,"subscription_logged_in": "Signed-in account"
,"subscription_choose_button": "VberAI subscription account"
,"license_choose_button": "Activation code"
,"subscription_method_label": "Subscription"
,"back_to_login_choices": "← Choose another sign-in method"
,"subscription_signed_out_other_device": "Signed out: your account was used on another device."
,"subscription_google_cancelled": "Google sign-in was cancelled."
,"subscription_request_timeout": "The request timed out. Check your network and try again."
,"subscription_network_error": "Network connection failed. Check your network and try again."
,"subscription_server_error": "The server returned an invalid response. Try again later."
,"subscription_google_authorization_failed": "Google authorization was not completed. Please try again."
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP Servidor",
"description": "Servidor AI MCP para Cocos Creator 3.8",
"panel_title": "Servidor MCP",
"open_panel": "Abrir panel MCP",
"start_server": "Iniciar servidor",
"stop_server": "Detener servidor",
"server_status": "Estado del servidor",
"port": "Puerto",
"settings": "Ajustes",
"connected": "Conectado",
"disconnected": "Desconectado",
"server_running": "El servidor está ejecutándose en el puerto {0}",
"server_stopped": "El servidor se ha detenido",
"auto_start": "Inicio automático",
"debug_log": "Registro de depuración",
"max_connections": "Conexiones máx.",
"connection_info": "Info de conexión",
"http_url": "URL HTTP",
"copy": "Copiar",
"save_settings": "Guardar",
"settings_saved": "Ajustes guardados correctamente",
"server_started": "Servidor MCP iniciado",
"server_stopped_msg": "Servidor MCP detenido",
"failed_to_start": "Error al iniciar el servidor",
"failed_to_stop": "Error al detener el servidor",
"failed_to_save": "Error al guardar los ajustes",
"url_copied": "URL HTTP copiada al portapapeles",
"tool_manager": "Gestor de herramientas",
"open_tool_manager": "Abrir gestor de herramientas",
"create_config": "Crear configuración",
"edit_config": "Editar configuración",
"delete_config": "Eliminar configuración",
"import_config": "Importar configuración",
"export_config": "Exportar configuración",
"apply_config": "Aplicar configuración",
"select_all": "Seleccionar todo",
"deselect_all": "Deseleccionar todo",
"save_changes": "Guardar cambios",
"config_name": "Nombre de configuración",
"config_description": "Descripción de configuración",
"current_config": "Configuración actual",
"tool_management": "Gestión de herramientas",
"total_tools": "Total de herramientas",
"enabled_tools": "Activadas",
"disabled_tools": "Desactivadas",
"no_config_selected": "Ninguna configuración seleccionada",
"select_config_first": "Por favor, seleccione o cree una configuración primero",
"config_created": "Configuración creada correctamente",
"config_updated": "Configuración actualizada correctamente",
"config_deleted": "Configuración eliminada correctamente",
"config_applied": "Configuración aplicada correctamente",
"config_exported": "Configuración exportada correctamente",
"config_imported": "Configuración importada correctamente",
"confirm_delete": "Confirmar eliminación",
"delete_config_confirm": "¿Está seguro de eliminar la configuración \"{0}\"? Esta acción no se puede deshacer.",
"max_config_slots_reached": "Se alcanzó el máximo de configuraciones ({0})",
"invalid_config_format": "Formato de configuración no válido",
"invalid_json_format": "Formato JSON o estructura de configuración no válidos",
"server_tab": "Servidor",
"tools_tab": "Gestión de herramientas",
"available_tools": "Herramientas disponibles",
"scene_tools": "Herramientas de escena",
"node_tools": "Herramientas de nodo",
"component_tools": "Herramientas de componente",
"prefab_tools": "Herramientas de prefab",
"project_tools": "Herramientas de proyecto",
"debug_tools": "Herramientas de depuración",
"preferences_tools": "Preferencias",
"server_tools": "Herramientas de servidor",
"broadcast_tools": "Herramientas de difusión",
"scene_advanced_tools": "Herramientas de escena avanzadas",
"scene_view_tools": "Herramientas de vista de escena",
"reference_image_tools": "Herramientas de imagen de referencia",
"asset_advanced_tools": "Herramientas de assets avanzadas",
"validation_tools": "Herramientas de validación",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "Correo",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "Código de máquina",
"purchase_renew": "Comprar / Renovar",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "Elegir método de inicio de sesión",
"auth_subtitle": "Ingrese su correo y código de activación",
"email_label": "Correo electrónico",
"activation_code_label": "Código de activación",
"email_placeholder": "Ingrese correo",
"code_placeholder": "Ingrese código (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "Activar",
"deactivate_button": "Desactivar",
"activating": "Activando...",
"deactivating": "Desactivando...",
"license_status": "Estado de licencia",
"license_type": "Tipo de licencia",
"expiry_date": "Fecha de vencimiento",
"days_remaining": "Días restantes",
"days_unit": "d",
"permanent": "Permanente",
"expiry_warning_title": "La licencia expira pronto",
"expiry_warning_message": "Su licencia expirará en {0} días. Por favor, renueve para evitar interrupciones.",
"renew_now": "Renovar ahora",
"i_understand": "Entendido",
"activation_success": "¡Licencia activada correctamente!",
"activation_failed": "Error de activación: {0}",
"invalid_code": "Formato de código de activación no válido",
"purchase_license": "Comprar licencia",
"renew_license": "Renovar licencia",
"contact_support": "Soporte",
"language_switch": "Español",
"version_label": "Versión",
"license_required": "Se requiere licencia para iniciar el servidor",
"license_check_failed": "Error en la verificación de licencia",
"status_stopped": "Detenido",
"status_running": "En ejecución",
"status_error": "Error de conexión",
"config_tab": "Config rápida",
"status_label": "Estado",
"connections_count": "Conexiones",
"transport_protocol": "Protocolo de transporte",
"transport_type": "Tipo de transporte",
"mcp_url": "URL MCP",
"port_fallback_title": "Puerto {0} no disponible: se cambió a {1}",
"port_fallback_reason": "El puerto {0} está reservado por el sistema o bloqueado por un firewall/antivirus (habitual en Windows con Hyper-V, WSL2 o Docker). El servidor funciona con normalidad en el nuevo puerto.",
"port_fallback_resynced": "Se actualizó la configuración de: {0}. Reinicie estos clientes para que el nuevo puerto surta efecto.",
"port_fallback_no_clients": "Si ya configuró clientes de IA, vuelva a configurarlos en el nuevo puerto desde la configuración con un clic.",
"port_fallback_dismiss": "Entendido",
"port_label": "Puerto",
"auto_start_label": "Inicio auto",
"debug_log_label": "Registro de depuración",
"max_connections_label": "Conexiones máx.",
"server_settings_title": "Ajustes del servidor",
"server_status_title": "Estado del servidor",
"connection_info_title": "Info de conexión",
"tools_count": "{0} herramientas",
"tools_stats": "{0} herramientas ({1} activadas / {2} desactivadas)",
"quick_config_title": "Config rápida de clientes MCP",
"quick_config_desc": "Configurar automáticamente el servidor MCP en editores AI y herramientas CLI",
"server_info": "Info del servidor",
"server_name_label": "Nombre del servidor",
"server_address": "Dirección del servidor",
"timeout_ms": "Tiempo de espera (ms)",
"auto_config_title": "Config auto (IDE + Codex CLI)",
"add_all": "Añadir todo",
"remove_all": "Eliminar todo",
"auto_config_note": "Haga clic para modificar automáticamente los archivos de config (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "Configurado",
"not_configured": "No configurado",
"auto_config_btn": "Config auto",
"delete_btn": "Eliminar",
"copy_config": "Copiar config",
"open_config": "Abrir config",
"manual_config_title": "Config manual (Claude CLI + Gemini CLI)",
"refresh_commands": "Actualizar comandos",
"manual_config_note": "Las siguientes herramientas CLI requieren ejecución manual en terminal",
"config_scope": "Ámbito de config:",
"scope_user": "Nivel de usuario",
"scope_project": "Nivel de proyecto",
"operation_log": "Registro de operaciones",
"no_logs": "Sin registros",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Basado en el protocolo Streamable HTTP (MCP 2025-03-26)",
"tools_copyright_line1": "Sistema de herramientas - 13 herramientas intent-level, 150+ operaciones de editor, despacho de código de acción",
"config_copyright_line1": "Config rápida - Soporta Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "Por favor ingrese correo y código de activación",
"permanent_label": "Permanente",
"logout_button": "Cerrar sesión",
"invite_code_label": "Código de invitación",
"copy_button": "Copiar",
"change_code_button": "Cambiar código",
"change_code_title": "Cambiar código de activación",
"change_code_desc": "Ingrese su nuevo código de activación. El enlace del dispositivo actual se liberará primero.",
"confirm_change": "Confirmar",
"cancel": "Cancelar",
"code_required": "Por favor ingrese un código de activación",
"expiry_inline_warning": "¡Expira pronto!",
"store_purchase_hint": "Si compró este plugin en Cocos Store, contácteme para obtener su código de activación.",
"license_expired_title": "Licencia expirada",
"license_expired_message": "Su licencia ha expirado y el servidor no puede iniciarse. Renueve o reactive para continuar.",
"go_to_login": "Ir a inicio de sesión",
"log_load_config_failed": "Error al cargar el estado de config: {0}",
"log_gen_cli_failed": "Error al generar comandos CLI: {0}",
"log_add_client_failed": "Error al añadir a {0}: {1}",
"log_remove_client_failed": "Error al eliminar de {0}: {1}",
"quick_config_dialog_title": "Configuración con un clic",
"config_operation_failed": "Error en la operación de configuración",
"config_file_not_found_changed_path": "Error de configuración: no se encontró el archivo de configuración. Es posible que {0} se haya instalado en una ruta personalizada.",
"log_batch_add_failed": "Error en la adición masiva: {0}",
"log_batch_remove_failed": "Error en la eliminación masiva: {0}",
"log_copied_config": "Config de {0} copiada al portapapeles",
"log_gen_config_failed": "Error al generar la config",
"log_copy_config_failed": "Error al copiar la config: {0}",
"log_copied_command": "Comando {0} copiado al portapapeles",
"log_copy_command_failed": "Error al copiar el comando: {0}",
"log_opened_config_file": "Archivo de config abierto: {0}",
"log_open_config_failed": "Error al abrir archivo de config",
"log_open_config_error": "Error al abrir archivo de config: {0}",
"update_available_title": "Actualización disponible",
"update_new_version": "¡La nueva versión v{0} está disponible!",
"update_now": "Actualizar ahora",
"update_not_now": "Ahora no",
"update_skip_version": "Omitir v{0}",
"check_for_updates": "Buscar actualizaciones",
"update_checking": "Comprobando...",
"update_no_update": "Está usando la última versión",
"update_urgency_critical": "Actualización crítica",
"update_urgency_high": "Actualización importante",
"update_current_version": "Versión actual: v{0}",
"update_changelog": "Registro de cambios",
"auth_choose_subtitle": "Elige un método de inicio de sesión para continuar",
"update_changelog_empty": "No se proporcionaron notas de la versión para esta versión.",
"subscription_login_title": "Cuenta de suscripción de VberAI",
"subscription_password_label": "Contraseña",
"subscription_password_placeholder": "Introduce la contraseña",
"subscription_login_button": "Iniciar sesión con VberAI",
"subscription_google_button": "Iniciar sesión con Google",
"subscription_google_pending": "Navegador abierto. Completa la autorización de Google y vuelve a Cocos Creator.",
"subscription_google_pending_button": "Esperando la autorización de Google...",
"subscription_register_button": "Crear una cuenta de VberAI",
"subscription_logging_in": "Iniciando sesión...",
"subscription_credentials_required": "Introduce el correo electrónico y la contraseña",
"subscription_login_success": "Sesión iniciada en la cuenta de suscripción",
"subscription_login_failed": "Error al iniciar sesión en la suscripción",
"subscription_not_entitled": "Tu suscripción actual no incluye Cocos 3.x MCP. Actualiza para continuar.",
"subscription_upgrade_to": "Actualiza a «{0}» para usar este producto",
"subscription_upgrade_action": "Mejorar suscripción",
"subscription_logged_in": "Cuenta con sesión iniciada",
"subscription_choose_button": "Cuenta de suscripción de VberAI",
"license_choose_button": "Código de activación",
"subscription_method_label": "Suscripción",
"back_to_login_choices": "← Elegir otro método de inicio de sesión",
"subscription_signed_out_other_device": "Sesión cerrada: tu cuenta se usó en otro dispositivo.",
"subscription_google_cancelled": "Se canceló el inicio de sesión con Google.",
"subscription_request_timeout": "Se agotó el tiempo de la solicitud. Comprueba tu red e inténtalo de nuevo.",
"subscription_network_error": "Falló la conexión de red. Comprueba tu red e inténtalo de nuevo.",
"subscription_server_error": "El servidor devolvió una respuesta no válida. Inténtalo más tarde.",
"subscription_google_authorization_failed": "No se completó la autorización de Google. Inténtalo de nuevo."
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP Serveur",
"description": "Serveur AI MCP pour Cocos Creator 3.8",
"panel_title": "Serveur MCP",
"open_panel": "Ouvrir le panneau MCP",
"start_server": "Démarrer le serveur",
"stop_server": "Arrêter le serveur",
"server_status": "État du serveur",
"port": "Port",
"settings": "Paramètres",
"connected": "Connecté",
"disconnected": "Déconnecté",
"server_running": "Le serveur fonctionne sur le port {0}",
"server_stopped": "Le serveur est arrêté",
"auto_start": "Démarrage automatique",
"debug_log": "Journal de débogage",
"max_connections": "Connexions max",
"connection_info": "Infos de connexion",
"http_url": "URL HTTP",
"copy": "Copier",
"save_settings": "Enregistrer",
"settings_saved": "Paramètres enregistrés avec succès",
"server_started": "Serveur MCP démarré",
"server_stopped_msg": "Serveur MCP arrêté",
"failed_to_start": "Échec du démarrage du serveur",
"failed_to_stop": "Échec de l'arrêt du serveur",
"failed_to_save": "Échec de l'enregistrement",
"url_copied": "URL HTTP copiée dans le presse-papiers",
"tool_manager": "Gestionnaire d'outils",
"open_tool_manager": "Ouvrir le gestionnaire d'outils",
"create_config": "Créer une configuration",
"edit_config": "Modifier la configuration",
"delete_config": "Supprimer la configuration",
"import_config": "Importer la configuration",
"export_config": "Exporter la configuration",
"apply_config": "Appliquer la configuration",
"select_all": "Tout sélectionner",
"deselect_all": "Tout désélectionner",
"save_changes": "Enregistrer les modifications",
"config_name": "Nom de configuration",
"config_description": "Description de configuration",
"current_config": "Configuration actuelle",
"tool_management": "Gestion des outils",
"total_tools": "Total des outils",
"enabled_tools": "Activés",
"disabled_tools": "Désactivés",
"no_config_selected": "Aucune configuration sélectionnée",
"select_config_first": "Veuillez d'abord sélectionner ou créer une configuration",
"config_created": "Configuration créée avec succès",
"config_updated": "Configuration mise à jour avec succès",
"config_deleted": "Configuration supprimée avec succès",
"config_applied": "Configuration appliquée avec succès",
"config_exported": "Configuration exportée avec succès",
"config_imported": "Configuration importée avec succès",
"confirm_delete": "Confirmer la suppression",
"delete_config_confirm": "Voulez-vous vraiment supprimer la configuration \"{0}\" ? Cette action est irréversible.",
"max_config_slots_reached": "Nombre maximal de configurations atteint ({0})",
"invalid_config_format": "Format de configuration invalide",
"invalid_json_format": "Format JSON ou structure de configuration invalide",
"server_tab": "Serveur",
"tools_tab": "Gestion des outils",
"available_tools": "Outils disponibles",
"scene_tools": "Outils de scène",
"node_tools": "Outils de nœud",
"component_tools": "Outils de composant",
"prefab_tools": "Outils de prefab",
"project_tools": "Outils de projet",
"debug_tools": "Outils de débogage",
"preferences_tools": "Préférences",
"server_tools": "Outils serveur",
"broadcast_tools": "Outils de diffusion",
"scene_advanced_tools": "Outils de scène avancés",
"scene_view_tools": "Outils de vue de scène",
"reference_image_tools": "Outils d'image de référence",
"asset_advanced_tools": "Outils d'assets avancés",
"validation_tools": "Outils de validation",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "E-mail",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "Code machine",
"purchase_renew": "Acheter / Renouveler",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "Choisir une méthode de connexion",
"auth_subtitle": "Entrez votre e-mail et votre code d'activation",
"email_label": "Adresse e-mail",
"activation_code_label": "Code d'activation",
"email_placeholder": "Entrez l'e-mail",
"code_placeholder": "Entrez le code (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "Activer",
"deactivate_button": "Désactiver",
"activating": "Activation...",
"deactivating": "Désactivation...",
"license_status": "État de la licence",
"license_type": "Type de licence",
"expiry_date": "Date d'expiration",
"days_remaining": "Jours restants",
"days_unit": "j",
"permanent": "Permanent",
"expiry_warning_title": "Licence bientôt expirée",
"expiry_warning_message": "Votre licence expirera dans {0} jours. Veuillez renouveler pour éviter toute interruption.",
"renew_now": "Renouveler",
"i_understand": "J'ai compris",
"activation_success": "Licence activée avec succès !",
"activation_failed": "Échec de l'activation : {0}",
"invalid_code": "Format de code d'activation invalide",
"purchase_license": "Acheter une licence",
"renew_license": "Renouveler la licence",
"contact_support": "Support",
"language_switch": "Français",
"version_label": "Version",
"license_required": "Une licence est requise pour démarrer le serveur",
"license_check_failed": "Échec de la vérification de licence",
"status_stopped": "Arrêté",
"status_running": "En cours",
"status_error": "Échec de connexion",
"config_tab": "Config rapide",
"status_label": "État",
"connections_count": "Connexions",
"transport_protocol": "Protocole de transport",
"transport_type": "Type de transport",
"mcp_url": "URL MCP",
"port_fallback_title": "Port {0} indisponible — basculé vers {1}",
"port_fallback_reason": "Le port {0} est réservé par le système ou bloqué par un pare-feu/antivirus (fréquent sous Windows avec Hyper-V, WSL2 ou Docker). Le serveur fonctionne normalement sur le nouveau port.",
"port_fallback_resynced": "Configuration mise à jour pour : {0}. Redémarrez ces clients pour que le nouveau port prenne effet.",
"port_fallback_no_clients": "Si vous avez déjà configuré des clients IA, reconfigurez-les sur le nouveau port via la configuration en un clic.",
"port_fallback_dismiss": "Compris",
"port_label": "Port",
"auto_start_label": "Démarrage auto",
"debug_log_label": "Journal de débogage",
"max_connections_label": "Connexions max",
"server_settings_title": "Paramètres du serveur",
"server_status_title": "État du serveur",
"connection_info_title": "Infos de connexion",
"tools_count": "{0} outils",
"tools_stats": "{0} outils ({1} activés / {2} désactivés)",
"quick_config_title": "Config rapide des clients MCP",
"quick_config_desc": "Configurer automatiquement le serveur MCP dans les éditeurs AI et outils CLI",
"server_info": "Infos serveur",
"server_name_label": "Nom du serveur",
"server_address": "Adresse du serveur",
"timeout_ms": "Délai d'attente (ms)",
"auto_config_title": "Config auto (IDE + Codex CLI)",
"add_all": "Tout ajouter",
"remove_all": "Tout supprimer",
"auto_config_note": "Cliquez pour modifier automatiquement les fichiers de config (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "Configuré",
"not_configured": "Non configuré",
"auto_config_btn": "Config auto",
"delete_btn": "Supprimer",
"copy_config": "Copier la config",
"open_config": "Ouvrir la config",
"manual_config_title": "Config manuelle (Claude CLI + Gemini CLI)",
"refresh_commands": "Actualiser les commandes",
"manual_config_note": "Les outils CLI suivants nécessitent une exécution manuelle dans le terminal",
"config_scope": "Portée de config :",
"scope_user": "Niveau utilisateur",
"scope_project": "Niveau projet",
"operation_log": "Journal des opérations",
"no_logs": "Aucun enregistrement",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Basé sur le protocole Streamable HTTP (MCP 2025-03-26)",
"tools_copyright_line1": "Système d'outils - 13 outils intent-level, 150+ opérations éditeur, dispatch par code d'action",
"config_copyright_line1": "Config rapide - Supporte Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "Veuillez entrer l'e-mail et le code d'activation",
"permanent_label": "Permanent",
"logout_button": "Déconnexion",
"invite_code_label": "Code d'invitation",
"copy_button": "Copier",
"change_code_button": "Changer le code",
"change_code_title": "Changer le code d'activation",
"change_code_desc": "Entrez votre nouveau code d'activation. La liaison de l'appareil actuel sera d'abord libérée.",
"confirm_change": "Confirmer",
"cancel": "Annuler",
"code_required": "Veuillez entrer un code d'activation",
"expiry_inline_warning": "Expiration imminente !",
"store_purchase_hint": "Si vous avez acheté ce plugin sur Cocos Store, veuillez me contacter pour obtenir votre code d'activation.",
"license_expired_title": "Licence expirée",
"license_expired_message": "Votre licence a expiré et le serveur ne peut pas être démarré. Veuillez renouveler ou réactiver pour continuer.",
"go_to_login": "Se connecter",
"log_load_config_failed": "Échec du chargement de la config : {0}",
"log_gen_cli_failed": "Échec de la génération des commandes CLI : {0}",
"log_add_client_failed": "Échec de l'ajout à {0} : {1}",
"log_remove_client_failed": "Échec de la suppression de {0} : {1}",
"quick_config_dialog_title": "Configuration en un clic",
"config_operation_failed": "Échec de l'opération de configuration",
"config_file_not_found_changed_path": "Échec de la configuration : fichier de configuration introuvable. {0} a peut-être été installé dans un chemin personnalisé.",
"log_batch_add_failed": "Échec de l'ajout groupé : {0}",
"log_batch_remove_failed": "Échec de la suppression groupée : {0}",
"log_copied_config": "Config {0} copiée dans le presse-papiers",
"log_gen_config_failed": "Échec de la génération de la config",
"log_copy_config_failed": "Échec de la copie de la config : {0}",
"log_copied_command": "Commande {0} copiée dans le presse-papiers",
"log_copy_command_failed": "Échec de la copie de la commande : {0}",
"log_opened_config_file": "Fichier de config ouvert : {0}",
"log_open_config_failed": "Échec de l'ouverture du fichier de config",
"log_open_config_error": "Échec de l'ouverture du fichier de config : {0}",
"update_available_title": "Mise à jour disponible",
"update_new_version": "La nouvelle version v{0} est disponible !",
"update_now": "Mettre à jour",
"update_not_now": "Plus tard",
"update_skip_version": "Ignorer v{0}",
"check_for_updates": "Vérifier les mises à jour",
"update_checking": "Vérification...",
"update_no_update": "Vous utilisez la dernière version",
"update_urgency_critical": "Mise à jour critique",
"update_urgency_high": "Mise à jour importante",
"update_current_version": "Version actuelle : v{0}",
"update_changelog": "Journal des modifications",
"auth_choose_subtitle": "Choisissez une méthode de connexion pour continuer",
"update_changelog_empty": "Aucune note de version n'a été fournie pour cette version.",
"subscription_login_title": "Compte d'abonnement VberAI",
"subscription_password_label": "Mot de passe",
"subscription_password_placeholder": "Saisissez le mot de passe",
"subscription_login_button": "Se connecter avec VberAI",
"subscription_google_button": "Se connecter avec Google",
"subscription_google_pending": "Navigateur ouvert. Terminez l'autorisation Google, puis revenez à Cocos Creator.",
"subscription_google_pending_button": "En attente de l'autorisation Google...",
"subscription_register_button": "Créer un compte VberAI",
"subscription_logging_in": "Connexion en cours...",
"subscription_credentials_required": "Veuillez saisir l'e-mail et le mot de passe",
"subscription_login_success": "Connecté au compte d'abonnement",
"subscription_login_failed": "Échec de la connexion à l'abonnement",
"subscription_not_entitled": "Votre abonnement actuel n'inclut pas Cocos 3.x MCP. Effectuez une mise à niveau pour continuer.",
"subscription_upgrade_to": "Passez à « {0} » pour utiliser ce produit",
"subscription_upgrade_action": "Mettre à niveau l'abonnement",
"subscription_logged_in": "Compte connecté",
"subscription_choose_button": "Compte d'abonnement VberAI",
"license_choose_button": "Code d'activation",
"subscription_method_label": "Abonnement",
"back_to_login_choices": "← Choisir une autre méthode de connexion",
"subscription_signed_out_other_device": "Déconnecté : votre compte a été utilisé sur un autre appareil.",
"subscription_google_cancelled": "La connexion Google a été annulée.",
"subscription_request_timeout": "La requête a expiré. Vérifiez votre réseau et réessayez.",
"subscription_network_error": "Échec de la connexion réseau. Vérifiez votre réseau et réessayez.",
"subscription_server_error": "Le serveur a renvoyé une réponse non valide. Réessayez plus tard.",
"subscription_google_authorization_failed": "L'autorisation Google n'a pas été effectuée. Veuillez réessayer."
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP サーバー",
"description": "Cocos Creator 3.8 用 AI MCP サーバー",
"panel_title": "MCP サーバー",
"open_panel": "MCP パネルを開く",
"start_server": "サーバー起動",
"stop_server": "サーバー停止",
"server_status": "サーバー状態",
"port": "ポート",
"settings": "設定",
"connected": "接続済み",
"disconnected": "未接続",
"server_running": "サーバーはポート {0} で実行中",
"server_stopped": "サーバーは停止しました",
"auto_start": "自動起動",
"debug_log": "デバッグログ",
"max_connections": "最大接続数",
"connection_info": "接続情報",
"http_url": "HTTP URL",
"copy": "コピー",
"save_settings": "設定を保存",
"settings_saved": "設定の保存に成功しました",
"server_started": "MCP サーバーが起動しました",
"server_stopped_msg": "MCP サーバーが停止しました",
"failed_to_start": "サーバーの起動に失敗しました",
"failed_to_stop": "サーバーの停止に失敗しました",
"failed_to_save": "設定の保存に失敗しました",
"url_copied": "HTTP URL をクリップボードにコピーしました",
"tool_manager": "ツールマネージャー",
"open_tool_manager": "ツールマネージャーを開く",
"create_config": "設定を作成",
"edit_config": "設定を編集",
"delete_config": "設定を削除",
"import_config": "設定をインポート",
"export_config": "設定をエクスポート",
"apply_config": "設定を適用",
"select_all": "全選択",
"deselect_all": "全解除",
"save_changes": "変更を保存",
"config_name": "設定名",
"config_description": "設定の説明",
"current_config": "現在の設定",
"tool_management": "ツール管理",
"total_tools": "ツール合計",
"enabled_tools": "有効",
"disabled_tools": "無効",
"no_config_selected": "設定が選択されていません",
"select_config_first": "先に設定を選択するか、新しく作成してください",
"config_created": "設定の作成に成功しました",
"config_updated": "設定の更新に成功しました",
"config_deleted": "設定の削除に成功しました",
"config_applied": "設定の適用に成功しました",
"config_exported": "設定のエクスポートに成功しました",
"config_imported": "設定のインポートに成功しました",
"confirm_delete": "削除の確認",
"delete_config_confirm": "設定 \"{0}\" を削除してもよろしいですか?この操作は元に戻せません。",
"max_config_slots_reached": "設定スロットの上限に達しました ({0})",
"invalid_config_format": "無効な設定形式",
"invalid_json_format": "無効なJSON形式または設定構造",
"server_tab": "サーバー",
"tools_tab": "ツール管理",
"available_tools": "利用可能なツール",
"scene_tools": "シーンツール",
"node_tools": "ノードツール",
"component_tools": "コンポーネントツール",
"prefab_tools": "プレハブツール",
"project_tools": "プロジェクトツール",
"debug_tools": "デバッグツール",
"preferences_tools": "環境設定",
"server_tools": "サーバーツール",
"broadcast_tools": "ブロードキャストツール",
"scene_advanced_tools": "高度なシーンツール",
"scene_view_tools": "シーンビューツール",
"reference_image_tools": "参照画像ツール",
"asset_advanced_tools": "高度なアセットツール",
"validation_tools": "検証ツール",
"brand_name": "LiDaxian MCP シリーズ",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "メール",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "マシンID",
"purchase_renew": "購入 / 更新",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "ログイン方法を選択",
"auth_subtitle": "メールアドレスとアクティベーションコードを入力してください",
"email_label": "メールアドレス",
"activation_code_label": "アクティベーションコード",
"email_placeholder": "メールを入力",
"code_placeholder": "コードを入力 (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "アクティベート",
"deactivate_button": "無効化",
"activating": "アクティベート中...",
"deactivating": "無効化中...",
"license_status": "ライセンス状態",
"license_type": "ライセンス種別",
"expiry_date": "有効期限",
"days_remaining": "残り日数",
"days_unit": "日",
"permanent": "永久",
"expiry_warning_title": "ライセンスの有効期限が近づいています",
"expiry_warning_message": "ライセンスはあと {0} 日で期限切れになります。サービスの中断を防ぐため、更新してください。",
"renew_now": "今すぐ更新",
"i_understand": "了解しました",
"activation_success": "ライセンスのアクティベーションに成功しました!",
"activation_failed": "アクティベーション失敗: {0}",
"invalid_code": "無効なアクティベーションコード形式",
"purchase_license": "ライセンスを購入",
"renew_license": "ライセンスを更新",
"contact_support": "サポート",
"language_switch": "日本語",
"version_label": "バージョン",
"license_required": "サーバーの起動にはライセンスが必要です",
"license_check_failed": "ライセンスチェックに失敗しました",
"status_stopped": "停止中",
"status_running": "実行中",
"status_error": "接続失敗",
"config_tab": "クイック設定",
"status_label": "状態",
"connections_count": "接続数",
"transport_protocol": "トランスポートプロトコル",
"transport_type": "トランスポートタイプ",
"mcp_url": "MCP URL",
"port_fallback_title": "ポート {0} が使用できないため、{1} に切り替えました",
"port_fallback_reason": "ポート {0} はシステムに予約されているか、ファイアウォール/ウイルス対策ソフトにブロックされています(Hyper-V、WSL2、Docker を導入した Windows でよく起こります)。サーバーは新しいポートで正常に動作しています。",
"port_fallback_resynced": "次のクライアントの設定を更新しました:{0}。新しいポートを有効にするには再起動してください。",
"port_fallback_no_clients": "AI クライアントを設定済みの場合は、「ワンクリック設定」で新しいポートに設定し直してください。",
"port_fallback_dismiss": "了解",
"port_label": "ポート",
"auto_start_label": "自動起動",
"debug_log_label": "デバッグログ",
"max_connections_label": "最大接続数",
"server_settings_title": "サーバー設定",
"server_status_title": "サーバー状態",
"connection_info_title": "接続情報",
"tools_count": "{0} ツール",
"tools_stats": "{0} ツール ({1} 有効 / {2} 無効)",
"quick_config_title": "MCP クライアントのクイック設定",
"quick_config_desc": "現在のMCPサーバーを主要AIエディターやCLIツールに自動設定します",
"server_info": "サーバー情報",
"server_name_label": "サーバー名",
"server_address": "サーバーアドレス",
"timeout_ms": "タイムアウト (ms)",
"auto_config_title": "自動設定 (IDE エディター + Codex CLI)",
"add_all": "すべて追加",
"remove_all": "すべて削除",
"auto_config_note": "ボタンをクリックして設定ファイルを自動変更 (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "設定済み",
"not_configured": "未設定",
"auto_config_btn": "自動設定",
"delete_btn": "削除",
"copy_config": "設定をコピー",
"open_config": "設定を開く",
"manual_config_title": "手動設定 (Claude CLI + Gemini CLI)",
"refresh_commands": "コマンドを更新",
"manual_config_note": "以下のCLIツールはターミナルで手動実行が必要です",
"config_scope": "設定範囲:",
"scope_user": "ユーザーレベル",
"scope_project": "プロジェクトレベル",
"operation_log": "操作ログ",
"no_logs": "操作記録なし",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Streamable HTTP プロトコル (MCP 2025-03-26) 準拠",
"tools_copyright_line1": "ツールシステム - 13のインテントレベルツール、150+のエディター操作、アクションコードディスパッチ",
"config_copyright_line1": "クイック設定 - Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI 対応",
"email_required": "メールとアクティベーションコードを入力してください",
"permanent_label": "永久",
"logout_button": "ログアウト",
"invite_code_label": "招待コード",
"copy_button": "コピー",
"change_code_button": "コード変更",
"change_code_title": "アクティベーションコードの変更",
"change_code_desc": "新しいアクティベーションコードを入力してください。現在のデバイスバインドは先に解除されます。",
"confirm_change": "確認",
"cancel": "キャンセル",
"code_required": "アクティベーションコードを入力してください",
"expiry_inline_warning": "まもなく期限切れ!",
"store_purchase_hint": "Cocos Store で本プラグインを購入済みの場合は、アクティベーションコードの取得にご連絡ください。",
"license_expired_title": "ライセンスの有効期限切れ",
"license_expired_message": "ライセンスの有効期限が切れたため、サーバーを起動できません。続行するには更新または再アクティベートしてください。",
"go_to_login": "ログインへ",
"log_load_config_failed": "設定状態の読み込みに失敗: {0}",
"log_gen_cli_failed": "CLIコマンドの生成に失敗: {0}",
"log_add_client_failed": "{0} への追加に失敗: {1}",
"log_remove_client_failed": "{0} からの削除に失敗: {1}",
"quick_config_dialog_title": "ワンクリック設定",
"config_operation_failed": "設定操作に失敗しました",
"config_file_not_found_changed_path": "設定に失敗しました: 設定ファイルが見つかりません。{0} がカスタムパスにインストールされている可能性があります。",
"log_batch_add_failed": "一括追加に失敗: {0}",
"log_batch_remove_failed": "一括削除に失敗: {0}",
"log_copied_config": "{0} の設定をクリップボードにコピーしました",
"log_gen_config_failed": "設定の生成に失敗しました",
"log_copy_config_failed": "設定のコピーに失敗: {0}",
"log_copied_command": "{0} コマンドをクリップボードにコピーしました",
"log_copy_command_failed": "コマンドのコピーに失敗: {0}",
"log_opened_config_file": "設定ファイルを開きました: {0}",
"log_open_config_failed": "設定ファイルを開けませんでした",
"log_open_config_error": "設定ファイルを開けませんでした: {0}",
"update_available_title": "アップデートがあります",
"update_new_version": "新バージョン v{0} がリリースされました!",
"update_now": "今すぐ更新",
"update_not_now": "後で",
"update_skip_version": "v{0} をスキップ",
"check_for_updates": "アップデートを確認",
"update_checking": "確認中...",
"update_no_update": "最新バージョンです",
"update_urgency_critical": "緊急アップデート",
"update_urgency_high": "重要なアップデート",
"update_current_version": "現在のバージョン: v{0}",
"update_changelog": "変更履歴",
"auth_choose_subtitle": "サインイン方法を選択して続行してください",
"update_changelog_empty": "このバージョンのリリースノートはありません。",
"subscription_login_title": "VberAI サブスクリプションアカウント",
"subscription_password_label": "パスワード",
"subscription_password_placeholder": "パスワードを入力",
"subscription_login_button": "VberAI アカウントでログイン",
"subscription_google_button": "Google でログイン",
"subscription_google_pending": "ブラウザを開きました。Google 認証を完了してから Cocos Creator に戻ってください。",
"subscription_google_pending_button": "Google 認証を待っています...",
"subscription_register_button": "VberAI アカウントを作成",
"subscription_logging_in": "ログイン中...",
"subscription_credentials_required": "メールアドレスとパスワードを入力してください",
"subscription_login_success": "サブスクリプションアカウントにログインしました",
"subscription_login_failed": "サブスクリプションのログインに失敗しました",
"subscription_not_entitled": "現在のサブスクリプションには Cocos 3.x MCP が含まれていません。続行するにはアップグレードしてください。",
"subscription_upgrade_to": "このプロダクトを使用するには「{0}」にアップグレードしてください",
"subscription_upgrade_action": "サブスクリプションをアップグレード",
"subscription_logged_in": "ログイン中のアカウント",
"subscription_choose_button": "VberAI サブスクリプションアカウント",
"license_choose_button": "アクティベーションコード",
"subscription_method_label": "サブスク",
"back_to_login_choices": "← 別のサインイン方法を選択",
"subscription_signed_out_other_device": "サインアウトしました:アカウントが別のデバイスで使用されました。",
"subscription_google_cancelled": "Google ログインがキャンセルされました。",
"subscription_request_timeout": "リクエストがタイムアウトしました。ネットワークを確認して再試行してください。",
"subscription_network_error": "ネットワーク接続に失敗しました。ネットワークを確認して再試行してください。",
"subscription_server_error": "サーバーから無効な応答が返されました。後でもう一度お試しください。",
"subscription_google_authorization_failed": "Google 認証が完了しませんでした。もう一度お試しください。"
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP 서버",
"description": "Cocos Creator 3.8용 AI MCP 서버",
"panel_title": "MCP 서버",
"open_panel": "MCP 패널 열기",
"start_server": "서버 시작",
"stop_server": "서버 중지",
"server_status": "서버 상태",
"port": "포트",
"settings": "설정",
"connected": "연결됨",
"disconnected": "연결 안 됨",
"server_running": "서버가 포트 {0}에서 실행 중입니다",
"server_stopped": "서버가 중지되었습니다",
"auto_start": "자동 시작",
"debug_log": "디버그 로그",
"max_connections": "최대 연결 수",
"connection_info": "연결 정보",
"http_url": "HTTP URL",
"copy": "복사",
"save_settings": "설정 저장",
"settings_saved": "설정이 저장되었습니다",
"server_started": "MCP 서버가 시작되었습니다",
"server_stopped_msg": "MCP 서버가 중지되었습니다",
"failed_to_start": "서버 시작 실패",
"failed_to_stop": "서버 중지 실패",
"failed_to_save": "설정 저장 실패",
"url_copied": "HTTP URL이 클립보드에 복사되었습니다",
"tool_manager": "도구 관리자",
"open_tool_manager": "도구 관리자 열기",
"create_config": "구성 생성",
"edit_config": "구성 편집",
"delete_config": "구성 삭제",
"import_config": "구성 가져오기",
"export_config": "구성 내보내기",
"apply_config": "구성 적용",
"select_all": "전체 선택",
"deselect_all": "전체 해제",
"save_changes": "변경 사항 저장",
"config_name": "구성 이름",
"config_description": "구성 설명",
"current_config": "현재 구성",
"tool_management": "도구 관리",
"total_tools": "전체 도구",
"enabled_tools": "활성화",
"disabled_tools": "비활성화",
"no_config_selected": "선택된 구성 없음",
"select_config_first": "먼저 구성을 선택하거나 새로 만들어 주세요",
"config_created": "구성이 생성되었습니다",
"config_updated": "구성이 업데이트되었습니다",
"config_deleted": "구성이 삭제되었습니다",
"config_applied": "구성이 적용되었습니다",
"config_exported": "구성이 내보내기되었습니다",
"config_imported": "구성이 가져오기되었습니다",
"confirm_delete": "삭제 확인",
"delete_config_confirm": "구성 \"{0}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
"max_config_slots_reached": "최대 구성 슬롯에 도달했습니다 ({0})",
"invalid_config_format": "잘못된 구성 형식",
"invalid_json_format": "잘못된 JSON 형식 또는 구성 구조",
"server_tab": "서버",
"tools_tab": "도구 관리",
"available_tools": "사용 가능한 도구",
"scene_tools": "씬 도구",
"node_tools": "노드 도구",
"component_tools": "컴포넌트 도구",
"prefab_tools": "프리팹 도구",
"project_tools": "프로젝트 도구",
"debug_tools": "디버그 도구",
"preferences_tools": "환경설정",
"server_tools": "서버 도구",
"broadcast_tools": "브로드캐스트 도구",
"scene_advanced_tools": "고급 씬 도구",
"scene_view_tools": "씬 뷰 도구",
"reference_image_tools": "참조 이미지 도구",
"asset_advanced_tools": "고급 에셋 도구",
"validation_tools": "검증 도구",
"brand_name": "LiDaxian MCP 시리즈",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "이메일",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "머신 코드",
"purchase_renew": "구매 / 갱신",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "로그인 방식 선택",
"auth_subtitle": "이메일과 활성화 코드를 입력하세요",
"email_label": "이메일 주소",
"activation_code_label": "활성화 코드",
"email_placeholder": "이메일 입력",
"code_placeholder": "코드 입력 (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "활성화",
"deactivate_button": "비활성화",
"activating": "활성화 중...",
"deactivating": "비활성화 중...",
"license_status": "라이선스 상태",
"license_type": "라이선스 유형",
"expiry_date": "만료일",
"days_remaining": "남은 일수",
"days_unit": "일",
"permanent": "영구",
"expiry_warning_title": "라이선스 만료 임박",
"expiry_warning_message": "라이선스가 {0}일 후 만료됩니다. 서비스 중단을 방지하려면 갱신해 주세요.",
"renew_now": "지금 갱신",
"i_understand": "확인했습니다",
"activation_success": "라이선스 활성화에 성공했습니다!",
"activation_failed": "활성화 실패: {0}",
"invalid_code": "잘못된 활성화 코드 형식",
"purchase_license": "라이선스 구매",
"renew_license": "라이선스 갱신",
"contact_support": "지원",
"language_switch": "한국어",
"version_label": "버전",
"license_required": "서버를 시작하려면 라이선스가 필요합니다",
"license_check_failed": "라이선스 확인 실패",
"status_stopped": "중지됨",
"status_running": "실행 중",
"status_error": "연결 실패",
"config_tab": "빠른 설정",
"status_label": "상태",
"connections_count": "연결 수",
"transport_protocol": "전송 프로토콜",
"transport_type": "전송 유형",
"mcp_url": "MCP URL",
"port_fallback_title": "포트 {0}을(를) 사용할 수 없어 {1}(으)로 전환했습니다",
"port_fallback_reason": "포트 {0}이(가) 시스템에 예약되어 있거나 방화벽/백신에 의해 차단되었습니다(Hyper-V, WSL2 또는 Docker가 설치된 Windows에서 흔합니다). 서버는 새 포트에서 정상 실행 중입니다.",
"port_fallback_resynced": "다음 클라이언트의 설정을 업데이트했습니다: {0}. 새 포트를 적용하려면 다시 시작하세요.",
"port_fallback_no_clients": "AI 클라이언트를 설정한 적이 있다면 \"원클릭 설정\"에서 새 포트로 다시 설정하세요.",
"port_fallback_dismiss": "확인",
"port_label": "포트",
"auto_start_label": "자동 시작",
"debug_log_label": "디버그 로그",
"max_connections_label": "최대 연결 수",
"server_settings_title": "서버 설정",
"server_status_title": "서버 상태",
"connection_info_title": "연결 정보",
"tools_count": "{0}개 도구",
"tools_stats": "{0}개 도구 ({1} 활성화 / {2} 비활성화)",
"quick_config_title": "MCP 클라이언트 빠른 설정",
"quick_config_desc": "현재 MCP 서버를 주요 AI 에디터 및 CLI 도구에 자동 구성합니다",
"server_info": "서버 정보",
"server_name_label": "서버 이름",
"server_address": "서버 주소",
"timeout_ms": "타임아웃 (ms)",
"auto_config_title": "자동 구성 (IDE 에디터 + Codex CLI)",
"add_all": "전체 추가",
"remove_all": "전체 삭제",
"auto_config_note": "버튼을 클릭하여 구성 파일을 자동 수정합니다 (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "구성됨",
"not_configured": "미구성",
"auto_config_btn": "자동 구성",
"delete_btn": "삭제",
"copy_config": "구성 복사",
"open_config": "구성 열기",
"manual_config_title": "수동 구성 (Claude CLI + Gemini CLI)",
"refresh_commands": "명령 새로고침",
"manual_config_note": "다음 CLI 도구는 터미널에서 수동 실행이 필요합니다",
"config_scope": "구성 범위:",
"scope_user": "사용자 수준",
"scope_project": "프로젝트 수준",
"operation_log": "작업 로그",
"no_logs": "작업 기록 없음",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Streamable HTTP 프로토콜 (MCP 2025-03-26) 기반",
"tools_copyright_line1": "도구 시스템 - 13개 인텐트 레벨 도구, 150+ 에디터 작업, 액션 코드 디스패치",
"config_copyright_line1": "빠른 설정 - Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI 지원",
"email_required": "이메일과 활성화 코드를 입력하세요",
"permanent_label": "영구",
"logout_button": "로그아웃",
"invite_code_label": "초대 코드",
"copy_button": "복사",
"change_code_button": "코드 변경",
"change_code_title": "활성화 코드 변경",
"change_code_desc": "새 활성화 코드를 입력하세요. 현재 기기 바인딩이 먼저 해제됩니다.",
"confirm_change": "확인",
"cancel": "취소",
"code_required": "활성화 코드를 입력하세요",
"expiry_inline_warning": "곧 만료됩니다!",
"store_purchase_hint": "Cocos Store에서 본 플러그인을 구매하셨다면, 활성화 코드를 받으시려면 연락해 주세요.",
"license_expired_title": "라이선스 만료",
"license_expired_message": "라이선스가 만료되어 서버를 시작할 수 없습니다. 계속 사용하려면 갱신하거나 다시 활성화하세요.",
"go_to_login": "로그인으로 이동",
"log_load_config_failed": "구성 상태 로드 실패: {0}",
"log_gen_cli_failed": "CLI 명령 생성 실패: {0}",
"log_add_client_failed": "{0}에 추가 실패: {1}",
"log_remove_client_failed": "{0}에서 삭제 실패: {1}",
"quick_config_dialog_title": "원클릭 구성",
"config_operation_failed": "구성 작업 실패",
"config_file_not_found_changed_path": "구성 실패: 구성 파일을 찾을 수 없습니다. {0}이 사용자 지정 경로에 설치되었을 수 있습니다.",
"log_batch_add_failed": "일괄 추가 실패: {0}",
"log_batch_remove_failed": "일괄 삭제 실패: {0}",
"log_copied_config": "{0} 구성을 클립보드에 복사했습니다",
"log_gen_config_failed": "구성 생성 실패",
"log_copy_config_failed": "구성 복사 실패: {0}",
"log_copied_command": "{0} 명령을 클립보드에 복사했습니다",
"log_copy_command_failed": "명령 복사 실패: {0}",
"log_opened_config_file": "구성 파일을 열었습니다: {0}",
"log_open_config_failed": "구성 파일을 열 수 없습니다",
"log_open_config_error": "구성 파일을 열 수 없습니다: {0}",
"update_available_title": "업데이트 가능",
"update_new_version": "새 버전 v{0}이 출시되었습니다!",
"update_now": "지금 업데이트",
"update_not_now": "나중에",
"update_skip_version": "v{0} 건너뛰기",
"check_for_updates": "업데이트 확인",
"update_checking": "확인 중...",
"update_no_update": "최신 버전입니다",
"update_urgency_critical": "긴급 업데이트",
"update_urgency_high": "중요 업데이트",
"update_current_version": "현재 버전: v{0}",
"update_changelog": "변경 사항",
"auth_choose_subtitle": "계속하려면 로그인 방법을 선택하세요",
"update_changelog_empty": "이 버전에 대한 릴리스 노트가 없습니다.",
"subscription_login_title": "VberAI 구독 계정",
"subscription_password_label": "비밀번호",
"subscription_password_placeholder": "비밀번호 입력",
"subscription_login_button": "VberAI 계정으로 로그인",
"subscription_google_button": "Google로 로그인",
"subscription_google_pending": "브라우저가 열렸습니다. Google 인증을 완료한 후 Cocos Creator로 돌아오세요.",
"subscription_google_pending_button": "Google 인증을 기다리는 중...",
"subscription_register_button": "VberAI 계정 만들기",
"subscription_logging_in": "로그인 중...",
"subscription_credentials_required": "이메일과 비밀번호를 입력하세요",
"subscription_login_success": "구독 계정에 로그인했습니다",
"subscription_login_failed": "구독 로그인에 실패했습니다",
"subscription_not_entitled": "현재 구독에는 Cocos 3.x MCP가 포함되어 있지 않습니다. 계속하려면 업그레이드하세요.",
"subscription_upgrade_to": "이 제품을 사용하려면 「{0}」(으)로 업그레이드하세요",
"subscription_upgrade_action": "구독 업그레이드",
"subscription_logged_in": "로그인된 계정",
"subscription_choose_button": "VberAI 구독 계정",
"license_choose_button": "활성화 코드",
"subscription_method_label": "구독 계정",
"back_to_login_choices": "← 다른 로그인 방법 선택",
"subscription_signed_out_other_device": "로그아웃됨: 계정이 다른 기기에서 사용되었습니다.",
"subscription_google_cancelled": "Google 로그인이 취소되었습니다.",
"subscription_request_timeout": "요청 시간이 초과되었습니다. 네트워크를 확인하고 다시 시도하세요.",
"subscription_network_error": "네트워크 연결에 실패했습니다. 네트워크를 확인하고 다시 시도하세요.",
"subscription_server_error": "서버가 잘못된 응답을 반환했습니다. 나중에 다시 시도하세요.",
"subscription_google_authorization_failed": "Google 인증이 완료되지 않았습니다. 다시 시도하세요."
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP Servidor",
"description": "Servidor AI MCP para Cocos Creator 3.8",
"panel_title": "Servidor MCP",
"open_panel": "Abrir painel MCP",
"start_server": "Iniciar servidor",
"stop_server": "Parar servidor",
"server_status": "Estado do servidor",
"port": "Porta",
"settings": "Configurações",
"connected": "Conectado",
"disconnected": "Desconectado",
"server_running": "O servidor está em execução na porta {0}",
"server_stopped": "O servidor foi parado",
"auto_start": "Início automático",
"debug_log": "Log de depuração",
"max_connections": "Conexões máx.",
"connection_info": "Info de conexão",
"http_url": "URL HTTP",
"copy": "Copiar",
"save_settings": "Salvar",
"settings_saved": "Configurações salvas com sucesso",
"server_started": "Servidor MCP iniciado",
"server_stopped_msg": "Servidor MCP parado",
"failed_to_start": "Falha ao iniciar o servidor",
"failed_to_stop": "Falha ao parar o servidor",
"failed_to_save": "Falha ao salvar configurações",
"url_copied": "URL HTTP copiada para a área de transferência",
"tool_manager": "Gerenciador de ferramentas",
"open_tool_manager": "Abrir gerenciador de ferramentas",
"create_config": "Criar configuração",
"edit_config": "Editar configuração",
"delete_config": "Excluir configuração",
"import_config": "Importar configuração",
"export_config": "Exportar configuração",
"apply_config": "Aplicar configuração",
"select_all": "Selecionar tudo",
"deselect_all": "Desmarcar tudo",
"save_changes": "Salvar alterações",
"config_name": "Nome da configuração",
"config_description": "Descrição da configuração",
"current_config": "Configuração atual",
"tool_management": "Gestão de ferramentas",
"total_tools": "Total de ferramentas",
"enabled_tools": "Ativadas",
"disabled_tools": "Desativadas",
"no_config_selected": "Nenhuma configuração selecionada",
"select_config_first": "Por favor, selecione ou crie uma configuração primeiro",
"config_created": "Configuração criada com sucesso",
"config_updated": "Configuração atualizada com sucesso",
"config_deleted": "Configuração excluída com sucesso",
"config_applied": "Configuração aplicada com sucesso",
"config_exported": "Configuração exportada com sucesso",
"config_imported": "Configuração importada com sucesso",
"confirm_delete": "Confirmar exclusão",
"delete_config_confirm": "Tem certeza de que deseja excluir a configuração \"{0}\"? Esta ação não pode ser desfeita.",
"max_config_slots_reached": "Número máximo de configurações atingido ({0})",
"invalid_config_format": "Formato de configuração inválido",
"invalid_json_format": "Formato JSON ou estrutura de configuração inválidos",
"server_tab": "Servidor",
"tools_tab": "Gestão de ferramentas",
"available_tools": "Ferramentas disponíveis",
"scene_tools": "Ferramentas de cena",
"node_tools": "Ferramentas de nó",
"component_tools": "Ferramentas de componente",
"prefab_tools": "Ferramentas de prefab",
"project_tools": "Ferramentas de projeto",
"debug_tools": "Ferramentas de depuração",
"preferences_tools": "Preferências",
"server_tools": "Ferramentas de servidor",
"broadcast_tools": "Ferramentas de broadcast",
"scene_advanced_tools": "Ferramentas de cena avançadas",
"scene_view_tools": "Ferramentas de visualização de cena",
"reference_image_tools": "Ferramentas de imagem de referência",
"asset_advanced_tools": "Ferramentas de assets avançadas",
"validation_tools": "Ferramentas de validação",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "E-mail",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "Código da máquina",
"purchase_renew": "Comprar / Renovar",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "Escolher método de login",
"auth_subtitle": "Digite seu e-mail e código de ativação",
"email_label": "Endereço de e-mail",
"activation_code_label": "Código de ativação",
"email_placeholder": "Digite o e-mail",
"code_placeholder": "Digite o código (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "Ativar",
"deactivate_button": "Desativar",
"activating": "Ativando...",
"deactivating": "Desativando...",
"license_status": "Estado da licença",
"license_type": "Tipo de licença",
"expiry_date": "Data de expiração",
"days_remaining": "Dias restantes",
"days_unit": "d",
"permanent": "Permanente",
"expiry_warning_title": "Licença expirando em breve",
"expiry_warning_message": "Sua licença expirará em {0} dias. Por favor, renove para evitar interrupções.",
"renew_now": "Renovar agora",
"i_understand": "Entendido",
"activation_success": "Licença ativada com sucesso!",
"activation_failed": "Falha na ativação: {0}",
"invalid_code": "Formato de código de ativação inválido",
"purchase_license": "Comprar licença",
"renew_license": "Renovar licença",
"contact_support": "Suporte",
"language_switch": "Português",
"version_label": "Versão",
"license_required": "É necessária uma licença para iniciar o servidor",
"license_check_failed": "Falha na verificação de licença",
"status_stopped": "Parado",
"status_running": "Em execução",
"status_error": "Falha na conexão",
"config_tab": "Config rápida",
"status_label": "Estado",
"connections_count": "Conexões",
"transport_protocol": "Protocolo de transporte",
"transport_type": "Tipo de transporte",
"mcp_url": "URL MCP",
"port_fallback_title": "Porta {0} indisponível — alterada para {1}",
"port_fallback_reason": "A porta {0} está reservada pelo sistema ou bloqueada por firewall/antivírus (comum no Windows com Hyper-V, WSL2 ou Docker). O servidor está funcionando normalmente na nova porta.",
"port_fallback_resynced": "Configuração atualizada para: {0}. Reinicie esses clientes para que a nova porta tenha efeito.",
"port_fallback_no_clients": "Se você já configurou clientes de IA, configure-os novamente para a nova porta pela configuração com um clique.",
"port_fallback_dismiss": "Entendi",
"port_label": "Porta",
"auto_start_label": "Início auto",
"debug_log_label": "Log de depuração",
"max_connections_label": "Conexões máx.",
"server_settings_title": "Configurações do servidor",
"server_status_title": "Estado do servidor",
"connection_info_title": "Info de conexão",
"tools_count": "{0} ferramentas",
"tools_stats": "{0} ferramentas ({1} ativadas / {2} desativadas)",
"quick_config_title": "Config rápida de clientes MCP",
"quick_config_desc": "Configurar automaticamente o servidor MCP em editores AI e ferramentas CLI",
"server_info": "Info do servidor",
"server_name_label": "Nome do servidor",
"server_address": "Endereço do servidor",
"timeout_ms": "Tempo limite (ms)",
"auto_config_title": "Config auto (IDE + Codex CLI)",
"add_all": "Adicionar tudo",
"remove_all": "Remover tudo",
"auto_config_note": "Clique para modificar automaticamente os arquivos de config (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "Configurado",
"not_configured": "Não configurado",
"auto_config_btn": "Config auto",
"delete_btn": "Excluir",
"copy_config": "Copiar config",
"open_config": "Abrir config",
"manual_config_title": "Config manual (Claude CLI + Gemini CLI)",
"refresh_commands": "Atualizar comandos",
"manual_config_note": "As seguintes ferramentas CLI requerem execução manual no terminal",
"config_scope": "Escopo de config:",
"scope_user": "Nível de usuário",
"scope_project": "Nível de projeto",
"operation_log": "Log de operações",
"no_logs": "Sem registros",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Baseado no protocolo Streamable HTTP (MCP 2025-03-26)",
"tools_copyright_line1": "Sistema de ferramentas - 13 ferramentas intent-level, 150+ operações de editor, despacho de código de ação",
"config_copyright_line1": "Config rápida - Suporta Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "Por favor, digite e-mail e código de ativação",
"permanent_label": "Permanente",
"logout_button": "Sair",
"invite_code_label": "Código de convite",
"copy_button": "Copiar",
"change_code_button": "Alterar código",
"change_code_title": "Alterar código de ativação",
"change_code_desc": "Digite seu novo código de ativação. A vinculação do dispositivo atual será liberada primeiro.",
"confirm_change": "Confirmar",
"cancel": "Cancelar",
"code_required": "Por favor, digite um código de ativação",
"expiry_inline_warning": "Expirando em breve!",
"store_purchase_hint": "Se você comprou este plugin na Cocos Store, entre em contato comigo para obter seu código de ativação.",
"license_expired_title": "Licença expirada",
"license_expired_message": "Sua licença expirou e o servidor não pode ser iniciado. Renove ou reative para continuar.",
"go_to_login": "Ir para login",
"log_load_config_failed": "Falha ao carregar estado da config: {0}",
"log_gen_cli_failed": "Falha ao gerar comandos CLI: {0}",
"log_add_client_failed": "Falha ao adicionar a {0}: {1}",
"log_remove_client_failed": "Falha ao remover de {0}: {1}",
"quick_config_dialog_title": "Configuração com um clique",
"config_operation_failed": "Falha na operação de configuração",
"config_file_not_found_changed_path": "Falha na configuração: arquivo de configuração não encontrado. {0} pode ter sido instalado em um caminho personalizado.",
"log_batch_add_failed": "Falha na adição em lote: {0}",
"log_batch_remove_failed": "Falha na remoção em lote: {0}",
"log_copied_config": "Config de {0} copiada para a área de transferência",
"log_gen_config_failed": "Falha ao gerar config",
"log_copy_config_failed": "Falha ao copiar config: {0}",
"log_copied_command": "Comando {0} copiado para a área de transferência",
"log_copy_command_failed": "Falha ao copiar comando: {0}",
"log_opened_config_file": "Arquivo de config aberto: {0}",
"log_open_config_failed": "Falha ao abrir arquivo de config",
"log_open_config_error": "Falha ao abrir arquivo de config: {0}",
"update_available_title": "Atualização disponível",
"update_new_version": "A nova versão v{0} está disponível!",
"update_now": "Atualizar agora",
"update_not_now": "Agora não",
"update_skip_version": "Pular v{0}",
"check_for_updates": "Verificar atualizações",
"update_checking": "Verificando...",
"update_no_update": "Você está usando a versão mais recente",
"update_urgency_critical": "Atualização crítica",
"update_urgency_high": "Atualização importante",
"update_current_version": "Versão atual: v{0}",
"update_changelog": "Registro de alterações",
"auth_choose_subtitle": "Escolha um método de login para continuar",
"update_changelog_empty": "Nenhuma nota de versão foi fornecida para esta versão.",
"subscription_login_title": "Conta de assinatura VberAI",
"subscription_password_label": "Senha",
"subscription_password_placeholder": "Digite a senha",
"subscription_login_button": "Entrar com VberAI",
"subscription_google_button": "Entrar com o Google",
"subscription_google_pending": "Navegador aberto. Conclua a autorização do Google e volte ao Cocos Creator.",
"subscription_google_pending_button": "Aguardando a autorização do Google...",
"subscription_register_button": "Criar uma conta VberAI",
"subscription_logging_in": "Entrando...",
"subscription_credentials_required": "Digite o e-mail e a senha",
"subscription_login_success": "Conectado à conta de assinatura",
"subscription_login_failed": "Falha ao entrar na assinatura",
"subscription_not_entitled": "Sua assinatura atual não inclui o Cocos 3.x MCP. Faça upgrade para continuar.",
"subscription_upgrade_to": "Faça upgrade para \"{0}\" para usar este produto",
"subscription_upgrade_action": "Fazer upgrade da assinatura",
"subscription_logged_in": "Conta conectada",
"subscription_choose_button": "Conta de assinatura VberAI",
"license_choose_button": "Código de ativação",
"subscription_method_label": "Assinatura",
"back_to_login_choices": "← Escolher outro método de login",
"subscription_signed_out_other_device": "Desconectado: sua conta foi usada em outro dispositivo.",
"subscription_google_cancelled": "O login com o Google foi cancelado.",
"subscription_request_timeout": "A solicitação expirou. Verifique sua rede e tente novamente.",
"subscription_network_error": "Falha na conexão de rede. Verifique sua rede e tente novamente.",
"subscription_server_error": "O servidor retornou uma resposta inválida. Tente novamente mais tarde.",
"subscription_google_authorization_failed": "A autorização do Google não foi concluída. Tente novamente."
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP Сервер",
"description": "AI MCP сервер для Cocos Creator 3.8",
"panel_title": "MCP Сервер",
"open_panel": "Открыть панель MCP",
"start_server": "Запустить сервер",
"stop_server": "Остановить сервер",
"server_status": "Состояние сервера",
"port": "Порт",
"settings": "Настройки",
"connected": "Подключено",
"disconnected": "Отключено",
"server_running": "Сервер работает на порту {0}",
"server_stopped": "Сервер остановлен",
"auto_start": "Автозапуск",
"debug_log": "Журнал отладки",
"max_connections": "Макс. подключений",
"connection_info": "Информация о подключении",
"http_url": "HTTP URL",
"copy": "Копировать",
"save_settings": "Сохранить",
"settings_saved": "Настройки успешно сохранены",
"server_started": "MCP сервер запущен",
"server_stopped_msg": "MCP сервер остановлен",
"failed_to_start": "Не удалось запустить сервер",
"failed_to_stop": "Не удалось остановить сервер",
"failed_to_save": "Не удалось сохранить настройки",
"url_copied": "HTTP URL скопирован в буфер обмена",
"tool_manager": "Менеджер инструментов",
"open_tool_manager": "Открыть менеджер инструментов",
"create_config": "Создать конфигурацию",
"edit_config": "Редактировать конфигурацию",
"delete_config": "Удалить конфигурацию",
"import_config": "Импортировать конфигурацию",
"export_config": "Экспортировать конфигурацию",
"apply_config": "Применить конфигурацию",
"select_all": "Выбрать все",
"deselect_all": "Снять все",
"save_changes": "Сохранить изменения",
"config_name": "Имя конфигурации",
"config_description": "Описание конфигурации",
"current_config": "Текущая конфигурация",
"tool_management": "Управление инструментами",
"total_tools": "Всего инструментов",
"enabled_tools": "Включено",
"disabled_tools": "Отключено",
"no_config_selected": "Конфигурация не выбрана",
"select_config_first": "Пожалуйста, сначала выберите или создайте конфигурацию",
"config_created": "Конфигурация успешно создана",
"config_updated": "Конфигурация успешно обновлена",
"config_deleted": "Конфигурация успешно удалена",
"config_applied": "Конфигурация успешно применена",
"config_exported": "Конфигурация успешно экспортирована",
"config_imported": "Конфигурация успешно импортирована",
"confirm_delete": "Подтвердить удаление",
"delete_config_confirm": "Вы уверены, что хотите удалить конфигурацию \"{0}\"? Это действие нельзя отменить.",
"max_config_slots_reached": "Достигнуто максимальное количество конфигураций ({0})",
"invalid_config_format": "Недопустимый формат конфигурации",
"invalid_json_format": "Недопустимый формат JSON или структура конфигурации",
"server_tab": "Сервер",
"tools_tab": "Управление инструментами",
"available_tools": "Доступные инструменты",
"scene_tools": "Инструменты сцены",
"node_tools": "Инструменты узлов",
"component_tools": "Инструменты компонентов",
"prefab_tools": "Инструменты префабов",
"project_tools": "Инструменты проекта",
"debug_tools": "Инструменты отладки",
"preferences_tools": "Настройки",
"server_tools": "Серверные инструменты",
"broadcast_tools": "Инструменты трансляции",
"scene_advanced_tools": "Расширенные инструменты сцены",
"scene_view_tools": "Инструменты просмотра сцены",
"reference_image_tools": "Инструменты эталонных изображений",
"asset_advanced_tools": "Расширенные инструменты ресурсов",
"validation_tools": "Инструменты валидации",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "Эл. почта",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "Код машины",
"purchase_renew": "Купить / Продлить",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "Выберите способ входа",
"auth_subtitle": "Введите email и код активации",
"email_label": "Адрес электронной почты",
"activation_code_label": "Код активации",
"email_placeholder": "Введите email",
"code_placeholder": "Введите код (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "Активировать",
"deactivate_button": "Деактивировать",
"activating": "Активация...",
"deactivating": "Деактивация...",
"license_status": "Состояние лицензии",
"license_type": "Тип лицензии",
"expiry_date": "Дата истечения",
"days_remaining": "Осталось дней",
"days_unit": "д",
"permanent": "Бессрочная",
"expiry_warning_title": "Срок лицензии скоро истекает",
"expiry_warning_message": "Ваша лицензия истекает через {0} дней. Пожалуйста, продлите, чтобы избежать прерывания.",
"renew_now": "Продлить сейчас",
"i_understand": "Понятно",
"activation_success": "Лицензия успешно активирована!",
"activation_failed": "Ошибка активации: {0}",
"invalid_code": "Неверный формат кода активации",
"purchase_license": "Купить лицензию",
"renew_license": "Продлить лицензию",
"contact_support": "Поддержка",
"language_switch": "Русский",
"version_label": "Версия",
"license_required": "Для запуска сервера требуется лицензия",
"license_check_failed": "Ошибка проверки лицензии",
"status_stopped": "Остановлен",
"status_running": "Работает",
"status_error": "Ошибка подключения",
"config_tab": "Быстрая настройка",
"status_label": "Состояние",
"connections_count": "Подключения",
"transport_protocol": "Транспортный протокол",
"transport_type": "Тип транспорта",
"mcp_url": "MCP URL",
"port_fallback_title": "Порт {0} недоступен — выполнен переход на {1}",
"port_fallback_reason": "Порт {0} зарезервирован системой или заблокирован брандмауэром/антивирусом (часто встречается в Windows с Hyper-V, WSL2 или Docker). Сервер нормально работает на новом порту.",
"port_fallback_resynced": "Обновлена конфигурация для: {0}. Перезапустите эти клиенты, чтобы новый порт вступил в силу.",
"port_fallback_no_clients": "Если вы уже настраивали ИИ-клиенты, перенастройте их на новый порт через конфигурацию в один клик.",
"port_fallback_dismiss": "Понятно",
"port_label": "Порт",
"auto_start_label": "Автозапуск",
"debug_log_label": "Журнал отладки",
"max_connections_label": "Макс. подключений",
"server_settings_title": "Настройки сервера",
"server_status_title": "Состояние сервера",
"connection_info_title": "Информация о подключении",
"tools_count": "{0} инструментов",
"tools_stats": "{0} инструментов ({1} включено / {2} отключено)",
"quick_config_title": "Быстрая настройка клиентов MCP",
"quick_config_desc": "Автоматическая настройка MCP сервера в AI-редакторах и CLI-инструментах",
"server_info": "Информация о сервере",
"server_name_label": "Имя сервера",
"server_address": "Адрес сервера",
"timeout_ms": "Тайм-аут (мс)",
"auto_config_title": "Автонастройка (IDE + Codex CLI)",
"add_all": "Добавить все",
"remove_all": "Удалить все",
"auto_config_note": "Нажмите для автоматического изменения файлов конфигурации (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "Настроено",
"not_configured": "Не настроено",
"auto_config_btn": "Автонастройка",
"delete_btn": "Удалить",
"copy_config": "Копировать конфиг",
"open_config": "Открыть конфиг",
"manual_config_title": "Ручная настройка (Claude CLI + Gemini CLI)",
"refresh_commands": "Обновить команды",
"manual_config_note": "Следующие CLI-инструменты требуют ручного выполнения в терминале",
"config_scope": "Область конфигурации:",
"scope_user": "Уровень пользователя",
"scope_project": "Уровень проекта",
"operation_log": "Журнал операций",
"no_logs": "Нет записей",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "На основе протокола Streamable HTTP (MCP 2025-03-26)",
"tools_copyright_line1": "Система инструментов - 13 intent-level инструментов, 150+ операций редактора, диспетчеризация action-кодов",
"config_copyright_line1": "Быстрая настройка - Поддержка Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "Пожалуйста, введите email и код активации",
"permanent_label": "Бессрочная",
"logout_button": "Выйти",
"invite_code_label": "Код приглашения",
"copy_button": "Копировать",
"change_code_button": "Сменить код",
"change_code_title": "Смена кода активации",
"change_code_desc": "Введите новый код активации. Привязка текущего устройства будет сначала снята.",
"confirm_change": "Подтвердить",
"cancel": "Отмена",
"code_required": "Пожалуйста, введите код активации",
"expiry_inline_warning": "Скоро истекает!",
"store_purchase_hint": "Если вы приобрели этот плагин в Cocos Store, свяжитесь со мной для получения кода активации.",
"license_expired_title": "Лицензия истекла",
"license_expired_message": "Ваша лицензия истекла, сервер не может быть запущен. Пожалуйста, продлите или повторно активируйте для продолжения.",
"go_to_login": "Перейти к входу",
"log_load_config_failed": "Не удалось загрузить состояние конфигурации: {0}",
"log_gen_cli_failed": "Не удалось сгенерировать команды CLI: {0}",
"log_add_client_failed": "Не удалось добавить в {0}: {1}",
"log_remove_client_failed": "Не удалось удалить из {0}: {1}",
"quick_config_dialog_title": "Настройка в один клик",
"config_operation_failed": "Операция настройки не удалась",
"config_file_not_found_changed_path": "Ошибка настройки: файл конфигурации не найден. Возможно, {0} установлен в пользовательский путь.",
"log_batch_add_failed": "Пакетное добавление не удалось: {0}",
"log_batch_remove_failed": "Пакетное удаление не удалось: {0}",
"log_copied_config": "Конфигурация {0} скопирована в буфер обмена",
"log_gen_config_failed": "Не удалось сгенерировать конфигурацию",
"log_copy_config_failed": "Не удалось скопировать конфигурацию: {0}",
"log_copied_command": "Команда {0} скопирована в буфер обмена",
"log_copy_command_failed": "Не удалось скопировать команду: {0}",
"log_opened_config_file": "Файл конфигурации открыт: {0}",
"log_open_config_failed": "Не удалось открыть файл конфигурации",
"log_open_config_error": "Не удалось открыть файл конфигурации: {0}",
"update_available_title": "Доступно обновление",
"update_new_version": "Новая версия v{0} доступна!",
"update_now": "Обновить сейчас",
"update_not_now": "Позже",
"update_skip_version": "Пропустить v{0}",
"check_for_updates": "Проверить обновления",
"update_checking": "Проверка...",
"update_no_update": "У вас последняя версия",
"update_urgency_critical": "Критическое обновление",
"update_urgency_high": "Важное обновление",
"update_current_version": "Текущая версия: v{0}",
"update_changelog": "Список изменений",
"auth_choose_subtitle": "Выберите способ входа, чтобы продолжить",
"update_changelog_empty": "Для этой версии не указаны примечания к выпуску.",
"subscription_login_title": "Аккаунт подписки VberAI",
"subscription_password_label": "Пароль",
"subscription_password_placeholder": "Введите пароль",
"subscription_login_button": "Войти через VberAI",
"subscription_google_button": "Войти через Google",
"subscription_google_pending": "Браузер открыт. Завершите авторизацию Google, затем вернитесь в Cocos Creator.",
"subscription_google_pending_button": "Ожидание авторизации Google...",
"subscription_register_button": "Создать аккаунт VberAI",
"subscription_logging_in": "Вход...",
"subscription_credentials_required": "Введите адрес электронной почты и пароль",
"subscription_login_success": "Вход в аккаунт подписки выполнен",
"subscription_login_failed": "Не удалось войти в аккаунт подписки",
"subscription_not_entitled": "Ваша текущая подписка не включает Cocos 3.x MCP. Оформите повышение, чтобы продолжить.",
"subscription_upgrade_to": "Перейдите на «{0}», чтобы использовать этот продукт",
"subscription_upgrade_action": "Повысить подписку",
"subscription_logged_in": "Вошедший аккаунт",
"subscription_choose_button": "Аккаунт подписки VberAI",
"license_choose_button": "Код активации",
"subscription_method_label": "Подписка",
"back_to_login_choices": "← Выбрать другой способ входа",
"subscription_signed_out_other_device": "Выполнен выход: ваш аккаунт использовался на другом устройстве.",
"subscription_google_cancelled": "Вход через Google отменён.",
"subscription_request_timeout": "Время ожидания запроса истекло. Проверьте сеть и повторите попытку.",
"subscription_network_error": "Сбой сетевого подключения. Проверьте сеть и повторите попытку.",
"subscription_server_error": "Сервер вернул недопустимый ответ. Повторите попытку позже.",
"subscription_google_authorization_failed": "Авторизация Google не завершена. Повторите попытку."
};
@@ -0,0 +1,244 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP Server",
"description": "Máy chủ AI MCP cho Cocos Creator 3.8",
"panel_title": "Máy chủ MCP",
"open_panel": "Mở bảng điều khiển MCP",
"start_server": "Khởi động máy chủ",
"stop_server": "Dừng máy chủ",
"server_status": "Trạng thái máy chủ",
"port": "Cổng",
"settings": "Cài đặt",
"connected": "Đã kết nối",
"disconnected": "Đã ngắt kết nối",
"server_running": "Máy chủ đang chạy trên cổng {0}",
"server_stopped": "Máy chủ đã dừng",
"auto_start": "Tự động khởi động",
"debug_log": "Ghi nhật ký gỡ lỗi",
"max_connections": "Số kết nối tối đa",
"connection_info": "Thông tin kết nối",
"http_url": "HTTP URL",
"copy": "Sao chép",
"save_settings": "Lưu cài đặt",
"settings_saved": "Đã lưu cài đặt thành công",
"server_started": "Máy chủ MCP đã khởi động",
"server_stopped_msg": "Máy chủ MCP đã dừng",
"failed_to_start": "Không thể khởi động máy chủ",
"failed_to_stop": "Không thể dừng máy chủ",
"failed_to_save": "Không thể lưu cài đặt",
"url_copied": "Đã sao chép HTTP URL vào bảng nhớ tạm",
"tool_manager": "Trình quản lý công cụ",
"open_tool_manager": "Mở trình quản lý công cụ",
"create_config": "Tạo cấu hình",
"edit_config": "Chỉnh sửa cấu hình",
"delete_config": "Xóa cấu hình",
"import_config": "Nhập cấu hình",
"export_config": "Xuất cấu hình",
"apply_config": "Áp dụng cấu hình",
"select_all": "Chọn tất cả",
"deselect_all": "Bỏ chọn tất cả",
"save_changes": "Lưu thay đổi",
"config_name": "Tên cấu hình",
"config_description": "Mô tả cấu hình",
"current_config": "Cấu hình hiện tại",
"tool_management": "Quản lý công cụ",
"total_tools": "Tổng số công cụ",
"enabled_tools": "Đã bật",
"disabled_tools": "Đã tắt",
"no_config_selected": "Chưa chọn cấu hình",
"select_config_first": "Vui lòng chọn một cấu hình hoặc tạo cấu hình mới trước",
"config_created": "Đã tạo cấu hình thành công",
"config_updated": "Đã cập nhật cấu hình thành công",
"config_deleted": "Đã xóa cấu hình thành công",
"config_applied": "Đã áp dụng cấu hình thành công",
"config_exported": "Đã xuất cấu hình thành công",
"config_imported": "Đã nhập cấu hình thành công",
"confirm_delete": "Xác nhận xóa",
"delete_config_confirm": "Bạn có chắc chắn muốn xóa cấu hình \"{0}\" không? Hành động này không thể hoàn tác.",
"max_config_slots_reached": "Đã đạt số lượng vị trí cấu hình tối đa ({0})",
"invalid_config_format": "Định dạng cấu hình không hợp lệ",
"invalid_json_format": "Định dạng JSON hoặc cấu trúc cấu hình không hợp lệ",
"server_tab": "Máy chủ",
"tools_tab": "Quản lý công cụ",
"available_tools": "Công cụ khả dụng",
"scene_tools": "Công cụ cảnh",
"node_tools": "Công cụ nút",
"component_tools": "Công cụ thành phần",
"prefab_tools": "Công cụ Prefab",
"project_tools": "Công cụ dự án",
"debug_tools": "Công cụ gỡ lỗi",
"preferences_tools": "Tùy chọn",
"server_tools": "Công cụ máy chủ",
"broadcast_tools": "Công cụ quảng bá",
"scene_advanced_tools": "Công cụ cảnh nâng cao",
"scene_view_tools": "Công cụ khung nhìn cảnh",
"reference_image_tools": "Công cụ ảnh tham chiếu",
"asset_advanced_tools": "Công cụ tài nguyên nâng cao",
"validation_tools": "Công cụ kiểm tra",
"brand_name": "LiDaxian MCP Series",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "Email",
"contact_wechat": "WeChat",
"contact_github": "GitHub",
"machine_id_label": "Mã máy",
"purchase_renew": "Mua / Gia hạn",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "Chọn phương thức đăng nhập",
"auth_subtitle": "Nhập email và mã kích hoạt của bạn",
"email_label": "Địa chỉ email",
"activation_code_label": "Mã kích hoạt",
"email_placeholder": "Nhập email",
"code_placeholder": "Nhập mã (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "Kích hoạt",
"deactivate_button": "Hủy kích hoạt",
"activating": "Đang kích hoạt...",
"deactivating": "Đang hủy kích hoạt...",
"license_status": "Trạng thái giấy phép",
"license_type": "Loại giấy phép",
"expiry_date": "Ngày hết hạn",
"days_remaining": "Số ngày còn lại",
"days_unit": "ngày",
"permanent": "Vĩnh viễn",
"expiry_warning_title": "Giấy phép sắp hết hạn",
"expiry_warning_message": "Giấy phép của bạn sẽ hết hạn sau {0} ngày. Vui lòng gia hạn để tránh gián đoạn dịch vụ.",
"renew_now": "Gia hạn ngay",
"i_understand": "Tôi đã hiểu",
"activation_success": "Đã kích hoạt giấy phép thành công!",
"activation_failed": "Kích hoạt thất bại: {0}",
"invalid_code": "Định dạng mã kích hoạt không hợp lệ",
"purchase_license": "Mua giấy phép",
"renew_license": "Gia hạn giấy phép",
"contact_support": "Hỗ trợ",
"language_switch": "Tiếng Việt",
"version_label": "Phiên bản",
"license_required": "Cần có giấy phép để khởi động máy chủ",
"license_check_failed": "Kiểm tra giấy phép thất bại",
"status_stopped": "Đã dừng",
"status_running": "Đang chạy",
"status_error": "Kết nối thất bại",
"config_tab": "Cấu hình nhanh",
"status_label": "Trạng thái",
"connections_count": "Số kết nối",
"transport_protocol": "Giao thức truyền tải",
"transport_type": "Loại truyền tải",
"mcp_url": "MCP URL",
"port_fallback_title": "Cổng {0} không khả dụng — đã chuyển sang {1}",
"port_fallback_reason": "Cổng {0} bị hệ thống dành riêng hoặc bị tường lửa/phần mềm diệt virus chặn (thường gặp trên Windows có cài Hyper-V, WSL2 hoặc Docker). Máy chủ đang chạy bình thường trên cổng mới.",
"port_fallback_resynced": "Đã cập nhật cấu hình cho: {0}. Hãy khởi động lại các client này để cổng mới có hiệu lực.",
"port_fallback_no_clients": "Nếu bạn đã cấu hình client AI, hãy cấu hình lại sang cổng mới bằng thiết lập một chạm.",
"port_fallback_dismiss": "Đã hiểu",
"port_label": "Cổng",
"auto_start_label": "Tự động khởi động",
"debug_log_label": "Nhật ký gỡ lỗi",
"max_connections_label": "Số kết nối tối đa",
"server_settings_title": "Cài đặt máy chủ",
"server_status_title": "Trạng thái máy chủ",
"connection_info_title": "Thông tin kết nối",
"tools_count": "{0} công cụ",
"tools_stats": "{0} công cụ ({1} đã bật / {2} đã tắt)",
"quick_config_title": "Cấu hình nhanh máy khách MCP",
"quick_config_desc": "Tự động cấu hình máy chủ MCP hiện tại vào các trình soạn thảo AI phổ biến và công cụ CLI",
"server_info": "Thông tin máy chủ",
"server_name_label": "Tên máy chủ",
"server_address": "Địa chỉ máy chủ",
"timeout_ms": "Thời gian chờ (ms)",
"auto_config_title": "Cấu hình tự động (Trình soạn thảo IDE + Codex CLI)",
"add_all": "Thêm tất cả",
"remove_all": "Xóa tất cả",
"auto_config_note": "Nhấp nút để tự động sửa đổi tệp cấu hình (Cursor, Windsurf, Trae, Codex CLI)",
"configured": "Đã cấu hình",
"not_configured": "Chưa cấu hình",
"auto_config_btn": "Cấu hình tự động",
"delete_btn": "Xóa",
"copy_config": "Sao chép cấu hình",
"open_config": "Mở cấu hình",
"manual_config_title": "Cấu hình thủ công (Claude CLI + Gemini CLI)",
"refresh_commands": "Làm mới lệnh",
"manual_config_note": "Các công cụ CLI sau đây cần thực thi lệnh thủ công trong terminal",
"config_scope": "Phạm vi cấu hình:",
"scope_user": "Cấp người dùng",
"scope_project": "Cấp dự án",
"operation_log": "Nhật ký thao tác",
"no_logs": "Không có bản ghi thao tác",
"copyright_line": "© 2025 LiDaxian - Cocos Creator MCP Server v{0}",
"protocol_line": "Dựa trên giao thức Streamable HTTP (MCP 2025-03-26)",
"tools_copyright_line1": "Hệ thống công cụ - 13 công cụ cấp ý định, 150+ thao tác trình soạn thảo, điều phối mã action",
"config_copyright_line1": "Cấu hình nhanh - Hỗ trợ Cursor, Windsurf, Trae, Codex CLI, Claude CLI, Gemini CLI",
"email_required": "Vui lòng nhập email và mã kích hoạt",
"permanent_label": "Vĩnh viễn",
"logout_button": "Đăng xuất",
"invite_code_label": "Mã mời",
"copy_button": "Sao chép",
"change_code_button": "Thay đổi mã",
"change_code_title": "Thay đổi mã kích hoạt",
"change_code_desc": "Nhập mã kích hoạt mới của bạn. Liên kết thiết bị hiện tại sẽ được giải phóng trước.",
"confirm_change": "Xác nhận",
"cancel": "Hủy",
"code_required": "Vui lòng nhập mã kích hoạt",
"expiry_inline_warning": "Sắp hết hạn!",
"store_purchase_hint": "Nếu bạn đã mua plugin này từ Cocos Store, vui lòng liên hệ với tôi để nhận mã kích hoạt.",
"license_expired_title": "Giấy phép đã hết hạn",
"license_expired_message": "Giấy phép của bạn đã hết hạn và không thể khởi động máy chủ. Vui lòng gia hạn hoặc kích hoạt lại để tiếp tục.",
"go_to_login": "Đi đến đăng nhập",
"log_load_config_failed": "Không thể tải trạng thái cấu hình: {0}",
"log_gen_cli_failed": "Không thể tạo lệnh CLI: {0}",
"log_add_client_failed": "Không thể thêm vào {0}: {1}",
"log_remove_client_failed": "Không thể xóa khỏi {0}: {1}",
"quick_config_dialog_title": "Cấu hình một chạm",
"config_operation_failed": "Thao tác cấu hình thất bại",
"config_file_not_found_changed_path": "Cấu hình thất bại: không tìm thấy tệp cấu hình. {0} có thể được cài đặt trong một đường dẫn tùy chỉnh.",
"log_batch_add_failed": "Thêm hàng loạt thất bại: {0}",
"log_batch_remove_failed": "Xóa hàng loạt thất bại: {0}",
"log_copied_config": "Đã sao chép cấu hình {0} vào bảng nhớ tạm",
"log_gen_config_failed": "Không thể tạo cấu hình",
"log_copy_config_failed": "Không thể sao chép cấu hình: {0}",
"log_copied_command": "Đã sao chép lệnh {0} vào bảng nhớ tạm",
"log_copy_command_failed": "Không thể sao chép lệnh: {0}",
"log_opened_config_file": "Đã mở tệp cấu hình: {0}",
"log_open_config_failed": "Không thể mở tệp cấu hình",
"log_open_config_error": "Không thể mở tệp cấu hình: {0}",
"update_available_title": "Có bản cập nhật",
"update_new_version": "Đã có phiên bản mới v{0}!",
"update_now": "Cập nhật ngay",
"update_not_now": "Để sau",
"update_skip_version": "Bỏ qua v{0}",
"check_for_updates": "Kiểm tra cập nhật",
"update_checking": "Đang kiểm tra...",
"update_no_update": "Bạn đang sử dụng phiên bản mới nhất",
"update_urgency_critical": "Cập nhật khẩn cấp",
"update_urgency_high": "Cập nhật quan trọng",
"update_current_version": "Phiên bản hiện tại: v{0}",
"update_changelog": "Nhật ký thay đổi",
"auth_choose_subtitle": "Chọn một phương thức đăng nhập để tiếp tục",
"update_changelog_empty": "Không có ghi chú phát hành cho phiên bản này.",
"subscription_login_title": "Tài khoản đăng ký VberAI",
"subscription_password_label": "Mật khẩu",
"subscription_password_placeholder": "Nhập mật khẩu",
"subscription_login_button": "Đăng nhập bằng VberAI",
"subscription_google_button": "Đăng nhập bằng Google",
"subscription_google_pending": "Đã mở trình duyệt. Hoàn tất ủy quyền Google rồi quay lại Cocos Creator.",
"subscription_google_pending_button": "Đang chờ ủy quyền Google...",
"subscription_register_button": "Tạo tài khoản VberAI",
"subscription_logging_in": "Đang đăng nhập...",
"subscription_credentials_required": "Vui lòng nhập email và mật khẩu",
"subscription_login_success": "Đã đăng nhập tài khoản đăng ký",
"subscription_login_failed": "Đăng nhập đăng ký thất bại",
"subscription_not_entitled": "Gói đăng ký hiện tại của bạn không bao gồm Cocos 3.x MCP. Nâng cấp để tiếp tục.",
"subscription_upgrade_to": "Nâng cấp lên \"{0}\" để sử dụng sản phẩm này",
"subscription_upgrade_action": "Nâng cấp gói đăng ký",
"subscription_logged_in": "Tài khoản đã đăng nhập",
"subscription_choose_button": "Tài khoản đăng ký VberAI",
"license_choose_button": "Mã kích hoạt",
"subscription_method_label": "Đăng ký",
"back_to_login_choices": "← Chọn phương thức đăng nhập khác",
"subscription_signed_out_other_device": "Đã đăng xuất: tài khoản của bạn đã được dùng trên thiết bị khác.",
"subscription_google_cancelled": "Đã hủy đăng nhập Google.",
"subscription_request_timeout": "Yêu cầu đã hết thời gian. Kiểm tra mạng và thử lại.",
"subscription_network_error": "Kết nối mạng thất bại. Kiểm tra mạng và thử lại.",
"subscription_server_error": "Máy chủ trả về phản hồi không hợp lệ. Thử lại sau.",
"subscription_google_authorization_failed": "Ủy quyền Google chưa hoàn tất. Vui lòng thử lại."
};
@@ -0,0 +1,243 @@
"use strict";
module.exports = {
"extension_name": "Cocos MCP 服务器",
"description": "适用于 Cocos Creator 3.8 的 AI MCP 服务器",
"panel_title": "MCP 服务器",
"open_panel": "打开 MCP 面板",
"start_server": "启动服务器",
"stop_server": "停止服务器",
"server_status": "服务器状态",
"port": "端口",
"settings": "设置",
"connected": "已连接",
"disconnected": "未连接",
"server_running": "服务器正在端口 {0} 上运行",
"server_stopped": "服务器已停止",
"auto_start": "自动启动",
"debug_log": "调试日志",
"max_connections": "最大连接数",
"connection_info": "连接信息",
"http_url": "HTTP 地址",
"copy": "复制",
"save_settings": "保存设置",
"settings_saved": "设置保存成功",
"server_started": "MCP 服务器已启动",
"server_stopped_msg": "MCP 服务器已停止",
"failed_to_start": "启动服务器失败",
"failed_to_stop": "停止服务器失败",
"failed_to_save": "保存设置失败",
"url_copied": "HTTP 地址已复制到剪贴板",
"tool_manager": "工具管理器",
"open_tool_manager": "打开工具管理器",
"create_config": "新建配置",
"edit_config": "编辑配置",
"delete_config": "删除配置",
"import_config": "导入配置",
"export_config": "导出配置",
"apply_config": "应用配置",
"select_all": "全选",
"deselect_all": "取消全选",
"save_changes": "保存更改",
"config_name": "配置名称",
"config_description": "配置描述",
"current_config": "当前配置",
"tool_management": "工具管理",
"total_tools": "总工具数",
"enabled_tools": "已启用",
"disabled_tools": "已禁用",
"no_config_selected": "没有选择配置",
"select_config_first": "请先选择一个配置或创建新配置",
"config_created": "配置创建成功",
"config_updated": "配置更新成功",
"config_deleted": "配置删除成功",
"config_applied": "配置应用成功",
"config_exported": "配置导出成功",
"config_imported": "配置导入成功",
"confirm_delete": "确认删除",
"delete_config_confirm": "确定要删除配置 \"{0}\" 吗?此操作不可撤销。",
"max_config_slots_reached": "已达到最大配置槽位数量 ({0})",
"invalid_config_format": "无效的配置格式",
"invalid_json_format": "无效的JSON格式或配置结构",
"server_tab": "服务器",
"tools_tab": "工具管理",
"available_tools": "可用工具",
"scene_tools": "场景工具",
"node_tools": "节点工具",
"component_tools": "组件工具",
"prefab_tools": "预制体工具",
"project_tools": "项目工具",
"debug_tools": "调试工具",
"preferences_tools": "偏好设置",
"server_tools": "服务器工具",
"broadcast_tools": "广播工具",
"scene_advanced_tools": "高级场景工具",
"scene_view_tools": "场景视图工具",
"reference_image_tools": "参考图片工具",
"asset_advanced_tools": "高级资源工具",
"validation_tools": "验证工具",
"brand_name": "李大仙 MCP 系列",
"brand_subtitle": "LiDaxian MCP Series",
"contact_email": "邮箱",
"contact_wechat": "微信",
"contact_github": "GitHub",
"machine_id_label": "机器码",
"purchase_renew": "购买/续费",
"purchase_url": "https://www.vberai.com/game-engines/cocos",
"auth_title": "选择登录方式",
"auth_subtitle": "请输入您的邮箱和激活码",
"auth_choose_subtitle": "请选择一种登录方式继续",
"email_label": "邮箱地址",
"activation_code_label": "激活码",
"email_placeholder": "请输入邮箱",
"code_placeholder": "请输入激活码 (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)",
"activate_button": "激活",
"deactivate_button": "取消激活",
"activating": "激活中...",
"deactivating": "取消激活中...",
"license_status": "授权状态",
"license_type": "授权类型",
"expiry_date": "到期日期",
"days_remaining": "剩余天数",
"days_unit": "天",
"permanent": "永久",
"expiry_warning_title": "授权即将到期",
"expiry_warning_message": "您的授权将在 {0} 天后到期,请及时续费以确保服务不中断。",
"renew_now": "立即续费",
"i_understand": "我知道了",
"activation_success": "授权激活成功!",
"activation_failed": "激活失败:{0}",
"invalid_code": "无效的激活码格式",
"purchase_license": "购买授权",
"renew_license": "续费授权",
"contact_support": "技术支持",
"language_switch": "中文",
"version_label": "版本",
"license_required": "需要授权才能启动服务器",
"license_check_failed": "授权检查失败",
"status_stopped": "已停止",
"status_running": "运行中",
"status_error": "连接失败",
"config_tab": "一键配置",
"status_label": "状态",
"connections_count": "连接数",
"transport_protocol": "传输协议",
"transport_type": "传输类型",
"mcp_url": "MCP URL",
"port_fallback_title": "端口 {0} 不可用,已自动切换到 {1}",
"port_fallback_reason": "端口 {0} 被系统保留或被防火墙/杀毒软件拦截(安装了 Hyper-V、WSL2 或 Docker 的 Windows 上很常见)。服务已在新端口上正常运行。",
"port_fallback_resynced": "已自动更新这些客户端的配置:{0}。请重启它们,新端口才会生效。",
"port_fallback_no_clients": "如果已配置过 AI 客户端,请在\"一键配置\"里重新配置到新端口。",
"port_fallback_dismiss": "知道了",
"port_label": "端口",
"auto_start_label": "自动启动",
"debug_log_label": "调试日志",
"max_connections_label": "最大连接数",
"server_settings_title": "服务器设置",
"server_status_title": "服务器状态",
"connection_info_title": "连接信息",
"tools_count": "{0} 个工具",
"tools_stats": "{0} 个工具 ({1} 启用 / {2} 禁用)",
"quick_config_title": "一键配置 MCP 客户端",
"quick_config_desc": "自动将当前MCP服务器配置到主流AI编辑器和CLI工具中",
"server_info": "服务器信息",
"server_name_label": "服务器名称",
"server_address": "服务器地址",
"timeout_ms": "超时时间(ms)",
"auto_config_title": "自动配置 (IDE编辑器 + Codex CLI)",
"add_all": "全部添加",
"remove_all": "全部删除",
"auto_config_note": "点击按钮将自动修改配置文件(包括Cursor、Windsurf、Trae、Codex CLI)",
"configured": "已配置",
"not_configured": "未配置",
"auto_config_btn": "自动配置",
"delete_btn": "删除",
"copy_config": "复制配置",
"open_config": "打开配置",
"manual_config_title": "手动配置 (Claude CLI + Gemini CLI)",
"refresh_commands": "刷新命令",
"manual_config_note": "以下CLI工具需要在终端中手动执行命令配置",
"config_scope": "配置范围:",
"scope_user": "用户级别",
"scope_project": "项目级别",
"operation_log": "操作日志",
"no_logs": "暂无操作记录",
"copyright_line": "© 2025 李大仙 (LiDaxian) - Cocos Creator MCP Server v{0}",
"protocol_line": "基于 Streamable HTTP 协议 (MCP 2025-03-26)",
"tools_copyright_line1": "工具系统 - 13个意图级工具,150+编辑器操作,action操作码分发",
"config_copyright_line1": "一键配置 - 支持 Cursor、Windsurf、Trae、Codex CLI、Claude CLI、Gemini CLI",
"email_required": "请输入邮箱和激活码",
"permanent_label": "永久",
"logout_button": "退出登录",
"invite_code_label": "邀请码",
"copy_button": "复制",
"change_code_button": "变更激活码",
"change_code_title": "变更激活码",
"change_code_desc": "输入新的激活码,当前设备的激活占位将被释放后重新激活。",
"confirm_change": "确认变更",
"cancel": "取消",
"code_required": "请输入激活码",
"expiry_inline_warning": "即将到期!",
"store_purchase_hint": "如果您已在 Cocos Store 购买了本插件,请联系我获取激活码。",
"license_expired_title": "授权已过期",
"license_expired_message": "您的授权已过期,无法启动服务。请续费或重新激活以继续使用。",
"go_to_login": "重新登录",
"log_load_config_failed": "加载配置状态失败: {0}",
"log_gen_cli_failed": "生成CLI命令失败: {0}",
"log_add_client_failed": "添加到{0}失败: {1}",
"log_remove_client_failed": "从{0}删除失败: {1}",
"quick_config_dialog_title": "一键配置",
"config_operation_failed": "配置操作失败",
"config_file_not_found_changed_path": "配置失败:找不到配置文件,{0} 安装时可能更改了路径",
"log_batch_add_failed": "批量添加失败: {0}",
"log_batch_remove_failed": "批量删除失败: {0}",
"log_copied_config": "已复制{0}配置到剪贴板",
"log_gen_config_failed": "生成配置失败",
"log_copy_config_failed": "复制配置失败: {0}",
"log_copied_command": "已复制{0}命令到剪贴板",
"log_copy_command_failed": "复制命令失败: {0}",
"log_opened_config_file": "已打开配置文件: {0}",
"log_open_config_failed": "打开配置文件失败",
"log_open_config_error": "打开配置文件失败: {0}",
"update_available_title": "发现新版本",
"update_new_version": "新版本 v{0} 已发布!",
"update_now": "立即更新",
"update_not_now": "暂不更新",
"update_skip_version": "跳过 v{0}",
"check_for_updates": "检查更新",
"update_checking": "检查中...",
"update_no_update": "当前已是最新版本",
"update_urgency_critical": "紧急更新",
"update_urgency_high": "重要更新",
"update_current_version": "当前版本: v{0}",
"update_changelog": "更新日志"
,"update_changelog_empty": "该版本暂未填写更新内容。"
,"subscription_login_title": "VberAI 官网订阅账号"
,"subscription_password_label": "密码"
,"subscription_password_placeholder": "请输入密码"
,"subscription_login_button": "使用 VberAI 账号登录"
,"subscription_google_button": "使用 Google 登录"
,"subscription_google_pending": "已打开浏览器,请完成 Google 授权后返回 Cocos Creator。"
,"subscription_google_pending_button": "正在等待 Google 授权..."
,"subscription_register_button": "注册 VberAI 账号"
,"subscription_logging_in": "登录中..."
,"subscription_credentials_required": "请输入邮箱和密码"
,"subscription_login_success": "订阅账号登录成功"
,"subscription_login_failed": "订阅账号登录失败"
,"subscription_not_entitled": "当前订阅计划不包含 Cocos 3.x MCP,请升级后使用。"
,"subscription_upgrade_to": "升级到「{0}」即可使用"
,"subscription_upgrade_action": "前往升级订阅"
,"subscription_logged_in": "当前登录账号"
,"subscription_choose_button": "VberAI 官网订阅账号"
,"license_choose_button": "激活码授权"
,"subscription_method_label": "订阅账号"
,"back_to_login_choices": "← 返回选择登录方式"
,"subscription_signed_out_other_device": "账号已在另一台设备登录,当前设备已退出。"
,"subscription_google_cancelled": "已取消 Google 登录"
,"subscription_request_timeout": "请求超时,请检查网络后重试。"
,"subscription_network_error": "网络连接失败,请检查网络后重试。"
,"subscription_server_error": "服务器响应异常,请稍后重试。"
,"subscription_google_authorization_failed": "Google 授权未完成,请重试。"
};
@@ -0,0 +1,259 @@
{
"$schema": "./@types/schema/package/index.json",
"package_version": 2,
"name": "cocos-mcp-server",
"version": "1.7.9",
"author": "LiDaxian",
"editor": ">=3.7.0",
"description": "i18n:cocos-mcp-server.description",
"main": "./dist/main.js",
"dependencies": {
"@iarna/toml": "^2.2.5",
"fs-extra": "^11.3.0",
"puppeteer-core": "^25.1.0",
"uuid": "^9.0.1",
"vue": "^3.1.4"
},
"panels": {
"default": {
"title": "i18n:cocos-mcp-server.panel_title",
"type": "dockable",
"main": "dist/panels/default",
"icon": "./static/icon.png",
"size": {
"min-width": 560,
"min-height": 680,
"width": 760,
"height": 820
}
}
},
"contributions": {
"menu": [
{
"path": "i18n:menu.extension/Cocos MCP Server",
"label": "i18n:cocos-mcp-server.open_panel",
"message": "open-panel"
}
],
"messages": {
"open-panel": {
"methods": [
"openPanel"
]
},
"open-tool-manager": {
"methods": [
"openToolManager"
]
},
"start-server": {
"methods": [
"startServer"
]
},
"stop-server": {
"methods": [
"stopServer"
]
},
"get-server-status": {
"methods": [
"getServerStatus"
]
},
"update-settings": {
"methods": [
"updateSettings"
]
},
"getToolsList": {
"methods": [
"getToolsList"
]
},
"get-server-settings": {
"methods": [
"getServerSettings"
]
},
"getToolManagerState": {
"methods": [
"getToolManagerState"
]
},
"createToolConfiguration": {
"methods": [
"createToolConfiguration"
]
},
"updateToolConfiguration": {
"methods": [
"updateToolConfiguration"
]
},
"deleteToolConfiguration": {
"methods": [
"deleteToolConfiguration"
]
},
"setCurrentToolConfiguration": {
"methods": [
"setCurrentToolConfiguration"
]
},
"updateToolStatus": {
"methods": [
"updateToolStatus"
]
},
"updateToolStatusBatch": {
"methods": [
"updateToolStatusBatch"
]
},
"exportToolConfiguration": {
"methods": [
"exportToolConfiguration"
]
},
"importToolConfiguration": {
"methods": [
"importToolConfiguration"
]
},
"getEnabledTools": {
"methods": [
"getEnabledTools"
]
},
"get-config-status": {
"methods": [
"getConfigStatus"
]
},
"generate-cli-commands": {
"methods": [
"generateCLICommands"
]
},
"generate-client-config": {
"methods": [
"generateClientConfig"
]
},
"add-to-client": {
"methods": [
"addToClient"
]
},
"remove-from-client": {
"methods": [
"removeFromClient"
]
},
"add-to-all-clients": {
"methods": [
"addToAllClients"
]
},
"remove-from-all-clients": {
"methods": [
"removeFromAllClients"
]
},
"open-config-file": {
"methods": [
"openConfigFile"
]
},
"check-license": {
"methods": [
"checkLicense"
]
},
"subscription-status": {
"methods": [
"subscriptionStatus"
]
},
"login-subscription": {
"methods": [
"loginSubscription"
]
},
"login-subscription-google": {
"methods": [
"loginSubscriptionGoogle"
]
},
"logout-subscription": {
"methods": [
"logoutSubscription"
]
},
"activate-license": {
"methods": [
"activateLicense"
]
},
"deactivate-license": {
"methods": [
"deactivateLicense"
]
},
"get-license-info": {
"methods": [
"getLicenseInfo"
]
},
"check-update": {
"methods": [
"checkUpdate"
]
},
"skip-update": {
"methods": [
"skipUpdate"
]
},
"get-machine-id": {
"methods": [
"getMachineId"
]
},
"preview-play": {
"methods": [
"default.onPreviewPlay"
]
},
"preview-stop": {
"methods": [
"default.onPreviewStop"
]
},
"device-removed": {
"methods": [
"default.device-removed"
]
}
},
"scene": {
"script": "./dist/scene.js",
"methods": [
"createNewScene",
"addComponentToNode",
"removeComponentFromNode",
"getNodeInfo",
"getAllNodes",
"findNodeByName",
"setNodeProperty",
"setComponentProperty",
"getCurrentSceneInfo",
"getSceneHierarchy",
"createPrefabFromNode",
"captureContent"
]
}
}
}
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Batch auto_knowledge via MCP HTTP — gets runtime defaults from editor
* Requires: editor running with MCP on port 3000
*/
import http from 'http';
const MCP_URL = 'http://127.0.0.1:3000/mcp';
const ALL = [
'cc.Label','cc.Sprite','cc.Button','cc.Widget','cc.Layout',
'cc.UITransform','cc.RichText','cc.ScrollView','cc.Toggle',
'cc.ToggleContainer','cc.ProgressBar','cc.Slider','cc.PageView',
'cc.Mask','cc.Graphics','cc.UIOpacity','cc.SafeArea',
'cc.BlockInputEvents','cc.LabelOutline','cc.LabelShadow',
'cc.PageViewIndicator','cc.EditBox',
'cc.AudioSource','cc.VideoPlayer','cc.WebView',
'cc.ParticleSystem2D',
'cc.Camera','cc.Canvas','cc.Animation','cc.SkeletalAnimation',
'cc.MeshRenderer','cc.SkinnedMeshRenderer',
'cc.DirectionalLight','cc.PointLight','cc.SpotLight','cc.SphereLight',
'cc.RigidBody2D','cc.BoxCollider2D','cc.CircleCollider2D','cc.PolygonCollider2D',
'cc.RigidBody','cc.BoxCollider','cc.SphereCollider','cc.CapsuleCollider','cc.MeshCollider',
'cc.MotionStreak','cc.Billboard','cc.Line',
'cc.TiledMap','cc.TiledLayer',
'cc.HingeJoint2D','cc.DistanceJoint2D','cc.SpringJoint2D',
'cc.FixedJoint2D','cc.WheelJoint2D','cc.SliderJoint2D',
'cc.RelativeJoint2D','cc.MouseJoint2D',
'cc.HingeConstraint','cc.FixedConstraint','cc.PointToPointConstraint',
'cc.Terrain','cc.LODGroup','cc.ReflectionProbe',
'sp.Skeleton','dragonBones.ArmatureDisplay',
];
function callMcp(toolName, args) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
jsonrpc: '2.0', id: Date.now(), method: 'tools/call',
params: { name: toolName, arguments: args }
});
const req = http.request(MCP_URL, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
timeout: 30000,
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
try {
const data = JSON.parse(Buffer.concat(chunks).toString());
const content = data.result?.content || [];
const text = content.find(c => c.type === 'text')?.text;
resolve(text ? JSON.parse(text) : {});
} catch (e) { reject(e); }
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
req.write(body);
req.end();
});
}
// Check connectivity
try {
await callMcp('cocos_editor', { action: 'project_info' });
} catch {
console.error('❌ Cannot connect to MCP at', MCP_URL);
console.error(' Make sure editor is running and plugin is loaded.');
process.exit(1);
}
console.log(`Connected. Running auto_knowledge(save:true) for ${ALL.length} components...\n`);
let ok = 0, fail = 0, withDefaults = 0;
for (const comp of ALL) {
try {
const r = await callMcp('cocos_devtools', { action: 'auto_knowledge', componentType: comp, save: true });
if (r.success) {
ok++;
const hasRuntime = r.data?.hasRuntimeDefaults;
if (hasRuntime) withDefaults++;
const propCount = Object.keys(r.data?.recommendedKnowledge?.properties || {}).length;
console.log(` ✅ ${comp.padEnd(35)} ${propCount} props ${hasRuntime ? '(+defaults)' : '(dts only)'}`);
} else {
fail++;
console.log(` ❌ ${comp}: ${r.error}`);
}
} catch (e) {
fail++;
console.log(` ❌ ${comp}: ${e.message}`);
}
}
console.log(`\nDone: ${ok} ok, ${fail} fail, ${withDefaults} with runtime defaults`);
@@ -0,0 +1,83 @@
/**
* Batch auto_knowledge — generate complete COMPONENT_PROPERTIES from d.ts
* Output: knowledge JSON files + combined TS snippet for knowledge-handler.ts
*/
const path = require('path');
const fs = require('fs');
const mod = require(path.resolve(__dirname, '../dist/tools/cocos/utils/component-knowledge'));
const ALL_COMPONENTS = [
'cc.Label', 'cc.Sprite', 'cc.Button', 'cc.Widget', 'cc.Layout',
'cc.UITransform', 'cc.RichText', 'cc.ScrollView', 'cc.Toggle',
'cc.ToggleContainer', 'cc.ProgressBar', 'cc.Slider', 'cc.PageView',
'cc.Mask', 'cc.Graphics', 'cc.UIOpacity', 'cc.SafeArea',
'cc.BlockInputEvents', 'cc.LabelOutline', 'cc.LabelShadow',
'cc.PageViewIndicator', 'cc.EditBox',
'cc.AudioSource', 'cc.VideoPlayer', 'cc.WebView',
'cc.ParticleSystem2D', 'cc.ParticleSystem',
'cc.Camera', 'cc.Canvas', 'cc.Animation', 'cc.SkeletalAnimation',
'cc.MeshRenderer', 'cc.SkinnedMeshRenderer',
'cc.DirectionalLight', 'cc.PointLight', 'cc.SpotLight', 'cc.SphereLight',
'cc.RigidBody2D', 'cc.BoxCollider2D', 'cc.CircleCollider2D', 'cc.PolygonCollider2D',
'cc.RigidBody', 'cc.BoxCollider', 'cc.SphereCollider', 'cc.CapsuleCollider', 'cc.MeshCollider',
'cc.MotionStreak', 'cc.Billboard', 'cc.Line',
'cc.TiledMap', 'cc.TiledLayer',
'cc.HingeJoint2D', 'cc.DistanceJoint2D', 'cc.SpringJoint2D',
'cc.FixedJoint2D', 'cc.WheelJoint2D', 'cc.SliderJoint2D',
'cc.RelativeJoint2D', 'cc.MouseJoint2D',
'cc.HingeConstraint', 'cc.FixedConstraint', 'cc.PointToPointConstraint',
'cc.Terrain', 'cc.LODGroup', 'cc.ReflectionProbe',
'sp.Skeleton', 'dragonBones.ArmatureDisplay',
];
const snippets = [];
let saved = 0, failed = 0;
for (const comp of ALL_COMPONENTS) {
const className = mod.componentTypeToClassName(comp);
const dtsInfo = mod.parseDtsClass(className, { recursive: true });
if (!dtsInfo) {
console.log(` ❌ ${comp}: not found in d.ts`);
failed++;
continue;
}
const classified = mod.classifyProperties(dtsInfo, null);
const knowledge = mod.buildRecommendedKnowledge(comp, dtsInfo, classified);
// Save knowledge file
mod.saveKnowledgeFile(comp, knowledge);
// Generate TS snippet
const snippet = mod.generateTsSnippet(comp, knowledge);
snippets.push(snippet);
const propCount = Object.keys(knowledge.properties).length;
console.log(` ✅ ${comp}: ${propCount} props`);
saved++;
}
// Write combined snippets to file
const outputPath = path.resolve(__dirname, '../dev-knowledge/COMPONENT_PROPERTIES_GENERATED.ts');
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
const header = `/**
* AUTO-GENERATED by batch-auto-knowledge.js
* Date: ${new Date().toISOString()}
* Components: ${saved}
*
* Paste this into COMPONENT_PROPERTIES in knowledge-handler.ts
*/
const COMPONENT_PROPERTIES: Record<string, any> = {\n`;
const footer = `\n};\n`;
fs.writeFileSync(outputPath, header + snippets.join('\n') + footer, 'utf-8');
console.log(`\n Saved: ${saved} | Failed: ${failed}`);
console.log(` Knowledge files: dev-knowledge/components/`);
console.log(` TS output: ${outputPath}`);
@@ -0,0 +1,111 @@
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const PATH = {
packageJSON: path.join(__dirname, "../package.json")
};
function checkCreatorTypesVersion(version) {
try {
// 根据平台选择合适的npm命令
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
// 检查npm命令是否可用
const npmCheck = spawnSync(npmCmd, ["--version"], {
stdio: 'pipe',
shell: process.platform === "win32"
});
if (npmCheck.error || npmCheck.status !== 0) {
console.warn("Warning: npm command not available, skipping version check");
return true; // 如果npm不可用,跳过检查
}
// 获取版本列表
const result = spawnSync(npmCmd, ["view", "@cocos/creator-types", "versions"], {
stdio: 'pipe',
shell: process.platform === "win32"
});
if (result.error || result.status !== 0) {
console.warn("Warning: Failed to fetch @cocos/creator-types versions, skipping check");
return true; // 如果获取失败,跳过检查
}
let output = result.stdout.toString().trim();
// 尝试解析JSON
try {
const versions = JSON.parse(output);
if (Array.isArray(versions)) {
return versions.includes(version);
} else if (typeof versions === 'string') {
return versions.includes(version);
}
} catch (parseError) {
// 如果JSON解析失败,尝试作为字符串处理
return output.includes(version);
}
return false;
} catch (error) {
console.warn("Warning: Version check failed:", error.message);
return true; // 出错时跳过检查
}
}
function getCreatorTypesVersion() {
try {
// 检查package.json文件是否存在
if (!fs.existsSync(PATH.packageJSON)) {
console.warn("Warning: package.json not found");
return null;
}
const packageContent = fs.readFileSync(PATH.packageJSON, "utf8");
const packageJson = JSON.parse(packageContent);
// 检查devDependencies是否存在
if (!packageJson.devDependencies || !packageJson.devDependencies["@cocos/creator-types"]) {
console.warn("Warning: @cocos/creator-types not found in devDependencies");
return null;
}
const versionString = packageJson.devDependencies["@cocos/creator-types"];
return versionString.replace(/^[^\d]+/, "");
} catch (error) {
console.warn("Warning: Failed to read package.json:", error.message);
return null;
}
}
function main() {
try {
const creatorTypesVersion = getCreatorTypesVersion();
if (!creatorTypesVersion) {
console.log("Skipping @cocos/creator-types version check");
return;
}
if (!checkCreatorTypesVersion(creatorTypesVersion)) {
console.log("\x1b[33mWarning:\x1b[0m");
console.log(" @en");
console.log(" Version check of @cocos/creator-types failed.");
console.log(` The definition of ${creatorTypesVersion} has not been released yet. Please export the definition to the ./node_modules directory by selecting "Developer -> Export Interface Definition" in the menu of the Creator editor.`);
console.log(" The definition of the corresponding version will be released on npm after the editor is officially released.");
console.log(" @zh");
console.log(" @cocos/creator-types 版本检查失败。");
console.log(` ${creatorTypesVersion} 定义还未发布,请先通过 Creator 编辑器菜单 "开发者 -> 导出接口定义",导出定义到 ./node_modules 目录。`);
console.log(" 对应版本的定义会在编辑器正式发布后同步发布到 npm 上。");
}
} catch (error) {
console.error("Preinstall script error:", error.message);
// 不要抛出错误,让安装继续进行
process.exit(0);
}
}
// 执行主函数
main();
@@ -0,0 +1,25 @@
/**
* Generate COMPONENT_PROPERTIES from saved dev-knowledge JSON files.
* Does NOT re-parse d.ts — uses the saved knowledge (which has runtime defaults).
*/
const path = require('path');
const fs = require('fs');
const mod = require(path.resolve(__dirname, '../dist/tools/cocos/utils/component-knowledge'));
const files = mod.listKnowledgeFiles();
const snippets = [];
for (const comp of files) {
const knowledge = mod.loadKnowledgeFile(comp);
if (!knowledge) continue;
snippets.push(mod.generateTsSnippet(comp, knowledge));
const propCount = Object.keys(knowledge.properties).length;
const defaultCount = Object.values(knowledge.properties).filter(p => p.default !== undefined).length;
console.log(` ${comp.padEnd(35)} ${propCount} props, ${defaultCount} with defaults`);
}
const outputPath = path.resolve(__dirname, '../dev-knowledge/COMPONENT_PROPERTIES_GENERATED.txt');
const header = `const COMPONENT_PROPERTIES: Record<string, any> = {\n`;
const footer = `\n};\n`;
fs.writeFileSync(outputPath, header + snippets.join('\n') + footer, 'utf-8');
console.log(`\nGenerated ${files.length} components → ${outputPath}`);
@@ -0,0 +1,126 @@
/**
* validate-schemas.js
* 构建后校验所有 MCP 工具的 inputSchema,检测常见 JSON Schema 错误:
* - array 类型缺少 items
* - object 类型缺少 properties
* - enum 为空数组
* - type 字段缺失
*/
const path = require('path');
const fs = require('fs');
// 颜色
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const GREEN = '\x1b[32m';
const RESET = '\x1b[0m';
let errors = 0;
let warnings = 0;
function validateSchema(schema, context, toolName) {
if (!schema || typeof schema !== 'object') return;
// 检查 array 缺少 items
if (schema.type === 'array' && !schema.items) {
console.error(`${RED}ERROR${RESET} [${toolName}] ${context}: type "array" missing "items"`);
errors++;
}
// 检查 items 但不是 array
if (schema.items && schema.type !== 'array') {
console.warn(`${YELLOW}WARN${RESET} [${toolName}] ${context}: has "items" but type is "${schema.type}", not "array"`);
warnings++;
}
// 检查 enum 为空
if (schema.enum && Array.isArray(schema.enum) && schema.enum.length === 0) {
console.warn(`${YELLOW}WARN${RESET} [${toolName}] ${context}: empty "enum" array`);
warnings++;
}
// 递归检查 properties
if (schema.properties && typeof schema.properties === 'object') {
for (const [key, value] of Object.entries(schema.properties)) {
validateSchema(value, `${context}.${key}`, toolName);
}
}
// 递归检查 items
if (schema.items && typeof schema.items === 'object') {
validateSchema(schema.items, `${context}.items`, toolName);
}
}
function main() {
const distDir = path.join(__dirname, '..', 'dist');
if (!fs.existsSync(distDir)) {
console.error(`${RED}ERROR${RESET} dist/ not found. Run "npm run build" first.`);
process.exit(1);
}
// 收集所有工具定义
const tools = [];
// 1. 加载 CocosTools
try {
const { CocosTools } = require(path.join(distDir, 'tools', 'cocos', 'cocos-tools.js'));
const cocosTools = new CocosTools();
tools.push(...cocosTools.getTools());
} catch (e) {
console.warn(`${YELLOW}WARN${RESET} Could not load CocosTools: ${e.message}`);
}
// 2. 加载独立工具集
const toolFiles = [
'tools/scene-tools.js',
'tools/node-tools.js',
'tools/component-tools.js',
'tools/prefab-tools.js',
'tools/project-tools.js',
'tools/scene-view-tools.js',
'tools/reference-image-tools.js',
'tools/preferences-tools.js',
'tools/asset-advanced-tools.js',
'tools/debug-tools.js',
'tools/server-tools.js',
];
for (const file of toolFiles) {
try {
const fullPath = path.join(distDir, file);
if (!fs.existsSync(fullPath)) continue;
const mod = require(fullPath);
// 找到导出的类并实例化
for (const [, ExportedClass] of Object.entries(mod)) {
if (typeof ExportedClass === 'function' && ExportedClass.prototype && typeof ExportedClass.prototype.getTools === 'function') {
const instance = new ExportedClass();
tools.push(...instance.getTools());
}
}
} catch (e) {
console.warn(`${YELLOW}WARN${RESET} Could not load ${file}: ${e.message}`);
}
}
console.log(`\nValidating ${tools.length} tool schemas...\n`);
for (const tool of tools) {
if (tool.inputSchema) {
validateSchema(tool.inputSchema, 'inputSchema', tool.name);
}
}
console.log('');
if (errors > 0) {
console.error(`${RED}FAILED${RESET}: ${errors} error(s), ${warnings} warning(s)`);
process.exit(1);
} else if (warnings > 0) {
console.log(`${YELLOW}PASSED${RESET} with ${warnings} warning(s)`);
} else {
console.log(`${GREEN}PASSED${RESET}: All schemas valid`);
}
}
main();
@@ -0,0 +1 @@
1.7.9
@@ -0,0 +1,9 @@
{
"port": 3000,
"autoStart": false,
"enableDebugLog": false,
"allowedOrigins": [
"*"
],
"maxConnections": 10
}
@@ -0,0 +1,111 @@
{
"configurations": [
{
"id": "1a189805-4f21-4147-91ab-461fe9ff5f28",
"name": "默认配置",
"description": "自动创建的默认工具配置",
"tools": [
{
"category": "cocos",
"name": "scene",
"enabled": true,
"description": "Scene management — open/save/create scenes, get hierarchy, undo, detect mode, list_components, validate_scene.\nCommon: hierarchy, get_info, open, save, create, validate_scene, list_components.\nNote: validate_scene checks broken refs IN this tool (NOT cocos_validate). list_components lists all available component types.\nExample: {action:\"hierarchy\"} or {action:\"validate_scene\"}"
},
{
"category": "cocos",
"name": "node",
"enabled": true,
"description": "⚡ BATCH: When modifying 3+ nodes (position/size/anchor), use \"batch_modify\" instead of calling \"modify\" repeatedly — it's faster and atomic.\nNode CRUD — create/find/modify/delete nodes, list all nodes, set transform, mount/remove scripts, reorder siblings.\nActions: find, info, list, tree, create, delete, modify, move, reorder, duplicate, copy, paste, cut, mount_script, remove_script, reset, detect_type, batch_modify.\nNote: \"list\" returns all nodes flat list, \"tree\" returns subtree of a node. Use \"info\" (NOT get_info), \"reset\" (NOT reset_transform). \"remove_script\" for scripts (NOT component.remove). \"reorder\" to change sibling order within same parent. \"batch_modify\" to modify multiple nodes in one call.\nExample: {action:\"list\"} or {action:\"tree\", node:\"Canvas\"} or {action:\"batch_modify\", nodes:[{node:\"Player\", position:{x:0,y:0,z:0}}]}"
},
{
"category": "cocos",
"name": "component",
"enabled": true,
"description": "⚡ BATCH: When binding 3+ click events, use \"batch_click_event\" instead of calling \"click_event\" repeatedly.\nComponent management — add/remove/configure components on nodes.\nActions: add, remove, list, info, set_property, available_types, click_event, batch_click_event.\nNote: use \"info\" to view component properties (NOT get_properties). To remove scripts, use cocos_node(action:\"remove_script\"). \"batch_click_event\" binds click events on multiple buttons in one call.\nExample: {action:\"info\", node:\"Canvas/Btn\", componentType:\"cc.Button\"}"
},
{
"category": "cocos",
"name": "prefab",
"enabled": true,
"description": "Prefab management — list/info/delete/create/instantiate/edit prefabs, apply/revert changes.\nAll prefab operations (including delete) belong here, NOT cocos_asset.\nCommon: instantiate, create, delete, apply, edit_enter, edit_save, edit_exit.\nExample: {action:\"instantiate\", prefabPath:\"db://assets/prefabs/Enemy.prefab\", parent:\"Canvas\"}"
},
{
"category": "cocos",
"name": "asset",
"enabled": true,
"description": "Asset operations — query/search/create/delete assets, path-UUID conversion.\nCommon: query_uuid, find_by_name, search, create, delete.\nExample: {action:\"query_uuid\", url:\"db://assets/textures/bg.png\"}"
},
{
"category": "cocos",
"name": "editor",
"enabled": true,
"description": "Editor utilities — project info, console logs, preferences, build, preview, run/stop, reload.\nGroups: project_*, console_*, log_*, pref_*, build*, server_*, run, stop, reload.\n\"run\" launches preview (platform: browser/simulator/editor). \"stop\" stops editor preview. \"reload\" reloads editor window (Developer → 重新加载).\nExample: {action:\"run\", platform:\"editor\"} then {action:\"stop\"}"
},
{
"category": "cocos",
"name": "view",
"enabled": true,
"description": "Viewport control — gizmo tools, 2D/3D mode, camera focus/align, reference images.\nCommon: gizmo_tool, camera_focus, camera_align_view (sync camera→view), camera_align_node (sync view→node).\nExample: {action:\"camera_focus\", nodes:[\"Player\"]} or {action:\"camera_align_node\", nodes:[\"Camera\"]}"
},
{
"category": "cocos",
"name": "composite",
"enabled": true,
"description": "One-call UI creation — create complete Button/Label/Image, mount script with bindings.\nActions: create_button, create_label, create_image, mount_and_bind, setup_widget, batch, batch_create_button, batch_create_label, batch_create_image.\nNote: mount_and_bind mounts script AND binds properties in one call (NOT node.mount_script which only mounts). create_button supports optional clickEvent to bind click handler in one call.\nBatch hint: 3+ buttons → batch_create_button, 3+ labels → batch_create_label, 3+ images → batch_create_image.\nExample: {action:\"mount_and_bind\", node:\"Hero\", scriptPath:\"db://assets/scripts/Game.ts\", bindings:{...}}"
},
{
"category": "cocos",
"name": "knowledge",
"enabled": true,
"description": "Cocos Creator reference — component properties, UI rules, layout patterns, animation recipes, best practices, tool guides.\nUse topic:\"tool_guide\" with query:\"tool.action\" before calling unfamiliar tools.\nExample: {topic:\"component_properties\", query:\"cc.Label\"}, {topic:\"animation_patterns\", query:\"scale_bounce\"}"
},
{
"category": "cocos",
"name": "validate",
"enabled": true,
"description": "Advanced validation rules — layout overlap/offscreen, reference consistency, hierarchy depth.\nNote: Use this for deep inspection (NOT scene.validate_scene which only checks broken refs).\nActions: layout (overlap/offscreen), references (broken asset refs), hierarchy (depth/naming).\nExample: {action:\"layout\"} or {action:\"references\"}"
},
{
"category": "cocos",
"name": "template",
"enabled": true,
"description": "UI templates — apply pre-built structures (dialog, scroll_list, nav_bar, settings_page).\nNote: Use this (NOT composite/builder) when user wants a standard UI pattern. Use list to show available templates.\nActions: list, apply.\nExample: {action:\"list\"} or {action:\"apply\", template:\"dialog\", parent:\"Canvas\"}"
},
{
"category": "cocos",
"name": "capture",
"enabled": true,
"description": "Capture — scene layout JSON + real screenshot.\nActions: scene_snapshot (full scene JSON), node_snapshot (focused subtree JSON), screenshot (PNG).\nscreenshot has TWO modes:\n • mode:\"content\" (RECOMMENDED for pixel diffs) — returns a REAL engine render of ONLY the Canvas content at design resolution with a transparent (or chosen) background: NO editor panels/grid/gizmos/FPS overlay. Runs the built web runtime headlessly internally (one MCP call → content PNG). Requires scenePath. Renders the LAST BUILT (saved) scene; reuses build/web-mobile (auto-builds if missing; rebuild:true forces a fresh build, ~60s). Deterministic. Returns pngBase64.\n • mode:\"window\" (default, legacy) — PNG of the Creator editor window via OS capture; includes panels/grid. Returns contentRect (the scene-viewport box, when detectable) so you can crop.\nUse scene/node_snapshot for position/size/component data; use screenshot for a real visual.\nExamples: {action:\"screenshot\", mode:\"content\", scenePath:\"db://assets/scenes/Main.scene\"} or {action:\"screenshot\", mode:\"content\", scenePath:\"Sprite\", width:1280, height:720, background:{r:0,g:0,b:0,a:0}, rebuild:true} or legacy {action:\"screenshot\"}"
},
{
"category": "cocos",
"name": "builder",
"enabled": true,
"description": "Build node tree from JSON — create complete hierarchies with components in one call.\nNote: Use this (NOT composite/node) when building complex multi-level UI structures. Query tool_guide for JSON format.\nPrefab shortcut: use \"type\":\"Button\" (or Label, Sprite, ScrollView, etc.) instead of components[] to create complete UI controls from engine prefab templates.\nExample: {action:\"build\", parent:\"Canvas\", tree:{name:\"Panel\", components:[\"cc.Sprite\"], children:[{name:\"MyBtn\", type:\"Button\"}]}}"
},
{
"category": "cocos",
"name": "animation",
"enabled": true,
"description": "Animation editing — clips, keyframes, tracks, events, presets, batch operations.\nRecommended: batch_file (no edit mode needed), fallback to batch. Use preset for common animations.\nExample: {action:\"preset\", clipUuid:\"...\", preset:\"sprite_fade_in\"} or {action:\"add_event\", clipUuid:\"...\", frame:10, eventData:{func:\"onHit\"}}"
},
{
"category": "cocos",
"name": "spine",
"enabled": true,
"description": "Spine (sp.Skeleton) — query animations/skins, set animation/skin, change properties, swap skeleton data, manage sockets.\nActions: info, list_animations, list_skins, set_animation, set_skin, set_property, set_data, add_socket, remove_socket.\nNote: \"node\" is the node path or UUID with sp.Skeleton component.\nExample: {action:\"list_animations\", node:\"spinNode\"} or {action:\"set_animation\", node:\"spinNode\", animation:\"idle\"}"
},
{
"category": "cocos",
"name": "label",
"enabled": true,
"description": "Label & Text — manage cc.Label, cc.RichText, cc.EditBox. Query/set text, font, style, outline, shadow. Batch operations.\nActions: info, list, set_text, set_font, set_style, set_outline, set_shadow, batch_set_font, batch_set_style.\nSupports: cc.Label (full), cc.RichText (text/font/fontSize/color), cc.EditBox (text/placeholder).\nExample: {action:\"set_text\", node:\"Title\", text:\"Hello\"}\n{action:\"set_style\", node:\"Title\", fontSize:32, isBold:true, color:{r:255,g:0,b:0,a:255}}\n{action:\"batch_set_font\", font:\"db://assets/fonts/myfont.ttf\"}"
}
],
"createdAt": "2026-08-18T23:28:53.590Z",
"updatedAt": "2026-08-18T23:28:53.590Z"
}
],
"currentConfigId": "1a189805-4f21-4147-91ab-461fe9ff5f28",
"maxConfigSlots": 5
}