Release v0.4.0
This commit is contained in:
@@ -6,9 +6,42 @@ This project follows a simple changelog format inspired by [Keep a Changelog](ht
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-06-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added project identity metadata to `/health` and MCP `initialize` responses so clients and duplicate listeners can verify the active Cocos project safely.
|
||||||
|
- Added same-project listener attach behavior before port fallback, preventing accidental attachment to a different Cocos project on the same port.
|
||||||
|
- Added default-on JavaScript safety checks for `execute_javascript`, `execute_scene_script`, and `execute_editor_script`, with per-call `safety_checks` overrides.
|
||||||
|
- Added named tool profiles in the Cocos panel, including save, apply, delete, import, and export workflows.
|
||||||
|
- Added category-level tool exposure controls in the panel for quick enable, disable, and clear actions.
|
||||||
|
- Added asset dependency tools: `inspect_asset_dependencies` and `validate_asset_dependencies`.
|
||||||
|
- Added Cocos project/editor tools: `get_build_status`, `open_build_panel`, `run_project_preview`, `save_current_scene`, `get_editor_preference`, `set_editor_preference`, and `broadcast_editor_message`.
|
||||||
|
- Added Button event binding tools: `list_button_click_events` and `bind_button_click_event`.
|
||||||
|
- Added `create_cocos_mcp_project_skill` for generating a recommended local Codex workflow skill.
|
||||||
|
- Added release package sensitive-content scanning for npm/GitHub/MCP token-like values and private keys.
|
||||||
|
|
||||||
|
### Optimized
|
||||||
|
|
||||||
|
- Improved tool exposure UX for larger projects by making custom profiles reusable and shareable.
|
||||||
|
- Expanded generated tool documentation and README coverage for the new 37-tool `core` profile and 101-tool `full` profile.
|
||||||
|
- Improved release packaging confidence with checksum verification and content scanning.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Expanded the default `core` profile from 34 tools to 37 tools.
|
||||||
|
- Expanded the `full` profile from 89 tools to 101 tools.
|
||||||
|
- Split more tool implementations into focused modules under `lib/tools/`, including advanced assets, Cocos project/editor helpers, and scene event helpers.
|
||||||
|
- Persisted `executeJavascriptSafetyChecks`, active tool profile names, and saved tool profiles in project configuration.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed the GitHub Release workflow so release pages use generated English changelog-style `RELEASE_NOTES.md` instead of the artifact installation README.
|
- Fixed the GitHub Release workflow so release pages use generated English changelog-style `RELEASE_NOTES.md` instead of the artifact installation README.
|
||||||
|
- Fixed release package scanning false positives around normal MCP server config names while preserving credential detection.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Added guardrails for JavaScript execution against obvious risky file-system and shell patterns, including delete/truncate calls, raw writable streams, path traversal, user/system absolute paths, and `child_process`.
|
||||||
|
|
||||||
## [0.3.3] - 2026-05-20
|
## [0.3.3] - 2026-05-20
|
||||||
|
|
||||||
|
|||||||
@@ -65,16 +65,17 @@ Funplay > MCP Server
|
|||||||
|
|
||||||
The server runs on `http://127.0.0.1:8765/` by default.
|
The server runs on `http://127.0.0.1:8765/` by default.
|
||||||
|
|
||||||
If the configured port is already occupied, the extension automatically falls back to the next available local port and uses the actual running port for one-click MCP client configuration.
|
If the configured port is already occupied, the extension first checks whether the existing listener belongs to the same Cocos project. Same-project listeners are reused safely; unrelated listeners trigger automatic fallback to the next available local port.
|
||||||
|
|
||||||
The panel is intentionally small:
|
The panel is intentionally small:
|
||||||
|
|
||||||
- Enable or disable the MCP server
|
- Enable or disable the MCP server
|
||||||
- Change the server port
|
- Change the server port
|
||||||
- Switch tool exposure between `core`, `full`, and `custom`
|
- Switch tool exposure between `core`, `full`, and `custom`
|
||||||
|
- Save, apply, import, and export named tool profiles
|
||||||
- Check the installed version against the latest GitHub release
|
- Check the installed version against the latest GitHub release
|
||||||
- Inspect recent tool calls and runtime log previews
|
- Inspect recent tool calls and runtime log previews
|
||||||
- Tune tool exposure by category or individual tool
|
- Tune tool exposure by category controls or individual tool names
|
||||||
- Configure AI clients with one click and preview the selected client config
|
- Configure AI clients with one click and preview the selected client config
|
||||||
- Copy quick `curl` commands for `/health` and `/tools`
|
- Copy quick `curl` commands for `/health` and `/tools`
|
||||||
- Expand debug output only when needed
|
- Expand debug output only when needed
|
||||||
@@ -236,10 +237,11 @@ Try a higher-level prompt in your AI client:
|
|||||||
- The MCP server listens on `http://127.0.0.1:8765/` by default.
|
- The MCP server listens on `http://127.0.0.1:8765/` by default.
|
||||||
- If the configured port is busy, the server automatically falls back to the next available port and the panel/client config use the actual running port.
|
- If the configured port is busy, the server automatically falls back to the next available port and the panel/client config use the actual running port.
|
||||||
- `GET /health` and `GET /tools` are read-only debug endpoints for quick local checks outside an MCP client.
|
- `GET /health` and `GET /tools` are read-only debug endpoints for quick local checks outside an MCP client.
|
||||||
- The default `core` profile exposes 34 high-signal tools. Switch to `full` for all 89 tools, or use `custom` to include/exclude tool categories and individual tools.
|
- The default `core` profile exposes 37 high-signal tools. Switch to `full` for all 101 tools, or use `custom` to include/exclude tool categories and individual tools.
|
||||||
- The panel includes a manual update check against the latest GitHub release.
|
- The panel includes a manual update check against the latest GitHub release.
|
||||||
- Streamable HTTP responses follow the MCP transport requirements for `Accept`, `MCP-Protocol-Version`, JSON-RPC notifications/responses, and optional `Mcp-Session-Id` sessions.
|
- Streamable HTTP responses follow the MCP transport requirements for `Accept`, `MCP-Protocol-Version`, JSON-RPC notifications/responses, and optional `Mcp-Session-Id` sessions.
|
||||||
- Tool listings include MCP `outputSchema` and `annotations`; structured tool results use a standard envelope with `ok`, `tool`, `callId`, `summary`, `data`, and follow-up `refs`.
|
- Tool listings include MCP `outputSchema` and `annotations`; structured tool results use a standard envelope with `ok`, `tool`, `callId`, `summary`, `data`, and follow-up `refs`.
|
||||||
|
- `execute_javascript` safety checks are enabled by default. They block obvious risky filesystem and shell patterns such as delete/truncate calls, raw writable streams, path traversal, user/system absolute paths, and `child_process`. This is a guardrail, not a full sandbox; a call can explicitly pass `safety_checks: false` when you have reviewed the risk.
|
||||||
- All exposed MCP tools execute directly. There is no extra approval toggle inside the Cocos extension.
|
- All exposed MCP tools execute directly. There is no extra approval toggle inside the Cocos extension.
|
||||||
- File tools and `cocos://asset/path/...` resources are restricted to the active Cocos project root.
|
- File tools and `cocos://asset/path/...` resources are restricted to the active Cocos project root.
|
||||||
- The recommended workflow is `execute_javascript` first, then focused helper tools for screenshots, diagnostics, assets, and inspection.
|
- The recommended workflow is `execute_javascript` first, then focused helper tools for screenshots, diagnostics, assets, and inspection.
|
||||||
@@ -251,12 +253,12 @@ Try a higher-level prompt in your AI client:
|
|||||||
- **Embedded Cocos Extension** — No separate Python daemon or external bridge process is required for the Cocos-side plugin
|
- **Embedded Cocos Extension** — No separate Python daemon or external bridge process is required for the Cocos-side plugin
|
||||||
- **One-Click Client Configuration** — Configure Claude Code, Cursor, VS Code, Trae, Kiro, and Codex directly from Cocos Creator
|
- **One-Click Client Configuration** — Configure Claude Code, Cursor, VS Code, Trae, Kiro, and Codex directly from Cocos Creator
|
||||||
- **Project Context Built In** — Exposes live project, scene, selection, script diagnostics, logs, and interaction-history resources
|
- **Project Context Built In** — Exposes live project, scene, selection, script diagnostics, logs, and interaction-history resources
|
||||||
- **Focused by Default, Full When Needed** — `core` reduces tool-list noise; `full` exposes every available tool; `custom` lets you tune by category or tool
|
- **Focused by Default, Full When Needed** — `core` reduces tool-list noise; `full` exposes every available tool; `custom` plus saved profiles lets you tune and restore tool exposure by category or tool
|
||||||
- **Visual Validation** — Scene/editor/preview screenshots and input simulation help AI verify UI and gameplay changes
|
- **Visual Validation** — Scene/editor/preview screenshots and input simulation help AI verify UI and gameplay changes
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **89 Built-in Tools** — Scene hierarchy, editor state, selection workflows, prefabs, assets, project instructions, UI creation, components, files, logs, script diagnostics, screenshots, runtime control, and input simulation
|
- **101 Built-in Tools** — Scene hierarchy, editor state, selection workflows, prefabs, assets, asset dependencies, project instructions, UI creation, components, files, logs, script diagnostics, screenshots, runtime control, build/preview helpers, editor preferences, event binding, and input simulation
|
||||||
- **Primary Unified Tool** — `execute_javascript` supports both `scene` and `editor` contexts
|
- **Primary Unified Tool** — `execute_javascript` supports both `scene` and `editor` contexts
|
||||||
- **Resources & Prompts** — Live project/log resources plus reusable workflows like script fixing, scene validation, and playable prototype creation
|
- **Resources & Prompts** — Live project/log resources plus reusable workflows like script fixing, scene validation, and playable prototype creation
|
||||||
- **Cocos Panel UI** — A compact `Funplay > MCP Server` panel for service management, update checks, tool exposure, recent activity, logs, curl diagnostics, and MCP client setup
|
- **Cocos Panel UI** — A compact `Funplay > MCP Server` panel for service management, update checks, tool exposure, recent activity, logs, curl diagnostics, and MCP client setup
|
||||||
@@ -273,22 +275,22 @@ Funplay MCP for Cocos follows the same design principles as Funplay MCP for Unit
|
|||||||
| Embedded server | Built-in HTTP MCP server | Built-in HTTP MCP server |
|
| Embedded server | Built-in HTTP MCP server | Built-in HTTP MCP server |
|
||||||
| Primary execution tool | `execute_javascript` | `execute_code` |
|
| Primary execution tool | `execute_javascript` | `execute_code` |
|
||||||
| Primary language | JavaScript in scene/editor contexts | C# in Unity editor/runtime contexts |
|
| Primary language | JavaScript in scene/editor contexts | C# in Unity editor/runtime contexts |
|
||||||
| Default profile | `core` with 34 tools | `core` focused tool profile |
|
| Default profile | `core` with 37 tools | `core` focused tool profile |
|
||||||
| Full profile | 89 tools plus `custom` exposure | 79 tools |
|
| Full profile | 101 tools plus `custom` exposure | 79 tools |
|
||||||
| Client setup | One-click config panel | One-click config window |
|
| Client setup | One-click config panel | One-click config window |
|
||||||
|
|
||||||
## MCP Capabilities
|
## MCP Capabilities
|
||||||
|
|
||||||
The current package exposes four capability layers:
|
The current package exposes four capability layers:
|
||||||
|
|
||||||
- **Tools** — 34 tools in `core`, 89 tools in `full`, plus `custom` include/exclude rules
|
- **Tools** — 37 tools in `core`, 101 tools in `full`, plus `custom` include/exclude rules and saved tool profiles
|
||||||
- **Primary execution** — `execute_javascript` for scene/runtime and editor/browser automation
|
- **Primary execution** — `execute_javascript` for scene/runtime and editor/browser automation
|
||||||
- **Prompts** — `fix_script_errors`, `create_playable_prototype`, `scene_validation`, and `auto_wire_scene`
|
- **Prompts** — `fix_script_errors`, `create_playable_prototype`, `scene_validation`, and `auto_wire_scene`
|
||||||
- **Resources** — project context, scene summaries, current selection, script diagnostics, asset selection, logs, and MCP interaction history
|
- **Resources** — project context, scene summaries, current selection, script diagnostics, asset selection, logs, and MCP interaction history
|
||||||
|
|
||||||
For the generated tool reference, including categories, profiles, and read/mutation hints, see [docs/TOOLS.md](./docs/TOOLS.md).
|
For the generated tool reference, including categories, profiles, and read/mutation hints, see [docs/TOOLS.md](./docs/TOOLS.md).
|
||||||
|
|
||||||
The default `core` set is intentionally small: `execute_javascript`, `execute_scene_script`, `execute_editor_script`, `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `list_project_instructions`, `read_project_instruction`, `set_selection`, `get_project_info`, `get_scene_info`, `get_hierarchy`, `list_scenes`, `open_scene`, `inspect_prefab`, `validate_prefab_references`, `inspect_prefab_instance`, `list_assets`, `inspect_asset`, `open_asset`, `select_asset`, `run_script_diagnostics`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot`, `get_script_diagnostic_context`, `get_runtime_state`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_preview_screenshot`, and `list_editor_windows`.
|
The default `core` set is intentionally small: `execute_javascript`, `execute_scene_script`, `execute_editor_script`, `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `list_project_instructions`, `read_project_instruction`, `set_selection`, `get_project_info`, `get_build_status`, `get_scene_info`, `get_hierarchy`, `list_scenes`, `open_scene`, `inspect_prefab`, `validate_prefab_references`, `inspect_prefab_instance`, `list_assets`, `inspect_asset`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `open_asset`, `select_asset`, `run_script_diagnostics`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot`, `get_script_diagnostic_context`, `get_runtime_state`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_preview_screenshot`, and `list_editor_windows`.
|
||||||
|
|
||||||
## Built-in Resources
|
## Built-in Resources
|
||||||
|
|
||||||
@@ -307,24 +309,25 @@ The default `core` set is intentionally small: `execute_javascript`, `execute_sc
|
|||||||
|
|
||||||
## Built-in Tools
|
## Built-in Tools
|
||||||
|
|
||||||
Funplay MCP for Cocos currently ships with **89 tool functions** in the `full` profile:
|
Funplay MCP for Cocos currently ships with **101 tool functions** in the `full` profile:
|
||||||
|
|
||||||
| Category | Tools |
|
| Category | Tools |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| **Script Execution** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
| **Script Execution** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
||||||
| **Editor State** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
| **Editor State** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
||||||
| **Project Instructions** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill` |
|
| **Project Instructions** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill`, `create_cocos_mcp_project_skill` |
|
||||||
| **Project & Scene** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
| **Project & Scene** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
||||||
| **Node Editing** | `create_node`, `delete_node`, `set_node_transform` |
|
| **Node Editing** | `create_node`, `delete_node`, `set_node_transform` |
|
||||||
| **Assets & Prefabs** | `list_assets`, `inspect_asset`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
| **Assets & Prefabs** | `list_assets`, `inspect_asset`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
||||||
| **Components** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
| **Components** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
||||||
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
||||||
| **Camera** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
| **Camera** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
||||||
| **Animation** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
| **Animation** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
||||||
| **Files** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
| **Files** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
||||||
| **Diagnostics & Logs** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
| **Diagnostics & Logs** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
||||||
|
| **Build & Editor** | `get_build_status`, `open_build_panel`, `run_project_preview`, `save_current_scene`, `get_editor_preference`, `set_editor_preference`, `broadcast_editor_message` |
|
||||||
| **Runtime** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
| **Runtime** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
||||||
| **Interaction** | `emit_node_event`, `simulate_button_click`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
| **Interaction & Events** | `emit_node_event`, `simulate_button_click`, `list_button_click_events`, `bind_button_click_event`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
||||||
| **Screenshots & Windows** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
| **Screenshots & Windows** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
||||||
|
|
||||||
## Primary Tool Examples
|
## Primary Tool Examples
|
||||||
@@ -365,8 +368,11 @@ Place `funplay-cocos-mcp.config.json` in the Cocos project root:
|
|||||||
"enabledTools": [],
|
"enabledTools": [],
|
||||||
"disabledTools": [],
|
"disabledTools": [],
|
||||||
"enableSessions": false,
|
"enableSessions": false,
|
||||||
|
"executeJavascriptSafetyChecks": true,
|
||||||
"autostart": true,
|
"autostart": true,
|
||||||
"maxInteractionLogEntries": 50
|
"maxInteractionLogEntries": 50,
|
||||||
|
"activeToolProfileName": "",
|
||||||
|
"savedToolProfiles": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -376,7 +382,7 @@ Environment variables are also supported:
|
|||||||
- `COCOS_MCP_PORT`
|
- `COCOS_MCP_PORT`
|
||||||
- `COCOS_MCP_PROFILE`
|
- `COCOS_MCP_PROFILE`
|
||||||
|
|
||||||
`toolProfile: "custom"` starts from the `core` set, then adds `enabledToolCategories` / `enabledTools` and removes `disabledToolCategories` / `disabledTools`. `enableSessions` is off by default because this server does not need cross-request client state for normal editor automation.
|
`toolProfile: "custom"` starts from the `core` set, then adds `enabledToolCategories` / `enabledTools` and removes `disabledToolCategories` / `disabledTools`. The panel can save these exposure settings as named `savedToolProfiles` for quick restore or sharing. `enableSessions` is off by default because this server does not need cross-request client state for normal editor automation.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -394,6 +400,12 @@ Cocos Creator Extension
|
|||||||
│ └─ Minimal MCP Server panel
|
│ └─ Minimal MCP Server panel
|
||||||
└─ lib/
|
└─ lib/
|
||||||
├─ assets, diagnostics, screenshots, input
|
├─ assets, diagnostics, screenshots, input
|
||||||
|
├─ tool-profiles, javascript-safety
|
||||||
|
├─ tools/
|
||||||
|
│ ├─ files
|
||||||
|
│ ├─ assets-advanced
|
||||||
|
│ ├─ cocos-project
|
||||||
|
│ └─ scene-events
|
||||||
└─ server, resources, prompts, tool registry
|
└─ server, resources, prompts, tool registry
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+28
-16
@@ -65,16 +65,17 @@ Funplay > MCP Server
|
|||||||
|
|
||||||
服务默认运行在 `http://127.0.0.1:8765/`。
|
服务默认运行在 `http://127.0.0.1:8765/`。
|
||||||
|
|
||||||
如果配置端口已被占用,扩展会自动回退到下一个可用本地端口,并在一键客户端配置时使用实际运行端口。
|
如果配置端口已被占用,扩展会先检查已有 listener 是否属于同一个 Cocos 项目;同项目 listener 会被安全复用,无关 listener 才会自动回退到下一个可用本地端口。
|
||||||
|
|
||||||
面板刻意保持精简:
|
面板刻意保持精简:
|
||||||
|
|
||||||
- 启用或停用 MCP Server
|
- 启用或停用 MCP Server
|
||||||
- 修改服务端口
|
- 修改服务端口
|
||||||
- 在 `core` / `full` / `custom` 工具暴露模式之间切换
|
- 在 `core` / `full` / `custom` 工具暴露模式之间切换
|
||||||
|
- 保存、套用、导入和导出命名工具 profile
|
||||||
- 检查当前安装版本是否落后于 GitHub 最新 Release
|
- 检查当前安装版本是否落后于 GitHub 最新 Release
|
||||||
- 查看最近工具调用和运行日志预览
|
- 查看最近工具调用和运行日志预览
|
||||||
- 按工具分类或单个工具调整暴露范围
|
- 通过分类控制或单个工具名调整暴露范围
|
||||||
- 一键配置 AI 客户端,并随目标客户端预览对应配置
|
- 一键配置 AI 客户端,并随目标客户端预览对应配置
|
||||||
- 复制 `/health` 和 `/tools` 的快速 `curl` 排障命令
|
- 复制 `/health` 和 `/tools` 的快速 `curl` 排障命令
|
||||||
- 需要时再展开 Debug Output
|
- 需要时再展开 Debug Output
|
||||||
@@ -234,12 +235,13 @@ curl http://127.0.0.1:8765/tools
|
|||||||
|
|
||||||
- 这是一个 **仅限 Editor** 的扩展,用于自动化 Cocos Creator,不会给最终游戏包添加运行时依赖。
|
- 这是一个 **仅限 Editor** 的扩展,用于自动化 Cocos Creator,不会给最终游戏包添加运行时依赖。
|
||||||
- MCP Server 默认监听 `http://127.0.0.1:8765/`。
|
- MCP Server 默认监听 `http://127.0.0.1:8765/`。
|
||||||
- 如果配置端口被占用,服务会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
|
- 如果配置端口被占用,服务会先通过项目身份识别同项目已有 listener;无法确认同项目时才会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
|
||||||
- `GET /health` 和 `GET /tools` 是只读调试端点,方便不用 MCP 客户端也能快速检查本地服务。
|
- `GET /health` 和 `GET /tools` 是只读调试端点,方便不用 MCP 客户端也能快速检查本地服务。
|
||||||
- 默认 `core` profile 暴露 34 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 89 个工具;也可以用 `custom` 按分类或工具名增删。
|
- 默认 `core` profile 暴露 37 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 101 个工具;也可以用 `custom` 按分类或工具名增删。
|
||||||
- 面板提供手动更新检查,会对比当前安装版本和 GitHub 最新 Release。
|
- 面板提供手动更新检查,会对比当前安装版本和 GitHub 最新 Release。
|
||||||
- Streamable HTTP 响应已补齐 MCP 传输层要求,包括 `Accept`、`MCP-Protocol-Version`、JSON-RPC notification/response,以及可选 `Mcp-Session-Id` session。
|
- Streamable HTTP 响应已补齐 MCP 传输层要求,包括 `Accept`、`MCP-Protocol-Version`、JSON-RPC notification/response,以及可选 `Mcp-Session-Id` session。
|
||||||
- 工具列表会包含 MCP `outputSchema` 和 `annotations`;结构化工具结果统一使用包含 `ok`、`tool`、`callId`、`summary`、`data`、`refs` 的标准 envelope。
|
- 工具列表会包含 MCP `outputSchema` 和 `annotations`;结构化工具结果统一使用包含 `ok`、`tool`、`callId`、`summary`、`data`、`refs` 的标准 envelope。
|
||||||
|
- `execute_javascript` 安全检查默认开启,会拦截明显高风险的文件系统和 shell 模式,例如删除/截断调用、原始写入流、路径穿越、用户/系统绝对路径和 `child_process`。这是防护栏,不是完整沙箱;确认风险后可在单次调用中显式传入 `safety_checks: false`。
|
||||||
- 所有已暴露的 MCP 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。
|
- 所有已暴露的 MCP 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。
|
||||||
- 文件工具和 `cocos://asset/path/...` 资源默认只能访问当前 Cocos 项目根目录内的路径。
|
- 文件工具和 `cocos://asset/path/...` 资源默认只能访问当前 Cocos 项目根目录内的路径。
|
||||||
- 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。
|
- 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。
|
||||||
@@ -251,12 +253,12 @@ curl http://127.0.0.1:8765/tools
|
|||||||
- **嵌入式 Cocos 扩展** — Cocos 侧不需要单独 Python 守护进程或外部 bridge
|
- **嵌入式 Cocos 扩展** — Cocos 侧不需要单独 Python 守护进程或外部 bridge
|
||||||
- **一键客户端配置** — 在 Cocos Creator 内直接配置 Claude Code、Cursor、VS Code、Trae、Kiro、Codex
|
- **一键客户端配置** — 在 Cocos Creator 内直接配置 Claude Code、Cursor、VS Code、Trae、Kiro、Codex
|
||||||
- **内建项目上下文** — 直接暴露项目、场景、选择、脚本诊断、日志和交互历史资源
|
- **内建项目上下文** — 直接暴露项目、场景、选择、脚本诊断、日志和交互历史资源
|
||||||
- **默认聚焦,必要时全量** — `core` 降低工具列表噪音,需要时切到 `full` 暴露全部工具;`custom` 可按分类或工具名调整
|
- **默认聚焦,必要时全量** — `core` 降低工具列表噪音,需要时切到 `full` 暴露全部工具;`custom` 与保存的 profile 可按分类或工具名调整并恢复
|
||||||
- **可视化验证** — 截图和输入模拟让 AI 能验证 UI 与玩法改动
|
- **可视化验证** — 截图和输入模拟让 AI 能验证 UI 与玩法改动
|
||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
|
|
||||||
- **89 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制和输入模拟
|
- **101 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、资产依赖、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制、构建/预览辅助、编辑器偏好、事件绑定和输入模拟
|
||||||
- **统一主工具** — `execute_javascript` 同时支持 `scene` 和 `editor` 两种上下文
|
- **统一主工具** — `execute_javascript` 同时支持 `scene` 和 `editor` 两种上下文
|
||||||
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
|
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
|
||||||
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露、最近活动、日志、curl 排障和 MCP 客户端配置
|
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露、最近活动、日志、curl 排障和 MCP 客户端配置
|
||||||
@@ -273,22 +275,22 @@ Funplay MCP for Cocos 延续 Funplay MCP for Unity 的设计原则,并针对 C
|
|||||||
| 内置服务 | 内嵌 HTTP MCP Server | 内嵌 HTTP MCP Server |
|
| 内置服务 | 内嵌 HTTP MCP Server | 内嵌 HTTP MCP Server |
|
||||||
| 主执行工具 | `execute_javascript` | `execute_code` |
|
| 主执行工具 | `execute_javascript` | `execute_code` |
|
||||||
| 主语言 | 场景/编辑器上下文中的 JavaScript | Unity 编辑器/运行态中的 C# |
|
| 主语言 | 场景/编辑器上下文中的 JavaScript | Unity 编辑器/运行态中的 C# |
|
||||||
| 默认工具集 | `core`,34 个工具 | 聚焦版 `core` 工具集 |
|
| 默认工具集 | `core`,37 个工具 | 聚焦版 `core` 工具集 |
|
||||||
| 完整工具集 | 89 个工具,并支持 `custom` 暴露 | 79 个工具 |
|
| 完整工具集 | 101 个工具,并支持 `custom` 暴露 | 79 个工具 |
|
||||||
| 客户端配置 | 一键配置面板 | 一键配置窗口 |
|
| 客户端配置 | 一键配置面板 | 一键配置窗口 |
|
||||||
|
|
||||||
## MCP 能力结构
|
## MCP 能力结构
|
||||||
|
|
||||||
当前包提供四层能力:
|
当前包提供四层能力:
|
||||||
|
|
||||||
- **Tools** — `core` 下 34 个工具,`full` 下 89 个工具,并支持 `custom` include/exclude 规则
|
- **Tools** — `core` 下 37 个工具,`full` 下 101 个工具,并支持 `custom` include/exclude 规则和命名工具 profile
|
||||||
- **Primary execution** — `execute_javascript` 用于场景/运行态和编辑器/browser 自动化
|
- **Primary execution** — `execute_javascript` 用于场景/运行态和编辑器/browser 自动化
|
||||||
- **Prompts** — `fix_script_errors`、`create_playable_prototype`、`scene_validation`、`auto_wire_scene`
|
- **Prompts** — `fix_script_errors`、`create_playable_prototype`、`scene_validation`、`auto_wire_scene`
|
||||||
- **Resources** — 项目上下文、场景摘要、当前选择、脚本诊断、资产选择、日志和 MCP 交互历史
|
- **Resources** — 项目上下文、场景摘要、当前选择、脚本诊断、资产选择、日志和 MCP 交互历史
|
||||||
|
|
||||||
自动生成的工具参考文档见 [docs/TOOLS.md](./docs/TOOLS.md),里面包含工具分类、profile 和读写/变更提示。
|
自动生成的工具参考文档见 [docs/TOOLS.md](./docs/TOOLS.md),里面包含工具分类、profile 和读写/变更提示。
|
||||||
|
|
||||||
当前默认 `core` 工具集刻意保持精简,只包含:`execute_javascript`、`execute_scene_script`、`execute_editor_script`、`get_editor_state`、`get_tool_catalog`、`check_for_updates`、`get_selection`、`list_project_instructions`、`read_project_instruction`、`set_selection`、`get_project_info`、`get_scene_info`、`get_hierarchy`、`list_scenes`、`open_scene`、`inspect_prefab`、`validate_prefab_references`、`inspect_prefab_instance`、`list_assets`、`inspect_asset`、`open_asset`、`select_asset`、`run_script_diagnostics`、`get_recent_logs`、`search_project_logs`、`clear_logs`、`validate_scene`、`get_performance_snapshot`、`get_script_diagnostic_context`、`get_runtime_state`、`capture_editor_screenshot`、`capture_scene_screenshot`、`capture_preview_screenshot`、`list_editor_windows`。
|
当前默认 `core` 工具集刻意保持精简,只包含:`execute_javascript`、`execute_scene_script`、`execute_editor_script`、`get_editor_state`、`get_tool_catalog`、`check_for_updates`、`get_selection`、`list_project_instructions`、`read_project_instruction`、`set_selection`、`get_project_info`、`get_build_status`、`get_scene_info`、`get_hierarchy`、`list_scenes`、`open_scene`、`inspect_prefab`、`validate_prefab_references`、`inspect_prefab_instance`、`list_assets`、`inspect_asset`、`inspect_asset_dependencies`、`validate_asset_dependencies`、`open_asset`、`select_asset`、`run_script_diagnostics`、`get_recent_logs`、`search_project_logs`、`clear_logs`、`validate_scene`、`get_performance_snapshot`、`get_script_diagnostic_context`、`get_runtime_state`、`capture_editor_screenshot`、`capture_scene_screenshot`、`capture_preview_screenshot`、`list_editor_windows`。
|
||||||
|
|
||||||
## 内置 Resources
|
## 内置 Resources
|
||||||
|
|
||||||
@@ -307,24 +309,25 @@ Funplay MCP for Cocos 延续 Funplay MCP for Unity 的设计原则,并针对 C
|
|||||||
|
|
||||||
## 内置工具
|
## 内置工具
|
||||||
|
|
||||||
Funplay MCP for Cocos 当前在 `full` profile 下提供 **89 个工具函数**:
|
Funplay MCP for Cocos 当前在 `full` profile 下提供 **101 个工具函数**:
|
||||||
|
|
||||||
| 分类 | 工具 |
|
| 分类 | 工具 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **脚本执行** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
| **脚本执行** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
||||||
| **编辑器状态** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
| **编辑器状态** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
||||||
| **项目指令** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill` |
|
| **项目指令** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill`, `create_cocos_mcp_project_skill` |
|
||||||
| **项目与场景** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
| **项目与场景** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
||||||
| **节点编辑** | `create_node`, `delete_node`, `set_node_transform` |
|
| **节点编辑** | `create_node`, `delete_node`, `set_node_transform` |
|
||||||
| **资产与 Prefab** | `list_assets`, `inspect_asset`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
| **资产与 Prefab** | `list_assets`, `inspect_asset`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
||||||
| **组件** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
| **组件** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
||||||
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
||||||
| **相机** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
| **相机** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
||||||
| **动画** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
| **动画** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
||||||
| **文件** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
| **文件** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
||||||
| **诊断与日志** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
| **诊断与日志** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
||||||
|
| **构建与编辑器** | `get_build_status`, `open_build_panel`, `run_project_preview`, `save_current_scene`, `get_editor_preference`, `set_editor_preference`, `broadcast_editor_message` |
|
||||||
| **运行态** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
| **运行态** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
||||||
| **交互** | `emit_node_event`, `simulate_button_click`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
| **交互与事件** | `emit_node_event`, `simulate_button_click`, `list_button_click_events`, `bind_button_click_event`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
||||||
| **截图与窗口** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
| **截图与窗口** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
||||||
|
|
||||||
## 主工具示例
|
## 主工具示例
|
||||||
@@ -365,8 +368,11 @@ Editor 上下文脚本可以访问 `Editor`、`fs`、`path`、`os`、`require`
|
|||||||
"enabledTools": [],
|
"enabledTools": [],
|
||||||
"disabledTools": [],
|
"disabledTools": [],
|
||||||
"enableSessions": false,
|
"enableSessions": false,
|
||||||
|
"executeJavascriptSafetyChecks": true,
|
||||||
"autostart": true,
|
"autostart": true,
|
||||||
"maxInteractionLogEntries": 50
|
"maxInteractionLogEntries": 50,
|
||||||
|
"activeToolProfileName": "",
|
||||||
|
"savedToolProfiles": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -376,7 +382,7 @@ Editor 上下文脚本可以访问 `Editor`、`fs`、`path`、`os`、`require`
|
|||||||
- `COCOS_MCP_PORT`
|
- `COCOS_MCP_PORT`
|
||||||
- `COCOS_MCP_PROFILE`
|
- `COCOS_MCP_PROFILE`
|
||||||
|
|
||||||
`toolProfile: "custom"` 会从 `core` 集合开始,再加入 `enabledToolCategories` / `enabledTools`,并移除 `disabledToolCategories` / `disabledTools`。`enableSessions` 默认关闭,因为常规编辑器自动化不需要跨请求客户端状态。
|
`toolProfile: "custom"` 会从 `core` 集合开始,再加入 `enabledToolCategories` / `enabledTools`,并移除 `disabledToolCategories` / `disabledTools`。面板可以把这些暴露设置保存为命名 `savedToolProfiles`,方便恢复或分享。`enableSessions` 默认关闭,因为常规编辑器自动化不需要跨请求客户端状态。
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
@@ -394,6 +400,12 @@ Cocos Creator Extension
|
|||||||
│ └─ Minimal MCP Server panel
|
│ └─ Minimal MCP Server panel
|
||||||
└─ lib/
|
└─ lib/
|
||||||
├─ assets, diagnostics, screenshots, input
|
├─ assets, diagnostics, screenshots, input
|
||||||
|
├─ tool-profiles, javascript-safety
|
||||||
|
├─ tools/
|
||||||
|
│ ├─ files
|
||||||
|
│ ├─ assets-advanced
|
||||||
|
│ ├─ cocos-project
|
||||||
|
│ └─ scene-events
|
||||||
└─ server, resources, prompts, tool registry
|
└─ server, resources, prompts, tool registry
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
|
|||||||
- [ ] The zip contains stdio wrapper metadata: `bin/funplay-cocos-mcp.js` and `server.json`
|
- [ ] The zip contains stdio wrapper metadata: `bin/funplay-cocos-mcp.js` and `server.json`
|
||||||
- [ ] The zip includes docs: `README.md`, `README_CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `LICENSE`
|
- [ ] The zip includes docs: `README.md`, `README_CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `LICENSE`
|
||||||
- [ ] The zip does not contain `.git/`, `.github/`, `.DS_Store`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`, `test/`, or `scripts/`
|
- [ ] The zip does not contain `.git/`, `.github/`, `.DS_Store`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`, `test/`, or `scripts/`
|
||||||
|
- [ ] Release packaging sensitive-content scan does not find npm/GitHub/MCP tokens or private keys
|
||||||
- [ ] `release-manifest.json` references the correct GitHub download URL
|
- [ ] `release-manifest.json` references the correct GitHub download URL
|
||||||
- [ ] `SHA256SUMS.txt` includes the zip, manifest, generated release notes, and release README
|
- [ ] `SHA256SUMS.txt` includes the zip, manifest, generated release notes, and release README
|
||||||
|
|
||||||
|
|||||||
+24
-1
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const manifest = require('./package.json');
|
const manifest = require('./package.json');
|
||||||
const { SERVER_NAME, buildTargets, configureTarget, getTargetStatuses } = require('./lib/client-config');
|
const { SERVER_NAME, buildTargets, configureTarget, getTargetStatuses } = require('./lib/client-config');
|
||||||
const { loadConfig, getProjectPath, getProjectName, getCocosVersion } = require('./lib/config');
|
const { loadConfig, getProjectPath, getProjectName, getProjectIdentity, getCocosVersion } = require('./lib/config');
|
||||||
const { McpServer } = require('./lib/server');
|
const { McpServer } = require('./lib/server');
|
||||||
const { createToolRegistry } = require('./lib/tool-registry');
|
const { createToolRegistry } = require('./lib/tool-registry');
|
||||||
const { ResourceProvider } = require('./lib/resources');
|
const { ResourceProvider } = require('./lib/resources');
|
||||||
@@ -13,6 +13,7 @@ const { PromptProvider } = require('./lib/prompts');
|
|||||||
const { InteractionLog } = require('./lib/interaction-log');
|
const { InteractionLog } = require('./lib/interaction-log');
|
||||||
const { RuntimeLog } = require('./lib/runtime-log');
|
const { RuntimeLog } = require('./lib/runtime-log');
|
||||||
const { checkForUpdate } = require('./lib/update-checker');
|
const { checkForUpdate } = require('./lib/update-checker');
|
||||||
|
const { normalizeSavedToolProfiles } = require('./lib/tool-profiles');
|
||||||
|
|
||||||
const EXTENSION_NAME = manifest.name || 'funplay-cocos-mcp';
|
const EXTENSION_NAME = manifest.name || 'funplay-cocos-mcp';
|
||||||
const LOG_PREFIX = '[Funplay Cocos MCP]';
|
const LOG_PREFIX = '[Funplay Cocos MCP]';
|
||||||
@@ -101,6 +102,7 @@ class ExtensionService {
|
|||||||
config: this.config,
|
config: this.config,
|
||||||
projectPath: getProjectPath(),
|
projectPath: getProjectPath(),
|
||||||
projectName: getProjectName(),
|
projectName: getProjectName(),
|
||||||
|
projectIdentity: getProjectIdentity(),
|
||||||
cocosVersion: getCocosVersion(),
|
cocosVersion: getCocosVersion(),
|
||||||
packagePath: path.dirname(__filename),
|
packagePath: path.dirname(__filename),
|
||||||
});
|
});
|
||||||
@@ -134,6 +136,8 @@ class ExtensionService {
|
|||||||
promptProvider: this.promptProvider,
|
promptProvider: this.promptProvider,
|
||||||
serverName: `Funplay Cocos MCP - ${getProjectName()}`,
|
serverName: `Funplay Cocos MCP - ${getProjectName()}`,
|
||||||
serverVersion: manifest.version || '0.0.0',
|
serverVersion: manifest.version || '0.0.0',
|
||||||
|
projectName: getProjectName(),
|
||||||
|
projectIdentity: getProjectIdentity(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.server.start();
|
await this.server.start();
|
||||||
@@ -178,8 +182,13 @@ class ExtensionService {
|
|||||||
const fallbackInfo = this.server && this.server.isRunning() && typeof this.server.getPortFallbackInfo === 'function'
|
const fallbackInfo = this.server && this.server.isRunning() && typeof this.server.getPortFallbackInfo === 'function'
|
||||||
? this.server.getPortFallbackInfo()
|
? this.server.getPortFallbackInfo()
|
||||||
: null;
|
: null;
|
||||||
|
const attachInfo = this.server && this.server.isRunning() && typeof this.server.getAttachInfo === 'function'
|
||||||
|
? this.server.getAttachInfo()
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
running: Boolean(this.server && this.server.isRunning()),
|
running: Boolean(this.server && this.server.isRunning()),
|
||||||
|
attachedToExisting: Boolean(attachInfo),
|
||||||
|
attachInfo,
|
||||||
host: this.config.host,
|
host: this.config.host,
|
||||||
port: effective.port,
|
port: effective.port,
|
||||||
requestedPort: this.config.port,
|
requestedPort: this.config.port,
|
||||||
@@ -191,10 +200,14 @@ class ExtensionService {
|
|||||||
enabledToolCategories: this.config.enabledToolCategories,
|
enabledToolCategories: this.config.enabledToolCategories,
|
||||||
disabledToolCategories: this.config.disabledToolCategories,
|
disabledToolCategories: this.config.disabledToolCategories,
|
||||||
enableSessions: this.config.enableSessions,
|
enableSessions: this.config.enableSessions,
|
||||||
|
executeJavascriptSafetyChecks: this.config.executeJavascriptSafetyChecks,
|
||||||
autostart: this.config.autostart,
|
autostart: this.config.autostart,
|
||||||
|
activeToolProfileName: this.config.activeToolProfileName,
|
||||||
|
savedToolProfiles: this.config.savedToolProfiles,
|
||||||
version: manifest.version || '0.0.0',
|
version: manifest.version || '0.0.0',
|
||||||
projectPath: getProjectPath(),
|
projectPath: getProjectPath(),
|
||||||
projectName: getProjectName(),
|
projectName: getProjectName(),
|
||||||
|
projectIdentity: getProjectIdentity(),
|
||||||
cocosVersion: getCocosVersion(),
|
cocosVersion: getCocosVersion(),
|
||||||
url: effective.url,
|
url: effective.url,
|
||||||
};
|
};
|
||||||
@@ -416,6 +429,9 @@ class ExtensionService {
|
|||||||
enableSessions: partialConfig && typeof partialConfig.enableSessions === 'boolean'
|
enableSessions: partialConfig && typeof partialConfig.enableSessions === 'boolean'
|
||||||
? partialConfig.enableSessions
|
? partialConfig.enableSessions
|
||||||
: this.config.enableSessions,
|
: this.config.enableSessions,
|
||||||
|
executeJavascriptSafetyChecks: partialConfig && typeof partialConfig.executeJavascriptSafetyChecks === 'boolean'
|
||||||
|
? partialConfig.executeJavascriptSafetyChecks
|
||||||
|
: this.config.executeJavascriptSafetyChecks,
|
||||||
autostart: partialConfig && typeof partialConfig.autostart === 'boolean'
|
autostart: partialConfig && typeof partialConfig.autostart === 'boolean'
|
||||||
? partialConfig.autostart
|
? partialConfig.autostart
|
||||||
: this.config.autostart,
|
: this.config.autostart,
|
||||||
@@ -425,6 +441,12 @@ class ExtensionService {
|
|||||||
lastClientTargetId: partialConfig && partialConfig.lastClientTargetId
|
lastClientTargetId: partialConfig && partialConfig.lastClientTargetId
|
||||||
? String(partialConfig.lastClientTargetId)
|
? String(partialConfig.lastClientTargetId)
|
||||||
: this.config.lastClientTargetId,
|
: this.config.lastClientTargetId,
|
||||||
|
activeToolProfileName: partialConfig && typeof partialConfig.activeToolProfileName === 'string'
|
||||||
|
? String(partialConfig.activeToolProfileName)
|
||||||
|
: this.config.activeToolProfileName,
|
||||||
|
savedToolProfiles: partialConfig && Array.isArray(partialConfig.savedToolProfiles)
|
||||||
|
? normalizeSavedToolProfiles(partialConfig.savedToolProfiles)
|
||||||
|
: this.config.savedToolProfiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
const configPath = this.config.configPath;
|
const configPath = this.config.configPath;
|
||||||
@@ -435,6 +457,7 @@ class ExtensionService {
|
|||||||
nextConfig.port !== this.config.port ||
|
nextConfig.port !== this.config.port ||
|
||||||
nextConfig.toolProfile !== this.config.toolProfile ||
|
nextConfig.toolProfile !== this.config.toolProfile ||
|
||||||
nextConfig.enableSessions !== this.config.enableSessions ||
|
nextConfig.enableSessions !== this.config.enableSessions ||
|
||||||
|
nextConfig.executeJavascriptSafetyChecks !== this.config.executeJavascriptSafetyChecks ||
|
||||||
JSON.stringify(nextConfig.enabledTools) !== JSON.stringify(this.config.enabledTools) ||
|
JSON.stringify(nextConfig.enabledTools) !== JSON.stringify(this.config.enabledTools) ||
|
||||||
JSON.stringify(nextConfig.disabledTools) !== JSON.stringify(this.config.disabledTools) ||
|
JSON.stringify(nextConfig.disabledTools) !== JSON.stringify(this.config.disabledTools) ||
|
||||||
JSON.stringify(nextConfig.enabledToolCategories) !== JSON.stringify(this.config.enabledToolCategories) ||
|
JSON.stringify(nextConfig.enabledToolCategories) !== JSON.stringify(this.config.enabledToolCategories) ||
|
||||||
|
|||||||
+38
-6
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
<!-- This file is generated by `npm run docs:generate`. Do not edit by hand. -->
|
<!-- This file is generated by `npm run docs:generate`. Do not edit by hand. -->
|
||||||
|
|
||||||
Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 tools; the `full` profile exposes 89 tools.
|
Generated from `lib/tool-registry.js`. The default `core` profile exposes 37 tools; the `full` profile exposes 101 tools.
|
||||||
|
|
||||||
## Profile Summary
|
## Profile Summary
|
||||||
|
|
||||||
| Profile | Tool Count | Purpose |
|
| Profile | Tool Count | Purpose |
|
||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
| `core` | 34 | Focused default surface for common editor automation. |
|
| `core` | 37 | Focused default surface for common editor automation. |
|
||||||
| `full` | 89 | All built-in tools, including destructive and low-level helpers. |
|
| `full` | 101 | All built-in tools, including destructive and low-level helpers. |
|
||||||
|
|
||||||
## Core Tools
|
## Core Tools
|
||||||
|
|
||||||
`capture_editor_screenshot`, `capture_preview_screenshot`, `capture_scene_screenshot`, `check_for_updates`, `clear_logs`, `execute_editor_script`, `execute_javascript`, `execute_scene_script`, `get_editor_state`, `get_hierarchy`, `get_performance_snapshot`, `get_project_info`, `get_recent_logs`, `get_runtime_state`, `get_scene_info`, `get_script_diagnostic_context`, `get_selection`, `get_tool_catalog`, `inspect_asset`, `inspect_prefab`, `inspect_prefab_instance`, `list_assets`, `list_editor_windows`, `list_project_instructions`, `list_scenes`, `open_asset`, `open_scene`, `read_project_instruction`, `run_script_diagnostics`, `search_project_logs`, `select_asset`, `set_selection`, `validate_prefab_references`, `validate_scene`
|
`capture_editor_screenshot`, `capture_preview_screenshot`, `capture_scene_screenshot`, `check_for_updates`, `clear_logs`, `execute_editor_script`, `execute_javascript`, `execute_scene_script`, `get_build_status`, `get_editor_state`, `get_hierarchy`, `get_performance_snapshot`, `get_project_info`, `get_recent_logs`, `get_runtime_state`, `get_scene_info`, `get_script_diagnostic_context`, `get_selection`, `get_tool_catalog`, `inspect_asset`, `inspect_asset_dependencies`, `inspect_prefab`, `inspect_prefab_instance`, `list_assets`, `list_editor_windows`, `list_project_instructions`, `list_scenes`, `open_asset`, `open_scene`, `read_project_instruction`, `run_script_diagnostics`, `search_project_logs`, `select_asset`, `set_selection`, `validate_asset_dependencies`, `validate_prefab_references`, `validate_scene`
|
||||||
|
|
||||||
## Tools By Category
|
## Tools By Category
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `delete_asset` | `full` | mutating | Delete an asset from asset-db by uuid, db url, or path. |
|
| `delete_asset` | `full` | mutating | Delete an asset from asset-db by uuid, db url, or path. |
|
||||||
| `inspect_asset` | `core`, `full` | read-only | [specialist] Inspect asset-db info, metadata, and serialized asset data by uuid or path. Prefer this when you need a precise structured asset read. |
|
| `inspect_asset` | `core`, `full` | read-only | [specialist] Inspect asset-db info, metadata, and serialized asset data by uuid or path. Prefer this when you need a precise structured asset read. |
|
||||||
|
| `inspect_asset_dependencies` | `core`, `full` | read-only | [specialist] Inspect UUID-style dependencies referenced by a serialized Cocos asset. |
|
||||||
| `list_assets` | `core`, `full` | read-only | [specialist] Query project assets from asset-db by pattern or asset type. Prefer this when you need exact asset discovery; otherwise use execute_javascript for broader automation. |
|
| `list_assets` | `core`, `full` | read-only | [specialist] Query project assets from asset-db by pattern or asset type. Prefer this when you need exact asset discovery; otherwise use execute_javascript for broader automation. |
|
||||||
| `list_scenes` | `core`, `full` | read-only | [specialist] List scene assets in the project. Prefer this when you need exact scene discovery before opening one; otherwise stay in execute_javascript for broader workflows. |
|
| `list_scenes` | `core`, `full` | read-only | [specialist] List scene assets in the project. Prefer this when you need exact scene discovery before opening one; otherwise stay in execute_javascript for broader workflows. |
|
||||||
| `open_asset` | `core`, `full` | stateful | [specialist] Open an asset inside Cocos Creator by uuid, db url, or path. Use this only when opening the asset itself is the explicit next step. |
|
| `open_asset` | `core`, `full` | stateful | [specialist] Open an asset inside Cocos Creator by uuid, db url, or path. Use this only when opening the asset itself is the explicit next step. |
|
||||||
@@ -39,6 +40,21 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
|
|||||||
| `run_scene_asset` | `full` | mutating | Load a scene asset by uuid directly into the current runtime scene context. |
|
| `run_scene_asset` | `full` | mutating | Load a scene asset by uuid directly into the current runtime scene context. |
|
||||||
| `select_asset` | `core`, `full` | stateful | [specialist] Select an asset in the Cocos editor. Use this when editor selection state matters; otherwise keep execute_javascript as the primary workflow. |
|
| `select_asset` | `core`, `full` | stateful | [specialist] Select an asset in the Cocos editor. Use this when editor selection state matters; otherwise keep execute_javascript as the primary workflow. |
|
||||||
|
|
||||||
|
### Broadcast
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `broadcast_editor_message` | `full` | stateful | [core] Send or broadcast a Cocos editor message for advanced editor automation. |
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_build_status` | `core`, `full` | read-only | [specialist] Query Cocos build/preview status using known builder message variants. |
|
||||||
|
| `open_build_panel` | `full` | stateful | [core] Open the Cocos build panel, defaulting to the builder panel id. |
|
||||||
|
| `run_project_preview` | `full` | stateful | [core] Start Cocos preview/run using known preview and builder message variants. |
|
||||||
|
| `save_current_scene` | `full` | stateful | [core] Save the currently open Cocos scene using available editor scene messages. |
|
||||||
|
|
||||||
### Camera
|
### Camera
|
||||||
|
|
||||||
| Tool | Profiles | Access | Description |
|
| Tool | Profiles | Access | Description |
|
||||||
@@ -65,9 +81,19 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `get_script_diagnostic_context` | `core`, `full` | read-only | [specialist] Run TypeScript diagnostics and attach source snippets for each error. This is a preferred specialist tool for compile-error triage before repair. |
|
| `get_script_diagnostic_context` | `core`, `full` | read-only | [specialist] Run TypeScript diagnostics and attach source snippets for each error. This is a preferred specialist tool for compile-error triage before repair. |
|
||||||
| `run_script_diagnostics` | `core`, `full` | stateful | [specialist] Run a TypeScript no-emit check for the current Cocos project and return parsed diagnostics. This is a preferred specialist tool for script errors when diagnostics are needed. |
|
| `run_script_diagnostics` | `core`, `full` | stateful | [specialist] Run a TypeScript no-emit check for the current Cocos project and return parsed diagnostics. This is a preferred specialist tool for script errors when diagnostics are needed. |
|
||||||
|
| `validate_asset_dependencies` | `core`, `full` | read-only | [specialist] Validate UUID-style dependencies for one asset or a project asset query. |
|
||||||
| `validate_prefab_references` | `core`, `full` | read-only | [specialist] Validate prefab asset references by checking serialized UUID references against asset-db. |
|
| `validate_prefab_references` | `core`, `full` | read-only | [specialist] Validate prefab asset references by checking serialized UUID references against asset-db. |
|
||||||
| `validate_scene` | `core`, `full` | read-only | [specialist] Run a compact validation pass over the active scene, runtime state, TypeScript diagnostics, and recent project log errors. |
|
| `validate_scene` | `core`, `full` | read-only | [specialist] Run a compact validation pass over the active scene, runtime state, TypeScript diagnostics, and recent project log errors. |
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `bind_button_click_event` | `full` | stateful | [core] Bind a Cocos Button click event to a target node component method. |
|
||||||
|
| `emit_node_event` | `full` | mutating | [core] Emit a custom event on a target scene node with an optional JSON payload. |
|
||||||
|
| `list_button_click_events` | `full` | read-only | [core] List click event bindings on a Cocos Button component. |
|
||||||
|
| `simulate_button_click` | `full` | mutating | [core] Simulate a Cocos Button click by emitting click events on the target button node. |
|
||||||
|
|
||||||
### Execution
|
### Execution
|
||||||
|
|
||||||
| Tool | Profiles | Access | Description |
|
| Tool | Profiles | Access | Description |
|
||||||
@@ -92,7 +118,6 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
|
|||||||
|
|
||||||
| Tool | Profiles | Access | Description |
|
| Tool | Profiles | Access | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `simulate_button_click` | `full` | mutating | [core] Simulate a Cocos Button click by emitting click events on the target button node. |
|
|
||||||
| `simulate_key_combo` | `full` | mutating | [core] Send a low-level Electron modified key press such as Ctrl+S or Cmd+P. |
|
| `simulate_key_combo` | `full` | mutating | [core] Send a low-level Electron modified key press such as Ctrl+S or Cmd+P. |
|
||||||
| `simulate_key_press` | `full` | mutating | [core] Send a low-level Electron key press to the editor, preview, or simulator window. |
|
| `simulate_key_press` | `full` | mutating | [core] Send a low-level Electron key press to the editor, preview, or simulator window. |
|
||||||
| `simulate_mouse_click` | `full` | mutating | [core] Send a low-level Electron mouse click to the editor, preview, or simulator window. |
|
| `simulate_mouse_click` | `full` | mutating | [core] Send a low-level Electron mouse click to the editor, preview, or simulator window. |
|
||||||
@@ -103,6 +128,7 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
|
|||||||
|
|
||||||
| Tool | Profiles | Access | Description |
|
| Tool | Profiles | Access | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| `create_cocos_mcp_project_skill` | `full` | stateful | [core] Create a recommended local Codex project skill for Funplay Cocos MCP workflows. |
|
||||||
| `create_project_skill` | `full` | stateful | [core] Create a local Codex project skill under .codex/skills/{skillName}/SKILL.md. |
|
| `create_project_skill` | `full` | stateful | [core] Create a local Codex project skill under .codex/skills/{skillName}/SKILL.md. |
|
||||||
| `list_project_instructions` | `core`, `full` | read-only | [specialist] List project AI instruction files and local Codex project skills. |
|
| `list_project_instructions` | `core`, `full` | read-only | [specialist] List project AI instruction files and local Codex project skills. |
|
||||||
| `read_project_instruction` | `core`, `full` | read-only | [specialist] Read a project AI instruction file such as AGENTS.md, CLAUDE.md, or a .codex skill SKILL.md. |
|
| `read_project_instruction` | `core`, `full` | read-only | [specialist] Read a project AI instruction file such as AGENTS.md, CLAUDE.md, or a .codex skill SKILL.md. |
|
||||||
@@ -137,6 +163,13 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
|
|||||||
| `list_prefabs` | `full` | read-only | [core] List prefab assets in the project. |
|
| `list_prefabs` | `full` | read-only | [core] List prefab assets in the project. |
|
||||||
| `revert_prefab_instance` | `full` | stateful | [core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages. |
|
| `revert_prefab_instance` | `full` | stateful | [core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages. |
|
||||||
|
|
||||||
|
### Preferences
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_editor_preference` | `full` | read-only | [core] Read a Cocos editor preference through Editor.Profile when available. |
|
||||||
|
| `set_editor_preference` | `full` | mutating | [core] Write a Cocos editor preference through Editor.Profile when available. |
|
||||||
|
|
||||||
### Project
|
### Project
|
||||||
|
|
||||||
| Tool | Profiles | Access | Description |
|
| Tool | Profiles | Access | Description |
|
||||||
@@ -149,7 +182,6 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
|
|||||||
|
|
||||||
| Tool | Profiles | Access | Description |
|
| Tool | Profiles | Access | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `emit_node_event` | `full` | mutating | [core] Emit a custom event on a target scene node with an optional JSON payload. |
|
|
||||||
| `get_runtime_state` | `core`, `full` | read-only | [specialist] Return structured Cocos runtime state including pause state, frame count, and scheduler time scale. Prefer this when you want a compact validation snapshot. |
|
| `get_runtime_state` | `core`, `full` | read-only | [specialist] Return structured Cocos runtime state including pause state, frame count, and scheduler time scale. Prefer this when you want a compact validation snapshot. |
|
||||||
| `pause_runtime` | `full` | stateful | [core] Pause Cocos director game logic execution. |
|
| `pause_runtime` | `full` | stateful | [core] Pause Cocos director game logic execution. |
|
||||||
| `resume_runtime` | `full` | stateful | [core] Resume Cocos director game logic execution. |
|
| `resume_runtime` | `full` | stateful | [core] Resume Cocos director game logic execution. |
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { normalizeSavedToolProfiles } = require('./tool-profiles');
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
@@ -12,9 +14,12 @@ const DEFAULTS = {
|
|||||||
enabledToolCategories: [],
|
enabledToolCategories: [],
|
||||||
disabledToolCategories: [],
|
disabledToolCategories: [],
|
||||||
enableSessions: false,
|
enableSessions: false,
|
||||||
|
executeJavascriptSafetyChecks: true,
|
||||||
autostart: true,
|
autostart: true,
|
||||||
maxInteractionLogEntries: 50,
|
maxInteractionLogEntries: 50,
|
||||||
lastClientTargetId: 'claude_code',
|
lastClientTargetId: 'claude_code',
|
||||||
|
activeToolProfileName: '',
|
||||||
|
savedToolProfiles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProjectPath() {
|
function getProjectPath() {
|
||||||
@@ -28,6 +33,19 @@ function getProjectName() {
|
|||||||
return path.basename(getProjectPath());
|
return path.basename(getProjectPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProjectIdentityPath(projectPath) {
|
||||||
|
const normalized = path.resolve(String(projectPath || process.cwd())).replace(/\\/g, '/');
|
||||||
|
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectIdentity(projectPath = getProjectPath()) {
|
||||||
|
return crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(`funplay-cocos-mcp:${normalizeProjectIdentityPath(projectPath)}`)
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, 24);
|
||||||
|
}
|
||||||
|
|
||||||
function getCocosVersion() {
|
function getCocosVersion() {
|
||||||
if (global.Editor && Editor.App) {
|
if (global.Editor && Editor.App) {
|
||||||
if (typeof Editor.App.version === 'string' && Editor.App.version) {
|
if (typeof Editor.App.version === 'string' && Editor.App.version) {
|
||||||
@@ -106,11 +124,16 @@ function loadConfig() {
|
|||||||
enabledToolCategories: normalizeStringList(fileConfig.enabledToolCategories).map((item) => item.toLowerCase()),
|
enabledToolCategories: normalizeStringList(fileConfig.enabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
disabledToolCategories: normalizeStringList(fileConfig.disabledToolCategories).map((item) => item.toLowerCase()),
|
disabledToolCategories: normalizeStringList(fileConfig.disabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
enableSessions: typeof fileConfig.enableSessions === 'boolean' ? fileConfig.enableSessions : DEFAULTS.enableSessions,
|
enableSessions: typeof fileConfig.enableSessions === 'boolean' ? fileConfig.enableSessions : DEFAULTS.enableSessions,
|
||||||
|
executeJavascriptSafetyChecks: typeof fileConfig.executeJavascriptSafetyChecks === 'boolean'
|
||||||
|
? fileConfig.executeJavascriptSafetyChecks
|
||||||
|
: DEFAULTS.executeJavascriptSafetyChecks,
|
||||||
autostart: typeof fileConfig.autostart === 'boolean' ? fileConfig.autostart : DEFAULTS.autostart,
|
autostart: typeof fileConfig.autostart === 'boolean' ? fileConfig.autostart : DEFAULTS.autostart,
|
||||||
maxInteractionLogEntries: Number.isInteger(fileConfig.maxInteractionLogEntries)
|
maxInteractionLogEntries: Number.isInteger(fileConfig.maxInteractionLogEntries)
|
||||||
? Math.max(10, Math.min(500, fileConfig.maxInteractionLogEntries))
|
? Math.max(10, Math.min(500, fileConfig.maxInteractionLogEntries))
|
||||||
: DEFAULTS.maxInteractionLogEntries,
|
: DEFAULTS.maxInteractionLogEntries,
|
||||||
lastClientTargetId: normalizeClientTargetId(fileConfig.lastClientTargetId),
|
lastClientTargetId: normalizeClientTargetId(fileConfig.lastClientTargetId),
|
||||||
|
activeToolProfileName: typeof fileConfig.activeToolProfileName === 'string' ? fileConfig.activeToolProfileName : '',
|
||||||
|
savedToolProfiles: normalizeSavedToolProfiles(fileConfig.savedToolProfiles),
|
||||||
configPath,
|
configPath,
|
||||||
configError: fileConfig.__error || '',
|
configError: fileConfig.__error || '',
|
||||||
};
|
};
|
||||||
@@ -120,6 +143,7 @@ module.exports = {
|
|||||||
DEFAULTS,
|
DEFAULTS,
|
||||||
getProjectPath,
|
getProjectPath,
|
||||||
getProjectName,
|
getProjectName,
|
||||||
|
getProjectIdentity,
|
||||||
getCocosVersion,
|
getCocosVersion,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
normalizeProfile,
|
normalizeProfile,
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { isPathInside } = require('./path-safety');
|
||||||
|
|
||||||
|
const DELETE_METHOD_PATTERN = /\bfs(?:\s*\.\s*promises)?\s*\.\s*(rm|rmdir|unlink|truncate|rmSync|rmdirSync|unlinkSync|truncateSync)\s*\(/;
|
||||||
|
const WRITE_STREAM_PATTERN = /\bfs\s*\.\s*(createWriteStream|openSync)\s*\(/;
|
||||||
|
const SHELL_PATTERN = /require\s*\(\s*['"]child_process['"]\s*\)|\bchild_process\s*\.|\b(exec|execFile|spawn|fork|execSync|execFileSync|spawnSync)\s*\(/;
|
||||||
|
const WRITE_METHOD_PATTERN = /\bfs(?:\s*\.\s*promises)?\s*\.\s*(writeFile|appendFile|copyFile|cp|rename|mkdir|writeFileSync|appendFileSync|copyFileSync|cpSync|renameSync|mkdirSync)\s*\(/;
|
||||||
|
const HOME_PATH_PATTERN = /(?:^~(?:\/|\\|$)|\$HOME|%USERPROFILE%|%HOMEPATH%)/i;
|
||||||
|
const TRAVERSAL_PATTERN = /(^|[\\/])\.\.([\\/]|$)/;
|
||||||
|
|
||||||
|
function extractStringLiterals(code) {
|
||||||
|
const literals = [];
|
||||||
|
const pattern = /(['"`])((?:\\[\s\S]|(?!\1)[\s\S])*?)\1/g;
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(String(code || '')))) {
|
||||||
|
literals.push(match[2]);
|
||||||
|
}
|
||||||
|
return literals;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteLiteral(value) {
|
||||||
|
return path.isAbsolute(value)
|
||||||
|
|| path.win32.isAbsolute(value)
|
||||||
|
|| /^\\\\/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteLiteralInsideProject(projectPath, value) {
|
||||||
|
if (!projectPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.win32.isAbsolute(value)) {
|
||||||
|
const root = projectPath.replace(/\//g, '\\');
|
||||||
|
const relative = path.win32.relative(root, value);
|
||||||
|
return relative === '' || (relative && !relative.startsWith('..') && !path.win32.isAbsolute(relative));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.isAbsolute(value)) {
|
||||||
|
return isPathInside(projectPath, path.resolve(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectJavascriptSafety(code, options = {}) {
|
||||||
|
const source = String(code || '');
|
||||||
|
const projectPath = options.projectPath ? path.resolve(String(options.projectPath)) : '';
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
if (DELETE_METHOD_PATTERN.test(source)) {
|
||||||
|
violations.push('direct fs delete/truncate calls are blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WRITE_STREAM_PATTERN.test(source)) {
|
||||||
|
violations.push('raw writable file streams are blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SHELL_PATTERN.test(source)) {
|
||||||
|
violations.push('child_process execution is blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFileMutation = DELETE_METHOD_PATTERN.test(source)
|
||||||
|
|| WRITE_METHOD_PATTERN.test(source)
|
||||||
|
|| WRITE_STREAM_PATTERN.test(source);
|
||||||
|
if (hasFileMutation && /\bos\s*\.\s*homedir\s*\(/.test(source)) {
|
||||||
|
violations.push('file mutations derived from os.homedir() are blocked by default');
|
||||||
|
}
|
||||||
|
if (hasFileMutation && /\bprocess\s*\.\s*env\s*\.\s*(HOME|USERPROFILE|HOMEPATH|APPDATA|LOCALAPPDATA|TMP|TEMP)\b/.test(source)) {
|
||||||
|
violations.push('file mutations derived from user/system environment paths are blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const literal of extractStringLiterals(source)) {
|
||||||
|
if (HOME_PATH_PATTERN.test(literal)) {
|
||||||
|
violations.push(`user-home path literal is blocked: ${literal}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TRAVERSAL_PATTERN.test(literal)) {
|
||||||
|
violations.push(`path traversal literal is blocked: ${literal}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAbsoluteLiteral(literal)) {
|
||||||
|
if (!isAbsoluteLiteralInsideProject(projectPath, literal)) {
|
||||||
|
violations.push(`absolute path outside the Cocos project is blocked: ${literal}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: violations.length === 0,
|
||||||
|
violations: Array.from(new Set(violations)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertJavascriptSafety(code, options = {}) {
|
||||||
|
const result = inspectJavascriptSafety(code, options);
|
||||||
|
if (result.ok) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'JavaScript safety checks blocked this code: ' +
|
||||||
|
`${result.violations.join('; ')}. ` +
|
||||||
|
'Use project-relative helper/file tools, or pass safety_checks=false only after reviewing the risk.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
assertJavascriptSafety,
|
||||||
|
inspectJavascriptSafety,
|
||||||
|
};
|
||||||
@@ -146,8 +146,26 @@ function createProjectSkill(projectPath, options = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCocosMcpProjectSkill(projectPath, options = {}) {
|
||||||
|
return createProjectSkill(projectPath, {
|
||||||
|
skillName: options.skillName || 'funplay-cocos-mcp-workflow',
|
||||||
|
title: options.title || 'Funplay Cocos MCP Workflow',
|
||||||
|
description: options.description || 'Use this skill when editing, validating, or debugging this Cocos Creator project through Funplay Cocos MCP.',
|
||||||
|
overwrite: options.overwrite !== false,
|
||||||
|
instructions: String(options.instructions || '').trim() || [
|
||||||
|
'- Start by reading `cocos://project/context` or calling `get_editor_state` to confirm the active project, scene, server URL, and tool profile.',
|
||||||
|
'- Prefer `execute_javascript` for high-level scene/editor orchestration, but keep safety checks enabled unless the code was reviewed.',
|
||||||
|
'- Use focused tools when they are better primitives: `list_assets`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `run_script_diagnostics`, `get_script_diagnostic_context`, and screenshot tools.',
|
||||||
|
'- For UI work, inspect the active Canvas/hierarchy first, mutate the smallest necessary node/component set, then verify with `validate_scene` and a screenshot.',
|
||||||
|
'- For prefab or asset edits, inspect dependencies/references before mutation and refresh assets afterward.',
|
||||||
|
'- When changing tool exposure, save a named tool profile so the same client setup can be restored later.',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
KNOWN_INSTRUCTION_PATHS,
|
KNOWN_INSTRUCTION_PATHS,
|
||||||
|
createCocosMcpProjectSkill,
|
||||||
createProjectSkill,
|
createProjectSkill,
|
||||||
listProjectInstructions,
|
listProjectInstructions,
|
||||||
readProjectInstruction,
|
readProjectInstruction,
|
||||||
|
|||||||
+135
-3
@@ -84,7 +84,11 @@ class McpServer {
|
|||||||
this.runtimeLog = options.runtimeLog;
|
this.runtimeLog = options.runtimeLog;
|
||||||
this.serverName = options.serverName;
|
this.serverName = options.serverName;
|
||||||
this.serverVersion = options.serverVersion;
|
this.serverVersion = options.serverVersion;
|
||||||
|
this.projectName = options.projectName || '';
|
||||||
|
this.projectIdentity = options.projectIdentity || '';
|
||||||
this.server = null;
|
this.server = null;
|
||||||
|
this.attached = false;
|
||||||
|
this.attachedInfo = null;
|
||||||
this.actualPort = null;
|
this.actualPort = null;
|
||||||
this.portFallbackInfo = null;
|
this.portFallbackInfo = null;
|
||||||
this.negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
|
this.negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
|
||||||
@@ -93,10 +97,13 @@ class McpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isRunning() {
|
isRunning() {
|
||||||
return Boolean(this.server && this.server.listening);
|
return Boolean(this.attached || (this.server && this.server.listening));
|
||||||
}
|
}
|
||||||
|
|
||||||
getPort() {
|
getPort() {
|
||||||
|
if (this.attached && this.actualPort) {
|
||||||
|
return this.actualPort;
|
||||||
|
}
|
||||||
if (this.server && typeof this.server.address === 'function') {
|
if (this.server && typeof this.server.address === 'function') {
|
||||||
const address = this.server.address();
|
const address = this.server.address();
|
||||||
if (address && typeof address.port === 'number') {
|
if (address && typeof address.port === 'number') {
|
||||||
@@ -114,6 +121,10 @@ class McpServer {
|
|||||||
return this.portFallbackInfo;
|
return this.portFallbackInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAttachInfo() {
|
||||||
|
return this.attachedInfo;
|
||||||
|
}
|
||||||
|
|
||||||
log(level, message) {
|
log(level, message) {
|
||||||
if (this.runtimeLog && typeof this.runtimeLog.add === 'function') {
|
if (this.runtimeLog && typeof this.runtimeLog.add === 'function') {
|
||||||
this.runtimeLog.add(level, message);
|
this.runtimeLog.add(level, message);
|
||||||
@@ -137,13 +148,21 @@ class McpServer {
|
|||||||
|
|
||||||
this.actualPort = null;
|
this.actualPort = null;
|
||||||
this.portFallbackInfo = null;
|
this.portFallbackInfo = null;
|
||||||
|
this.attached = false;
|
||||||
|
this.attachedInfo = null;
|
||||||
|
|
||||||
const requestHandler = async (request, response) => {
|
const requestHandler = async (request, response) => {
|
||||||
try {
|
try {
|
||||||
const requestUrl = new URL(request.url || '/', 'http://localhost');
|
const requestUrl = new URL(request.url || '/', 'http://localhost');
|
||||||
if (request.method === 'GET' && requestUrl.pathname === '/health') {
|
if (request.method === 'GET' && requestUrl.pathname === '/health') {
|
||||||
this.log('info', 'GET /health');
|
this.log('info', 'GET /health');
|
||||||
return json(response, 200, { ok: true, name: this.serverName, version: this.serverVersion }, this.negotiatedProtocolVersion);
|
return json(response, 200, {
|
||||||
|
ok: true,
|
||||||
|
name: this.serverName,
|
||||||
|
version: this.serverVersion,
|
||||||
|
projectName: this.projectName,
|
||||||
|
projectIdentity: this.projectIdentity,
|
||||||
|
}, this.negotiatedProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.method === 'GET' && requestUrl.pathname === '/tools') {
|
if (request.method === 'GET' && requestUrl.pathname === '/tools') {
|
||||||
@@ -288,7 +307,12 @@ class McpServer {
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
candidate.removeAllListeners();
|
||||||
if (error && error.code === 'EADDRINUSE' && port < 65535 && attempt < MAX_PORT_FALLBACK_ATTEMPTS) {
|
if (error && error.code === 'EADDRINUSE' && port < 65535 && attempt < MAX_PORT_FALLBACK_ATTEMPTS) {
|
||||||
|
if (await this.tryAttachToExisting(port)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nextPort = port + 1;
|
const nextPort = port + 1;
|
||||||
this.log(
|
this.log(
|
||||||
'warn',
|
'warn',
|
||||||
@@ -299,7 +323,6 @@ class McpServer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate.removeAllListeners();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +334,15 @@ class McpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
|
if (this.attached) {
|
||||||
|
this.log('info', `Detached from existing MCP listener on ${this.config.host}:${this.actualPort}.`);
|
||||||
|
this.attached = false;
|
||||||
|
this.attachedInfo = null;
|
||||||
|
this.actualPort = null;
|
||||||
|
this.portFallbackInfo = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.server) {
|
if (!this.server) {
|
||||||
this.log('info', 'Stop skipped: server object is empty.');
|
this.log('info', 'Stop skipped: server object is empty.');
|
||||||
return;
|
return;
|
||||||
@@ -321,6 +353,7 @@ class McpServer {
|
|||||||
this.server = null;
|
this.server = null;
|
||||||
this.actualPort = null;
|
this.actualPort = null;
|
||||||
this.portFallbackInfo = null;
|
this.portFallbackInfo = null;
|
||||||
|
this.attachedInfo = null;
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
active.close((error) => {
|
active.close((error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -334,6 +367,100 @@ class McpServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tryAttachToExisting(port) {
|
||||||
|
if (!this.projectIdentity || this.config.attachToExisting === false || port === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const probe = await this.probeExistingServer(port);
|
||||||
|
if (!probe || !probe.result) {
|
||||||
|
this.log('warn', `Port ${port} is occupied, but no compatible Funplay MCP initialize response was received.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = probe.result || {};
|
||||||
|
const serverInfo = result.serverInfo || {};
|
||||||
|
const funplay = result.funplay || {};
|
||||||
|
const remoteProjectIdentity = funplay.projectIdentity || serverInfo.projectIdentity || '';
|
||||||
|
const remoteName = serverInfo.name || '';
|
||||||
|
|
||||||
|
if (remoteName === this.serverName && remoteProjectIdentity === this.projectIdentity) {
|
||||||
|
this.attached = true;
|
||||||
|
this.attachedInfo = {
|
||||||
|
host: this.config.host,
|
||||||
|
port,
|
||||||
|
serverName: remoteName,
|
||||||
|
projectName: funplay.projectName || this.projectName,
|
||||||
|
projectIdentity: remoteProjectIdentity,
|
||||||
|
version: serverInfo.version || '',
|
||||||
|
};
|
||||||
|
this.actualPort = port;
|
||||||
|
this.portFallbackInfo = null;
|
||||||
|
this.log('info', `Attached to existing MCP listener for this project at http://${this.config.host}:${port}/.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
'warn',
|
||||||
|
`Port ${port} belongs to another listener; expected name=${this.serverName}, project=${this.projectIdentity}, ` +
|
||||||
|
`got name=${remoteName || 'unknown'}, project=${remoteProjectIdentity || 'unknown'}.`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
probeExistingServer(port) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 'funplay-probe',
|
||||||
|
method: 'initialize',
|
||||||
|
params: {
|
||||||
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||||
|
clientInfo: {
|
||||||
|
name: 'funplay-cocos-mcp-probe',
|
||||||
|
version: this.serverVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const request = http.request(
|
||||||
|
{
|
||||||
|
host: this.config.host,
|
||||||
|
port,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/',
|
||||||
|
timeout: 600,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, text/event-stream',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
const chunks = [];
|
||||||
|
response.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
response.on('end', () => {
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
request.on('timeout', () => {
|
||||||
|
request.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
request.on('error', () => resolve(null));
|
||||||
|
request.end(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
listen(server, port, host) {
|
listen(server, port, host) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const onError = (error) => {
|
const onError = (error) => {
|
||||||
@@ -559,6 +686,11 @@ class McpServer {
|
|||||||
name: this.serverName,
|
name: this.serverName,
|
||||||
version: this.serverVersion,
|
version: this.serverVersion,
|
||||||
},
|
},
|
||||||
|
funplay: {
|
||||||
|
server: 'funplay-cocos-mcp',
|
||||||
|
projectName: this.projectName,
|
||||||
|
projectIdentity: this.projectIdentity,
|
||||||
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
resources: {},
|
resources: {},
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PROFILE_FIELDS = [
|
||||||
|
'toolProfile',
|
||||||
|
'enabledToolCategories',
|
||||||
|
'disabledToolCategories',
|
||||||
|
'enabledTools',
|
||||||
|
'disabledTools',
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeProfileName(value) {
|
||||||
|
const normalized = String(value || '').trim();
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error('profile name is required.');
|
||||||
|
}
|
||||||
|
return normalized.slice(0, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStringList(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.split(/[\n,]/).map((item) => item.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProfileMode(value) {
|
||||||
|
const normalized = String(value || 'core').trim().toLowerCase();
|
||||||
|
return normalized === 'full' || normalized === 'custom' ? normalized : 'core';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToolProfile(value) {
|
||||||
|
const profile = value || {};
|
||||||
|
return {
|
||||||
|
name: normalizeProfileName(profile.name),
|
||||||
|
toolProfile: normalizeProfileMode(profile.toolProfile),
|
||||||
|
enabledToolCategories: normalizeStringList(profile.enabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
disabledToolCategories: normalizeStringList(profile.disabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
enabledTools: normalizeStringList(profile.enabledTools),
|
||||||
|
disabledTools: normalizeStringList(profile.disabledTools),
|
||||||
|
updatedAt: profile.updatedAt ? String(profile.updatedAt) : new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSavedToolProfiles(value) {
|
||||||
|
const profiles = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const item of Array.isArray(value) ? value : []) {
|
||||||
|
try {
|
||||||
|
const profile = normalizeToolProfile(item);
|
||||||
|
const key = profile.name.toLowerCase();
|
||||||
|
if (seen.has(key)) {
|
||||||
|
const index = profiles.findIndex((existing) => existing.name.toLowerCase() === key);
|
||||||
|
profiles[index] = profile;
|
||||||
|
} else {
|
||||||
|
seen.add(key);
|
||||||
|
profiles.push(profile);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore malformed saved profile entries rather than breaking extension startup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profiles.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToolProfileSnapshot(config = {}, name) {
|
||||||
|
return normalizeToolProfile({
|
||||||
|
name,
|
||||||
|
toolProfile: config.toolProfile,
|
||||||
|
enabledToolCategories: config.enabledToolCategories,
|
||||||
|
disabledToolCategories: config.disabledToolCategories,
|
||||||
|
enabledTools: config.enabledTools,
|
||||||
|
disabledTools: config.disabledTools,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertToolProfile(savedProfiles, profile) {
|
||||||
|
const normalized = normalizeToolProfile(profile);
|
||||||
|
const profiles = normalizeSavedToolProfiles(savedProfiles);
|
||||||
|
const key = normalized.name.toLowerCase();
|
||||||
|
const index = profiles.findIndex((item) => item.name.toLowerCase() === key);
|
||||||
|
if (index >= 0) {
|
||||||
|
profiles[index] = normalized;
|
||||||
|
} else {
|
||||||
|
profiles.push(normalized);
|
||||||
|
}
|
||||||
|
return normalizeSavedToolProfiles(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteToolProfile(savedProfiles, name) {
|
||||||
|
const key = normalizeProfileName(name).toLowerCase();
|
||||||
|
return normalizeSavedToolProfiles(savedProfiles)
|
||||||
|
.filter((profile) => profile.name.toLowerCase() !== key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findToolProfile(savedProfiles, name) {
|
||||||
|
const key = normalizeProfileName(name).toLowerCase();
|
||||||
|
return normalizeSavedToolProfiles(savedProfiles)
|
||||||
|
.find((profile) => profile.name.toLowerCase() === key) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyToolProfile(config = {}, profile) {
|
||||||
|
const normalized = normalizeToolProfile(profile);
|
||||||
|
const next = { ...config };
|
||||||
|
for (const field of PROFILE_FIELDS) {
|
||||||
|
next[field] = Array.isArray(normalized[field])
|
||||||
|
? normalized[field].slice()
|
||||||
|
: normalized[field];
|
||||||
|
}
|
||||||
|
next.activeToolProfileName = normalized.name;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportToolProfiles(savedProfiles) {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
profiles: normalizeSavedToolProfiles(savedProfiles),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProfileImportPayload(payload) {
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
return JSON.parse(payload);
|
||||||
|
}
|
||||||
|
return payload || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function importToolProfiles(savedProfiles, payload, options = {}) {
|
||||||
|
const parsed = parseProfileImportPayload(payload);
|
||||||
|
const incoming = Array.isArray(parsed)
|
||||||
|
? parsed
|
||||||
|
: Array.isArray(parsed.profiles)
|
||||||
|
? parsed.profiles
|
||||||
|
: [];
|
||||||
|
if (!incoming.length) {
|
||||||
|
throw new Error('No tool profiles found in import payload.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = options.replace ? [] : normalizeSavedToolProfiles(savedProfiles);
|
||||||
|
return incoming.reduce((profiles, profile) => upsertToolProfile(profiles, profile), base);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
applyToolProfile,
|
||||||
|
createToolProfileSnapshot,
|
||||||
|
deleteToolProfile,
|
||||||
|
exportToolProfiles,
|
||||||
|
findToolProfile,
|
||||||
|
importToolProfiles,
|
||||||
|
normalizeSavedToolProfiles,
|
||||||
|
normalizeToolProfile,
|
||||||
|
upsertToolProfile,
|
||||||
|
};
|
||||||
+62
-1
@@ -23,6 +23,7 @@ const {
|
|||||||
} = require('./logs');
|
} = require('./logs');
|
||||||
const { resolveProjectPath } = require('./path-safety');
|
const { resolveProjectPath } = require('./path-safety');
|
||||||
const {
|
const {
|
||||||
|
createCocosMcpProjectSkill,
|
||||||
createProjectSkill,
|
createProjectSkill,
|
||||||
listProjectInstructions,
|
listProjectInstructions,
|
||||||
readProjectInstruction,
|
readProjectInstruction,
|
||||||
@@ -36,14 +37,22 @@ const {
|
|||||||
revertPrefabInstance,
|
revertPrefabInstance,
|
||||||
validatePrefabReferences,
|
validatePrefabReferences,
|
||||||
} = require('./prefabs');
|
} = require('./prefabs');
|
||||||
|
const { createAssetsAdvancedTools } = require('./tools/assets-advanced');
|
||||||
|
const { createCocosProjectTools } = require('./tools/cocos-project');
|
||||||
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
|
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
|
||||||
|
const { createSceneEventTools } = require('./tools/scene-events');
|
||||||
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
||||||
const { checkForUpdate } = require('./update-checker');
|
const { checkForUpdate } = require('./update-checker');
|
||||||
|
const { assertJavascriptSafety } = require('./javascript-safety');
|
||||||
const { safeStringify } = require('./utils');
|
const { safeStringify } = require('./utils');
|
||||||
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
|
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
|
||||||
|
|
||||||
const TOOL_CATEGORY_RULES = [
|
const TOOL_CATEGORY_RULES = [
|
||||||
['project', /^(get_project_info|get_editor_state|get_tool_catalog)$/],
|
['project', /^(get_project_info|get_editor_state|get_tool_catalog)$/],
|
||||||
|
['build', /^(get_build_status|open_build_panel|run_project_preview|save_current_scene)$/],
|
||||||
|
['preferences', /preference/],
|
||||||
|
['broadcast', /broadcast/],
|
||||||
|
['events', /event|bind_button_click|button_click/],
|
||||||
['updates', /update/],
|
['updates', /update/],
|
||||||
['logs', /log/],
|
['logs', /log/],
|
||||||
['diagnostics', /diagnostic|validate/],
|
['diagnostics', /diagnostic|validate/],
|
||||||
@@ -306,6 +315,30 @@ function toOutput(value) {
|
|||||||
return safeStringify(value);
|
return safeStringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useJavascriptSafetyChecks(args, runtimeContext) {
|
||||||
|
if (args && typeof args.safety_checks === 'boolean') {
|
||||||
|
return args.safety_checks;
|
||||||
|
}
|
||||||
|
if (args && typeof args.safetyChecks === 'boolean') {
|
||||||
|
return args.safetyChecks;
|
||||||
|
}
|
||||||
|
const config = runtimeContext && runtimeContext.config;
|
||||||
|
if (config && typeof config.executeJavascriptSafetyChecks === 'boolean') {
|
||||||
|
return config.executeJavascriptSafetyChecks;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertToolJavascriptSafety(args, runtimeContext) {
|
||||||
|
if (!useJavascriptSafetyChecks(args, runtimeContext)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertJavascriptSafety(args && args.code, {
|
||||||
|
projectPath: runtimeContext && runtimeContext.projectPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveNodeUuid(sceneBridge, args) {
|
async function resolveNodeUuid(sceneBridge, args) {
|
||||||
if (args && args.uuid) {
|
if (args && args.uuid) {
|
||||||
return String(args.uuid);
|
return String(args.uuid);
|
||||||
@@ -328,11 +361,14 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
context: { type: 'string', description: 'Execution context: scene or editor.' },
|
context: { type: 'string', description: 'Execution context: scene or editor.' },
|
||||||
code: { type: 'string', description: 'JavaScript code to execute. May directly return a value, define run(env), or export a function.' },
|
code: { type: 'string', description: 'JavaScript code to execute. May directly return a value, define run(env), or export a function.' },
|
||||||
args: { type: 'object', description: 'Optional JSON object passed into the script.' },
|
args: { type: 'object', description: 'Optional JSON object passed into the script.' },
|
||||||
|
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||||
},
|
},
|
||||||
['context', 'code']
|
['context', 'code']
|
||||||
),
|
),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
const context = String(args.context || '').toLowerCase();
|
const context = String(args.context || '').toLowerCase();
|
||||||
|
const runtimeContext = getRuntimeContext();
|
||||||
|
assertToolJavascriptSafety(args, runtimeContext);
|
||||||
if (context === 'scene') {
|
if (context === 'scene') {
|
||||||
return sceneBridge.call('executeCode', { code: args.code, args: args.args || {} });
|
return sceneBridge.call('executeCode', { code: args.code, args: args.args || {} });
|
||||||
}
|
}
|
||||||
@@ -353,10 +389,14 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
{
|
{
|
||||||
code: { type: 'string', description: 'JavaScript code to execute inside the scene script context.' },
|
code: { type: 'string', description: 'JavaScript code to execute inside the scene script context.' },
|
||||||
args: { type: 'object', description: 'Optional JSON object passed to the scene script.' },
|
args: { type: 'object', description: 'Optional JSON object passed to the scene script.' },
|
||||||
|
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||||
},
|
},
|
||||||
['code']
|
['code']
|
||||||
),
|
),
|
||||||
handler: async (args) => sceneBridge.call('executeCode', { code: args.code, args: args.args || {} }),
|
handler: async (args) => {
|
||||||
|
assertToolJavascriptSafety(args, getRuntimeContext());
|
||||||
|
return sceneBridge.call('executeCode', { code: args.code, args: args.args || {} });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'execute_editor_script',
|
name: 'execute_editor_script',
|
||||||
@@ -366,10 +406,12 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
{
|
{
|
||||||
code: { type: 'string', description: 'JavaScript code to execute inside the editor context.' },
|
code: { type: 'string', description: 'JavaScript code to execute inside the editor context.' },
|
||||||
args: { type: 'object', description: 'Optional JSON object passed to the editor script.' },
|
args: { type: 'object', description: 'Optional JSON object passed to the editor script.' },
|
||||||
|
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||||
},
|
},
|
||||||
['code']
|
['code']
|
||||||
),
|
),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
|
assertToolJavascriptSafety(args, getRuntimeContext());
|
||||||
if (typeof editorExecutor !== 'function') {
|
if (typeof editorExecutor !== 'function') {
|
||||||
throw new Error('Editor JavaScript execution is unavailable.');
|
throw new Error('Editor JavaScript execution is unavailable.');
|
||||||
}
|
}
|
||||||
@@ -512,6 +554,22 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
return createProjectSkill(projectPath, args);
|
return createProjectSkill(projectPath, args);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'create_cocos_mcp_project_skill',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a recommended local Codex project skill for Funplay Cocos MCP workflows.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
skillName: { type: 'string', description: 'Optional filesystem-safe project skill name.' },
|
||||||
|
overwrite: { type: 'boolean', description: 'Allow overwriting an existing skill. Defaults to true.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return createCocosMcpProjectSkill(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'set_selection',
|
name: 'set_selection',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -656,6 +714,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema({}, []),
|
inputSchema: createSchema({}, []),
|
||||||
handler: async () => getRuntimeContext(),
|
handler: async () => getRuntimeContext(),
|
||||||
},
|
},
|
||||||
|
...createCocosProjectTools({ createSchema }),
|
||||||
{
|
{
|
||||||
name: 'list_scenes',
|
name: 'list_scenes',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -961,6 +1020,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
return selectAsset(info.uuid || args.target);
|
return selectAsset(info.uuid || args.target);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...createAssetsAdvancedTools({ createSchema, getRuntimeContext }),
|
||||||
{
|
{
|
||||||
name: 'get_editor_selection',
|
name: 'get_editor_selection',
|
||||||
profile: 'full',
|
profile: 'full',
|
||||||
@@ -1463,6 +1523,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
),
|
),
|
||||||
handler: async (args) => sceneBridge.call('simulateButtonClick', args),
|
handler: async (args) => sceneBridge.call('simulateButtonClick', args),
|
||||||
},
|
},
|
||||||
|
...createSceneEventTools({ createSchema, sceneBridge }),
|
||||||
{
|
{
|
||||||
name: 'invoke_component_method',
|
name: 'invoke_component_method',
|
||||||
profile: 'full',
|
profile: 'full',
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { listAssets, queryAssetInfo } = require('../assets');
|
||||||
|
const { resolveProjectPath } = require('../path-safety');
|
||||||
|
|
||||||
|
const UUID_KEY_PATTERN = /uuid|assetUuid|prefabUuid|sceneUuid|__uuid__/i;
|
||||||
|
const UUID_LITERAL_PATTERN = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[A-Za-z0-9+/=-]{20,32}/g;
|
||||||
|
|
||||||
|
function assetUrlToPath(projectPath, url) {
|
||||||
|
if (!url || !String(url).startsWith('db://assets/')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return path.join(projectPath, String(url).slice('db://'.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function assetFilePath(projectPath, info) {
|
||||||
|
const candidates = [
|
||||||
|
info && info.file,
|
||||||
|
info && info.path,
|
||||||
|
info && info.source,
|
||||||
|
info && info.url ? assetUrlToPath(projectPath, info.url) : '',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const fullPath = resolveProjectPath(projectPath, candidate);
|
||||||
|
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectStructuredUuidReferences(value, refs = [], pointer = '') {
|
||||||
|
if (value == null) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item, index) => collectStructuredUuidReferences(item, refs, `${pointer}/${index}`));
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, child] of Object.entries(value)) {
|
||||||
|
const childPointer = `${pointer}/${key}`;
|
||||||
|
if (typeof child === 'string' && UUID_KEY_PATTERN.test(key)) {
|
||||||
|
refs.push({ uuid: child, path: childPointer, key, source: 'structured' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collectStructuredUuidReferences(child, refs, childPointer);
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectTextUuidReferences(text) {
|
||||||
|
const refs = [];
|
||||||
|
const seen = new Set();
|
||||||
|
let match;
|
||||||
|
while ((match = UUID_LITERAL_PATTERN.exec(String(text || '')))) {
|
||||||
|
const uuid = match[0];
|
||||||
|
if (seen.has(uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(uuid);
|
||||||
|
refs.push({ uuid, path: `@${match.index}`, key: '', source: 'text' });
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeReferences(refs) {
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
for (const ref of refs) {
|
||||||
|
const key = `${ref.uuid}:${ref.path}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
result.push(ref);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUuidReferences(content) {
|
||||||
|
const refs = [];
|
||||||
|
try {
|
||||||
|
refs.push(...collectStructuredUuidReferences(JSON.parse(content)));
|
||||||
|
} catch (error) {
|
||||||
|
// Non-JSON assets still get a literal reference scan below.
|
||||||
|
}
|
||||||
|
refs.push(...collectTextUuidReferences(content));
|
||||||
|
return dedupeReferences(refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspectAssetDependencies(projectPath, target, options = {}) {
|
||||||
|
const info = await queryAssetInfo(target);
|
||||||
|
const filePath = assetFilePath(projectPath, info);
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error(`Asset file was not found: ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(500, options.limit)) : 200;
|
||||||
|
const references = collectUuidReferences(content).slice(0, limit);
|
||||||
|
const dependencies = [];
|
||||||
|
const missing = [];
|
||||||
|
|
||||||
|
for (const ref of references) {
|
||||||
|
try {
|
||||||
|
const asset = await queryAssetInfo(ref.uuid);
|
||||||
|
dependencies.push({
|
||||||
|
...ref,
|
||||||
|
exists: true,
|
||||||
|
asset: {
|
||||||
|
uuid: asset.uuid,
|
||||||
|
url: asset.url,
|
||||||
|
type: asset.type,
|
||||||
|
importer: asset.importer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
missing.push({ ...ref, exists: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: missing.length === 0,
|
||||||
|
target,
|
||||||
|
asset: {
|
||||||
|
uuid: info.uuid,
|
||||||
|
url: info.url,
|
||||||
|
type: info.type,
|
||||||
|
},
|
||||||
|
filePath: path.relative(projectPath, filePath).replace(/\\/g, '/'),
|
||||||
|
referenceCount: references.length,
|
||||||
|
dependencyCount: dependencies.length,
|
||||||
|
missingCount: missing.length,
|
||||||
|
dependencies,
|
||||||
|
missing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateAssetDependencies(projectPath, options = {}) {
|
||||||
|
const targets = options.target
|
||||||
|
? [options.target]
|
||||||
|
: (await listAssets({ pattern: options.pattern || 'db://assets/**', ccType: options.ccType }))
|
||||||
|
.slice(0, Number.isFinite(options.limit) ? Math.max(1, Math.min(200, options.limit)) : 50)
|
||||||
|
.map((asset) => asset.uuid || asset.url)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const assets = [];
|
||||||
|
for (const target of targets) {
|
||||||
|
try {
|
||||||
|
assets.push(await inspectAssetDependencies(projectPath, target, options));
|
||||||
|
} catch (error) {
|
||||||
|
assets.push({
|
||||||
|
ok: false,
|
||||||
|
target,
|
||||||
|
error: error.message,
|
||||||
|
missingCount: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingCount = assets.reduce((sum, asset) => sum + (Number(asset.missingCount) || 0), 0);
|
||||||
|
return {
|
||||||
|
ok: missingCount === 0,
|
||||||
|
assetCount: assets.length,
|
||||||
|
missingCount,
|
||||||
|
assets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAssetsAdvancedTools({ createSchema, getRuntimeContext }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'inspect_asset_dependencies',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Inspect UUID-style dependencies referenced by a serialized Cocos asset.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Asset uuid, db url, or project path.' },
|
||||||
|
limit: { type: 'number', description: 'Maximum dependency references to inspect.' },
|
||||||
|
},
|
||||||
|
['target']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return await inspectAssetDependencies(projectPath, args.target, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'validate_asset_dependencies',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Validate UUID-style dependencies for one asset or a project asset query.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Optional asset uuid, db url, or project path.' },
|
||||||
|
pattern: { type: 'string', description: 'Asset-db pattern used when target is omitted.' },
|
||||||
|
ccType: { type: 'string', description: 'Optional Cocos asset type filter.' },
|
||||||
|
limit: { type: 'number', description: 'Maximum assets to scan when target is omitted.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return await validateAssetDependencies(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
collectUuidReferences,
|
||||||
|
createAssetsAdvancedTools,
|
||||||
|
inspectAssetDependencies,
|
||||||
|
validateAssetDependencies,
|
||||||
|
};
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function hasEditorMessage() {
|
||||||
|
return Boolean(global.Editor && Editor.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureEditorMessage() {
|
||||||
|
if (!hasEditorMessage()) {
|
||||||
|
throw new Error('Editor.Message is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestEditorMessage(channel, method, ...args) {
|
||||||
|
ensureEditorMessage();
|
||||||
|
if (typeof Editor.Message.request !== 'function') {
|
||||||
|
throw new Error('Editor.Message.request is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
return await Editor.Message.request(channel, method, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryEditorRequests(candidates) {
|
||||||
|
const attempts = [];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const channel = candidate.channel;
|
||||||
|
const method = candidate.method;
|
||||||
|
const args = Array.isArray(candidate.args) ? candidate.args : [];
|
||||||
|
try {
|
||||||
|
const result = await requestEditorMessage(channel, method, ...args);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
channel,
|
||||||
|
method,
|
||||||
|
result,
|
||||||
|
attempts,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
attempts.push({ channel, method, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = attempts.length
|
||||||
|
? attempts.map((attempt) => `${attempt.channel}.${attempt.method}: ${attempt.error}`).join('; ')
|
||||||
|
: 'no editor message candidates were provided';
|
||||||
|
const error = new Error(`No compatible Cocos editor message succeeded: ${message}`);
|
||||||
|
error.attempts = attempts;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryEditorRequestsStatus(candidates) {
|
||||||
|
try {
|
||||||
|
return await tryEditorRequests(candidates);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
available: false,
|
||||||
|
attempts: error.attempts || [],
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPanel(panelName) {
|
||||||
|
const id = String(panelName || 'builder').trim();
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('panelName is required.');
|
||||||
|
}
|
||||||
|
if (!global.Editor || !Editor.Panel || typeof Editor.Panel.open !== 'function') {
|
||||||
|
throw new Error('Editor.Panel.open is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
const result = await Editor.Panel.open(id);
|
||||||
|
return { opened: true, panelName: id, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEditorPreference(scope, key) {
|
||||||
|
if (!global.Editor || !Editor.Profile) {
|
||||||
|
throw new Error('Editor.Profile is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
const normalizedScope = String(scope || 'project').toLowerCase();
|
||||||
|
const target = normalizedScope === 'global' ? Editor.Profile : Editor.Profile;
|
||||||
|
const getters = normalizedScope === 'global'
|
||||||
|
? ['getConfig', 'getGlobal']
|
||||||
|
: ['getProject', 'getConfig'];
|
||||||
|
for (const getter of getters) {
|
||||||
|
if (typeof target[getter] === 'function') {
|
||||||
|
return target[getter](key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('No compatible Editor.Profile getter is available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorPreference(scope, key, value) {
|
||||||
|
if (!global.Editor || !Editor.Profile) {
|
||||||
|
throw new Error('Editor.Profile is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
const normalizedScope = String(scope || 'project').toLowerCase();
|
||||||
|
const target = Editor.Profile;
|
||||||
|
const setters = normalizedScope === 'global'
|
||||||
|
? ['setConfig', 'setGlobal']
|
||||||
|
: ['setProject', 'setConfig'];
|
||||||
|
for (const setter of setters) {
|
||||||
|
if (typeof target[setter] === 'function') {
|
||||||
|
const result = target[setter](key, value);
|
||||||
|
return { set: true, scope: normalizedScope, key, value, method: setter, result };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('No compatible Editor.Profile setter is available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastEditorMessage(options = {}) {
|
||||||
|
ensureEditorMessage();
|
||||||
|
const channel = String(options.channel || '').trim();
|
||||||
|
const message = String(options.message || '').trim();
|
||||||
|
if (!message) {
|
||||||
|
throw new Error('message is required.');
|
||||||
|
}
|
||||||
|
const payload = options.payload === undefined ? {} : options.payload;
|
||||||
|
if (channel && typeof Editor.Message.send === 'function') {
|
||||||
|
const result = Editor.Message.send(channel, message, payload);
|
||||||
|
return { sent: true, mode: 'send', channel, message, payload, result };
|
||||||
|
}
|
||||||
|
if (typeof Editor.Message.broadcast === 'function') {
|
||||||
|
const result = Editor.Message.broadcast(message, payload);
|
||||||
|
return { sent: true, mode: 'broadcast', message, payload, result };
|
||||||
|
}
|
||||||
|
throw new Error('Neither Editor.Message.send nor Editor.Message.broadcast is available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCocosProjectTools({ createSchema }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'save_current_scene',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Save the currently open Cocos scene using available editor scene messages.',
|
||||||
|
inputSchema: createSchema({}, []),
|
||||||
|
handler: async () => {
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{ channel: 'scene', method: 'save-scene' },
|
||||||
|
{ channel: 'scene', method: 'save' },
|
||||||
|
]);
|
||||||
|
return { saved: true, ...result };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'open_build_panel',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Open the Cocos build panel, defaulting to the builder panel id.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
panelName: { type: 'string', description: 'Panel id to open. Defaults to builder.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => openPanel(args.panelName || 'builder'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_build_status',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Query Cocos build/preview status using known builder message variants.',
|
||||||
|
inputSchema: createSchema({}, []),
|
||||||
|
handler: async () => await tryEditorRequestsStatus([
|
||||||
|
{ channel: 'builder', method: 'query-build-status' },
|
||||||
|
{ channel: 'builder', method: 'get-build-status' },
|
||||||
|
{ channel: 'builder', method: 'query-build-tasks' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'run_project_preview',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Start Cocos preview/run using known preview and builder message variants.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
platform: { type: 'string', description: 'Optional preview platform or build target.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => await tryEditorRequests([
|
||||||
|
{ channel: 'preview', method: 'start', args: [args || {}] },
|
||||||
|
{ channel: 'preview', method: 'open-preview', args: [args || {}] },
|
||||||
|
{ channel: 'builder', method: 'preview', args: [args || {}] },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_editor_preference',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Read a Cocos editor preference through Editor.Profile when available.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
scope: { type: 'string', description: 'Preference scope: project or global. Defaults to project.' },
|
||||||
|
key: { type: 'string', description: 'Preference key.' },
|
||||||
|
},
|
||||||
|
['key']
|
||||||
|
),
|
||||||
|
handler: async (args) => ({
|
||||||
|
scope: args.scope || 'project',
|
||||||
|
key: args.key,
|
||||||
|
value: getEditorPreference(args.scope, args.key),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'set_editor_preference',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Write a Cocos editor preference through Editor.Profile when available.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
scope: { type: 'string', description: 'Preference scope: project or global. Defaults to project.' },
|
||||||
|
key: { type: 'string', description: 'Preference key.' },
|
||||||
|
valueJson: { type: 'string', description: 'JSON encoded preference value.' },
|
||||||
|
},
|
||||||
|
['key', 'valueJson']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
let value;
|
||||||
|
try {
|
||||||
|
value = JSON.parse(args.valueJson);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`valueJson must be valid JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
return setEditorPreference(args.scope, args.key, value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'broadcast_editor_message',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Send or broadcast a Cocos editor message for advanced editor automation.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
channel: { type: 'string', description: 'Optional Editor.Message channel for send().' },
|
||||||
|
message: { type: 'string', description: 'Message name to send or broadcast.' },
|
||||||
|
payload: { type: 'object', description: 'Optional JSON payload.' },
|
||||||
|
},
|
||||||
|
['message']
|
||||||
|
),
|
||||||
|
handler: async (args) => broadcastEditorMessage(args),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
broadcastEditorMessage,
|
||||||
|
createCocosProjectTools,
|
||||||
|
getEditorPreference,
|
||||||
|
setEditorPreference,
|
||||||
|
tryEditorRequests,
|
||||||
|
tryEditorRequestsStatus,
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createSceneEventTools({ createSchema, sceneBridge }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'list_button_click_events',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] List click event bindings on a Cocos Button component.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Button node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Button node uuid.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact button node name.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('listButtonClickEvents', args),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bind_button_click_event',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Bind a Cocos Button click event to a target node component method.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Button node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Button node uuid.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact button node name.' },
|
||||||
|
targetPath: { type: 'string', description: 'Target node path containing the handler component.' },
|
||||||
|
targetUuid: { type: 'string', description: 'Target node uuid containing the handler component.' },
|
||||||
|
targetName: { type: 'string', description: 'Fallback exact target node name.' },
|
||||||
|
componentName: { type: 'string', description: 'Target component class name.' },
|
||||||
|
handler: { type: 'string', description: 'Method name to invoke on the target component.' },
|
||||||
|
customEventData: { type: 'string', description: 'Optional custom event data string.' },
|
||||||
|
replace: { type: 'boolean', description: 'Replace an identical existing binding.' },
|
||||||
|
},
|
||||||
|
['componentName', 'handler']
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('bindButtonClickEvent', args),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createSceneEventTools,
|
||||||
|
};
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "funplay-cocos-mcp",
|
"name": "funplay-cocos-mcp",
|
||||||
"package_version": 2,
|
"package_version": 2,
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.",
|
"description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.",
|
||||||
"author": "Funplay",
|
"author": "Funplay",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check bin/funplay-cocos-mcp.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-registry.js && node --check lib/tools/files.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/generate-tool-docs.js && node --check scripts/release.js",
|
"check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check bin/funplay-cocos-mcp.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/javascript-safety.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-profiles.js && node --check lib/tool-registry.js && node --check lib/tools/assets-advanced.js && node --check lib/tools/cocos-project.js && node --check lib/tools/files.js && node --check lib/tools/scene-events.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/generate-tool-docs.js && node --check scripts/release.js",
|
||||||
"test": "node --test",
|
"test": "node --test",
|
||||||
"docs:generate": "node scripts/generate-tool-docs.js",
|
"docs:generate": "node scripts/generate-tool-docs.js",
|
||||||
"docs:check": "node scripts/generate-tool-docs.js --check",
|
"docs:check": "node scripts/generate-tool-docs.js --check",
|
||||||
|
|||||||
+318
-1
@@ -51,6 +51,10 @@ module.exports = Editor.Panel.define({
|
|||||||
<ui-checkbox id="sessionsInput"></ui-checkbox>
|
<ui-checkbox id="sessionsInput"></ui-checkbox>
|
||||||
MCP Sessions
|
MCP Sessions
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox-line">
|
||||||
|
<ui-checkbox id="javascriptSafetyInput"></ui-checkbox>
|
||||||
|
JavaScript Safety Checks
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="updateStatus" class="client-status muted"></div>
|
<div id="updateStatus" class="client-status muted"></div>
|
||||||
<p>Changes auto-save. Port/profile changes restart the server when needed.</p>
|
<p>Changes auto-save. Port/profile changes restart the server when needed.</p>
|
||||||
@@ -64,6 +68,19 @@ module.exports = Editor.Panel.define({
|
|||||||
<ui-button id="useFullBtn">Full</ui-button>
|
<ui-button id="useFullBtn">Full</ui-button>
|
||||||
<ui-button id="useCustomBtn">Custom</ui-button>
|
<ui-button id="useCustomBtn">Custom</ui-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="profile-manager">
|
||||||
|
<div class="row">
|
||||||
|
<ui-input id="toolProfileNameInput" placeholder="Profile name"></ui-input>
|
||||||
|
<ui-select id="savedToolProfileSelect"></ui-select>
|
||||||
|
<ui-button id="saveToolProfileBtn">Save Profile</ui-button>
|
||||||
|
<ui-button id="applyToolProfileBtn">Apply</ui-button>
|
||||||
|
<ui-button id="deleteToolProfileBtn">Delete</ui-button>
|
||||||
|
<ui-button id="exportToolProfilesBtn">Export</ui-button>
|
||||||
|
<ui-button id="importToolProfilesBtn">Import</ui-button>
|
||||||
|
</div>
|
||||||
|
<ui-textarea id="toolProfileImportText"></ui-textarea>
|
||||||
|
</div>
|
||||||
|
<div id="categoryControls" class="category-controls"></div>
|
||||||
<div class="tool-config-grid">
|
<div class="tool-config-grid">
|
||||||
<label>Enabled Categories <ui-textarea id="enabledCategoriesInput"></ui-textarea></label>
|
<label>Enabled Categories <ui-textarea id="enabledCategoriesInput"></ui-textarea></label>
|
||||||
<label>Disabled Categories <ui-textarea id="disabledCategoriesInput"></ui-textarea></label>
|
<label>Disabled Categories <ui-textarea id="disabledCategoriesInput"></ui-textarea></label>
|
||||||
@@ -180,6 +197,54 @@ module.exports = Editor.Panel.define({
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.profile-manager {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
#toolProfileNameInput {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
#savedToolProfileSelect {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
#toolProfileImportText {
|
||||||
|
min-height: 54px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.category-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.category-row {
|
||||||
|
border: 1px solid var(--color-normal-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(0,0,0,0.10);
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.category-heading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.category-name {
|
||||||
|
color: var(--color-normal-contrast);
|
||||||
|
font-weight: 600;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.category-count {
|
||||||
|
color: var(--color-normal-contrast-weakest);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.category-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -298,6 +363,9 @@ module.exports = Editor.Panel.define({
|
|||||||
.activity-grid {
|
.activity-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.category-controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
$: {
|
$: {
|
||||||
@@ -309,6 +377,7 @@ module.exports = Editor.Panel.define({
|
|||||||
portInput: '#portInput',
|
portInput: '#portInput',
|
||||||
profileSelect: '#profileSelect',
|
profileSelect: '#profileSelect',
|
||||||
sessionsInput: '#sessionsInput',
|
sessionsInput: '#sessionsInput',
|
||||||
|
javascriptSafetyInput: '#javascriptSafetyInput',
|
||||||
restartBtn: '#restartBtn',
|
restartBtn: '#restartBtn',
|
||||||
copyUrlBtn: '#copyUrlBtn',
|
copyUrlBtn: '#copyUrlBtn',
|
||||||
copyHealthCurlBtn: '#copyHealthCurlBtn',
|
copyHealthCurlBtn: '#copyHealthCurlBtn',
|
||||||
@@ -319,6 +388,15 @@ module.exports = Editor.Panel.define({
|
|||||||
useCoreBtn: '#useCoreBtn',
|
useCoreBtn: '#useCoreBtn',
|
||||||
useFullBtn: '#useFullBtn',
|
useFullBtn: '#useFullBtn',
|
||||||
useCustomBtn: '#useCustomBtn',
|
useCustomBtn: '#useCustomBtn',
|
||||||
|
toolProfileNameInput: '#toolProfileNameInput',
|
||||||
|
savedToolProfileSelect: '#savedToolProfileSelect',
|
||||||
|
saveToolProfileBtn: '#saveToolProfileBtn',
|
||||||
|
applyToolProfileBtn: '#applyToolProfileBtn',
|
||||||
|
deleteToolProfileBtn: '#deleteToolProfileBtn',
|
||||||
|
exportToolProfilesBtn: '#exportToolProfilesBtn',
|
||||||
|
importToolProfilesBtn: '#importToolProfilesBtn',
|
||||||
|
toolProfileImportText: '#toolProfileImportText',
|
||||||
|
categoryControls: '#categoryControls',
|
||||||
enabledCategoriesInput: '#enabledCategoriesInput',
|
enabledCategoriesInput: '#enabledCategoriesInput',
|
||||||
disabledCategoriesInput: '#disabledCategoriesInput',
|
disabledCategoriesInput: '#disabledCategoriesInput',
|
||||||
enabledToolsInput: '#enabledToolsInput',
|
enabledToolsInput: '#enabledToolsInput',
|
||||||
@@ -353,19 +431,23 @@ module.exports = Editor.Panel.define({
|
|||||||
const portText = status.portFallbackActive
|
const portText = status.portFallbackActive
|
||||||
? ` | Port fallback: ${status.requestedPort} -> ${status.port}`
|
? ` | Port fallback: ${status.requestedPort} -> ${status.port}`
|
||||||
: '';
|
: '';
|
||||||
|
const attachText = status.attachedToExisting ? ' | Attached listener' : '';
|
||||||
this.$.statusText.textContent =
|
this.$.statusText.textContent =
|
||||||
`${status.url || ''} | Project: ${status.projectName || ''} | Cocos ${status.cocosVersion || ''}${portText}`;
|
`${status.url || ''} | Project: ${status.projectName || ''} | Cocos ${status.cocosVersion || ''}${portText}${attachText}`;
|
||||||
|
|
||||||
this.$.enabledInput.value = Boolean(isRunning || config.autostart);
|
this.$.enabledInput.value = Boolean(isRunning || config.autostart);
|
||||||
this.$.portInput.value = Number(config.port || status.port || 8765);
|
this.$.portInput.value = Number(config.port || status.port || 8765);
|
||||||
this.$.profileSelect.value = config.toolProfile || status.toolProfile || 'core';
|
this.$.profileSelect.value = config.toolProfile || status.toolProfile || 'core';
|
||||||
this.$.sessionsInput.value = Boolean(config.enableSessions || status.enableSessions);
|
this.$.sessionsInput.value = Boolean(config.enableSessions || status.enableSessions);
|
||||||
|
this.$.javascriptSafetyInput.value = config.executeJavascriptSafetyChecks !== false;
|
||||||
this.$.enabledCategoriesInput.value = this.formatList(config.enabledToolCategories);
|
this.$.enabledCategoriesInput.value = this.formatList(config.enabledToolCategories);
|
||||||
this.$.disabledCategoriesInput.value = this.formatList(config.disabledToolCategories);
|
this.$.disabledCategoriesInput.value = this.formatList(config.disabledToolCategories);
|
||||||
this.$.enabledToolsInput.value = this.formatList(config.enabledTools);
|
this.$.enabledToolsInput.value = this.formatList(config.enabledTools);
|
||||||
this.$.disabledToolsInput.value = this.formatList(config.disabledTools);
|
this.$.disabledToolsInput.value = this.formatList(config.disabledTools);
|
||||||
|
this.renderToolProfiles();
|
||||||
this.renderUpdateStatus();
|
this.renderUpdateStatus();
|
||||||
this.renderToolSummary();
|
this.renderToolSummary();
|
||||||
|
this.renderCategoryControls();
|
||||||
this.renderClientTargets();
|
this.renderClientTargets();
|
||||||
this.renderActivity();
|
this.renderActivity();
|
||||||
},
|
},
|
||||||
@@ -373,11 +455,136 @@ module.exports = Editor.Panel.define({
|
|||||||
return Array.isArray(value) ? value.join('\n') : '';
|
return Array.isArray(value) ? value.join('\n') : '';
|
||||||
},
|
},
|
||||||
parseList(value) {
|
parseList(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.split(/[\n,]/)
|
.split(/[\n,]/)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
|
normalizeToolProfile(profile) {
|
||||||
|
const name = String(profile && profile.name || '').trim();
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('Profile name is required.');
|
||||||
|
}
|
||||||
|
const mode = String(profile.toolProfile || 'core').toLowerCase();
|
||||||
|
return {
|
||||||
|
name: name.slice(0, 80),
|
||||||
|
toolProfile: mode === 'full' || mode === 'custom' ? mode : 'core',
|
||||||
|
enabledToolCategories: this.parseList(profile.enabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
disabledToolCategories: this.parseList(profile.disabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
enabledTools: this.parseList(profile.enabledTools),
|
||||||
|
disabledTools: this.parseList(profile.disabledTools),
|
||||||
|
updatedAt: profile.updatedAt || new Date().toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
normalizeToolProfiles(value) {
|
||||||
|
const result = [];
|
||||||
|
const seen = new Set();
|
||||||
|
(Array.isArray(value) ? value : []).forEach((profile) => {
|
||||||
|
try {
|
||||||
|
const normalized = this.normalizeToolProfile(profile);
|
||||||
|
const key = normalized.name.toLowerCase();
|
||||||
|
const existing = result.findIndex((item) => item.name.toLowerCase() === key);
|
||||||
|
if (existing >= 0) {
|
||||||
|
result[existing] = normalized;
|
||||||
|
} else if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
result.push(normalized);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore malformed imported entries in the panel; backend normalization repeats this.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
},
|
||||||
|
currentToolProfileSnapshot(name) {
|
||||||
|
return this.normalizeToolProfile({
|
||||||
|
name,
|
||||||
|
toolProfile: this.$.profileSelect.value || 'core',
|
||||||
|
enabledToolCategories: this.parseList(this.$.enabledCategoriesInput.value).map((item) => item.toLowerCase()),
|
||||||
|
disabledToolCategories: this.parseList(this.$.disabledCategoriesInput.value).map((item) => item.toLowerCase()),
|
||||||
|
enabledTools: this.parseList(this.$.enabledToolsInput.value),
|
||||||
|
disabledTools: this.parseList(this.$.disabledToolsInput.value),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getSavedToolProfiles() {
|
||||||
|
const config = this.state && this.state.config ? this.state.config : {};
|
||||||
|
return this.normalizeToolProfiles(config.savedToolProfiles || []);
|
||||||
|
},
|
||||||
|
renderToolProfiles() {
|
||||||
|
const config = this.state && this.state.config ? this.state.config : {};
|
||||||
|
const profiles = this.getSavedToolProfiles();
|
||||||
|
const selected = this.$.savedToolProfileSelect.value || config.activeToolProfileName || (profiles[0] && profiles[0].name) || '';
|
||||||
|
this.$.savedToolProfileSelect.innerHTML = '';
|
||||||
|
if (profiles.length) {
|
||||||
|
profiles.forEach((profile) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = profile.name;
|
||||||
|
option.textContent = profile.name;
|
||||||
|
this.$.savedToolProfileSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = '';
|
||||||
|
option.textContent = 'No saved profiles';
|
||||||
|
this.$.savedToolProfileSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
this.$.savedToolProfileSelect.value = selected;
|
||||||
|
if (!this.$.toolProfileNameInput.value) {
|
||||||
|
this.$.toolProfileNameInput.value = selected || config.activeToolProfileName || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderCategoryControls() {
|
||||||
|
const catalog = (this.state && this.state.toolCatalog) || [];
|
||||||
|
const groups = catalog.reduce((acc, tool) => {
|
||||||
|
const category = tool.category || 'other';
|
||||||
|
if (!acc[category]) {
|
||||||
|
acc[category] = { total: 0, enabled: 0 };
|
||||||
|
}
|
||||||
|
acc[category].total += 1;
|
||||||
|
if (tool.enabled) {
|
||||||
|
acc[category].enabled += 1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.$.categoryControls.innerHTML = '';
|
||||||
|
Object.keys(groups).sort().forEach((category) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'category-row';
|
||||||
|
|
||||||
|
const heading = document.createElement('div');
|
||||||
|
heading.className = 'category-heading';
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'category-name';
|
||||||
|
name.textContent = category;
|
||||||
|
const count = document.createElement('div');
|
||||||
|
count.className = 'category-count';
|
||||||
|
count.textContent = `${groups[category].enabled}/${groups[category].total}`;
|
||||||
|
heading.appendChild(name);
|
||||||
|
heading.appendChild(count);
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'category-actions';
|
||||||
|
[
|
||||||
|
['enable', 'Enable'],
|
||||||
|
['disable', 'Disable'],
|
||||||
|
['clear', 'Clear'],
|
||||||
|
].forEach(([mode, label]) => {
|
||||||
|
const button = document.createElement('ui-button');
|
||||||
|
button.textContent = label;
|
||||||
|
button.dataset.category = category;
|
||||||
|
button.dataset.mode = mode;
|
||||||
|
actions.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(heading);
|
||||||
|
row.appendChild(actions);
|
||||||
|
this.$.categoryControls.appendChild(row);
|
||||||
|
});
|
||||||
|
},
|
||||||
renderUpdateStatus() {
|
renderUpdateStatus() {
|
||||||
const update = this.state && this.state.updateInfo;
|
const update = this.state && this.state.updateInfo;
|
||||||
if (!update) {
|
if (!update) {
|
||||||
@@ -546,11 +753,103 @@ module.exports = Editor.Panel.define({
|
|||||||
enabledTools: this.parseList(this.$.enabledToolsInput.value),
|
enabledTools: this.parseList(this.$.enabledToolsInput.value),
|
||||||
disabledTools: this.parseList(this.$.disabledToolsInput.value),
|
disabledTools: this.parseList(this.$.disabledToolsInput.value),
|
||||||
enableSessions: Boolean(this.$.sessionsInput.value),
|
enableSessions: Boolean(this.$.sessionsInput.value),
|
||||||
|
executeJavascriptSafetyChecks: Boolean(this.$.javascriptSafetyInput.value),
|
||||||
autostart: Boolean(this.$.enabledInput.value),
|
autostart: Boolean(this.$.enabledInput.value),
|
||||||
maxInteractionLogEntries: this.state && this.state.config ? this.state.config.maxInteractionLogEntries : 50,
|
maxInteractionLogEntries: this.state && this.state.config ? this.state.config.maxInteractionLogEntries : 50,
|
||||||
lastClientTargetId: this.$.clientTargetSelect.value || 'claude_code',
|
lastClientTargetId: this.$.clientTargetSelect.value || 'claude_code',
|
||||||
|
activeToolProfileName: this.$.toolProfileNameInput.value || '',
|
||||||
|
savedToolProfiles: this.getSavedToolProfiles(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async saveCurrentToolProfile() {
|
||||||
|
const name = this.$.toolProfileNameInput.value || this.$.savedToolProfileSelect.value;
|
||||||
|
const snapshot = this.currentToolProfileSnapshot(name);
|
||||||
|
const profiles = this.getSavedToolProfiles();
|
||||||
|
const key = snapshot.name.toLowerCase();
|
||||||
|
const existing = profiles.findIndex((profile) => profile.name.toLowerCase() === key);
|
||||||
|
if (existing >= 0) {
|
||||||
|
profiles[existing] = snapshot;
|
||||||
|
} else {
|
||||||
|
profiles.push(snapshot);
|
||||||
|
}
|
||||||
|
this.state.config.savedToolProfiles = this.normalizeToolProfiles(profiles);
|
||||||
|
this.state.config.activeToolProfileName = snapshot.name;
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
|
async applySavedToolProfile() {
|
||||||
|
const name = this.$.savedToolProfileSelect.value;
|
||||||
|
const profile = this.getSavedToolProfiles().find((item) => item.name === name);
|
||||||
|
if (!profile) {
|
||||||
|
this.showOutput('Select a saved profile first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$.profileSelect.value = profile.toolProfile;
|
||||||
|
this.$.enabledCategoriesInput.value = this.formatList(profile.enabledToolCategories);
|
||||||
|
this.$.disabledCategoriesInput.value = this.formatList(profile.disabledToolCategories);
|
||||||
|
this.$.enabledToolsInput.value = this.formatList(profile.enabledTools);
|
||||||
|
this.$.disabledToolsInput.value = this.formatList(profile.disabledTools);
|
||||||
|
this.$.toolProfileNameInput.value = profile.name;
|
||||||
|
this.state.config.activeToolProfileName = profile.name;
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
|
async deleteSavedToolProfile() {
|
||||||
|
const name = this.$.savedToolProfileSelect.value;
|
||||||
|
if (!name) {
|
||||||
|
this.showOutput('Select a saved profile first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.config.savedToolProfiles = this.getSavedToolProfiles()
|
||||||
|
.filter((profile) => profile.name !== name);
|
||||||
|
if (this.state.config.activeToolProfileName === name) {
|
||||||
|
this.state.config.activeToolProfileName = '';
|
||||||
|
}
|
||||||
|
this.$.toolProfileNameInput.value = '';
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
|
exportSavedToolProfiles() {
|
||||||
|
const payload = JSON.stringify({ version: 1, profiles: this.getSavedToolProfiles() }, null, 2);
|
||||||
|
this.$.toolProfileImportText.value = payload;
|
||||||
|
this.copyText(payload, 'Copied tool profiles to clipboard.');
|
||||||
|
},
|
||||||
|
async importSavedToolProfiles() {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(this.$.toolProfileImportText.value || '{}');
|
||||||
|
const incoming = Array.isArray(payload)
|
||||||
|
? payload
|
||||||
|
: Array.isArray(payload.profiles)
|
||||||
|
? payload.profiles
|
||||||
|
: [];
|
||||||
|
if (!incoming.length) {
|
||||||
|
throw new Error('No profiles found.');
|
||||||
|
}
|
||||||
|
this.state.config.savedToolProfiles = this.normalizeToolProfiles([
|
||||||
|
...this.getSavedToolProfiles(),
|
||||||
|
...incoming,
|
||||||
|
]);
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
} catch (error) {
|
||||||
|
this.showOutput(`Import profiles failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setCategoryExposure(category, mode) {
|
||||||
|
const enabled = new Set(this.parseList(this.$.enabledCategoriesInput.value).map((item) => item.toLowerCase()));
|
||||||
|
const disabled = new Set(this.parseList(this.$.disabledCategoriesInput.value).map((item) => item.toLowerCase()));
|
||||||
|
const key = String(category || '').toLowerCase();
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enabled.delete(key);
|
||||||
|
disabled.delete(key);
|
||||||
|
if (mode === 'enable') {
|
||||||
|
enabled.add(key);
|
||||||
|
} else if (mode === 'disable') {
|
||||||
|
disabled.add(key);
|
||||||
|
}
|
||||||
|
this.$.profileSelect.value = 'custom';
|
||||||
|
this.$.enabledCategoriesInput.value = Array.from(enabled).sort().join('\n');
|
||||||
|
this.$.disabledCategoriesInput.value = Array.from(disabled).sort().join('\n');
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
async handleEnableToggle() {
|
async handleEnableToggle() {
|
||||||
const shouldEnable = Boolean(this.$.enabledInput.value);
|
const shouldEnable = Boolean(this.$.enabledInput.value);
|
||||||
const wasRunning = Boolean(this.state && this.state.status && this.state.status.running);
|
const wasRunning = Boolean(this.state && this.state.status && this.state.status.running);
|
||||||
@@ -587,6 +886,7 @@ module.exports = Editor.Panel.define({
|
|||||||
this.$.portInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.portInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.profileSelect.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.profileSelect.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.sessionsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.sessionsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
|
this.$.javascriptSafetyInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.enabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.enabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.disabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.disabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.enabledToolsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.enabledToolsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
@@ -611,6 +911,23 @@ module.exports = Editor.Panel.define({
|
|||||||
this.$.profileSelect.value = 'custom';
|
this.$.profileSelect.value = 'custom';
|
||||||
this.persistConfig({ showOutput: true });
|
this.persistConfig({ showOutput: true });
|
||||||
});
|
});
|
||||||
|
this.$.saveToolProfileBtn.addEventListener('click', () => this.saveCurrentToolProfile());
|
||||||
|
this.$.applyToolProfileBtn.addEventListener('click', () => this.applySavedToolProfile());
|
||||||
|
this.$.deleteToolProfileBtn.addEventListener('click', () => this.deleteSavedToolProfile());
|
||||||
|
this.$.exportToolProfilesBtn.addEventListener('click', () => this.exportSavedToolProfiles());
|
||||||
|
this.$.importToolProfilesBtn.addEventListener('click', () => this.importSavedToolProfiles());
|
||||||
|
this.$.savedToolProfileSelect.addEventListener('change', () => {
|
||||||
|
this.$.toolProfileNameInput.value = this.$.savedToolProfileSelect.value || '';
|
||||||
|
});
|
||||||
|
this.$.categoryControls.addEventListener('click', (event) => {
|
||||||
|
const target = event.target && typeof event.target.closest === 'function'
|
||||||
|
? event.target.closest('ui-button')
|
||||||
|
: event.target;
|
||||||
|
if (!target || !target.dataset || !target.dataset.category) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setCategoryExposure(target.dataset.category, target.dataset.mode);
|
||||||
|
});
|
||||||
this.$.clientTargetSelect.addEventListener('confirm', () => this.renderClientTargetStatus());
|
this.$.clientTargetSelect.addEventListener('confirm', () => this.renderClientTargetStatus());
|
||||||
this.$.clientTargetSelect.addEventListener('change', () => {
|
this.$.clientTargetSelect.addEventListener('change', () => {
|
||||||
this.renderClientTargetStatus();
|
this.renderClientTargetStatus();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const {
|
|||||||
SceneAsset,
|
SceneAsset,
|
||||||
js,
|
js,
|
||||||
Component,
|
Component,
|
||||||
|
EventHandler,
|
||||||
Canvas,
|
Canvas,
|
||||||
UITransform,
|
UITransform,
|
||||||
Label,
|
Label,
|
||||||
@@ -241,6 +242,23 @@ function findComponent(node, options = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEventHandlerClass() {
|
||||||
|
return (Component && Component.EventHandler) || EventHandler || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeEventHandler(handler) {
|
||||||
|
if (!handler) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
target: handler.target && handler.target.name ? getNodePath(handler.target) : '',
|
||||||
|
targetUuid: handler.target && handler.target.uuid ? handler.target.uuid : '',
|
||||||
|
component: handler.component || '',
|
||||||
|
handler: handler.handler || '',
|
||||||
|
customEventData: handler.customEventData || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getOrAddComponent(node, componentClass) {
|
function getOrAddComponent(node, componentClass) {
|
||||||
return node.getComponent(componentClass) || node.addComponent(componentClass);
|
return node.getComponent(componentClass) || node.addComponent(componentClass);
|
||||||
}
|
}
|
||||||
@@ -1345,6 +1363,101 @@ exports.methods = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async listButtonClickEvents(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Target node was not found.');
|
||||||
|
}
|
||||||
|
const button = node.getComponent(Button);
|
||||||
|
if (!button) {
|
||||||
|
throw new Error('Button component was not found on target node.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: getNodePath(node),
|
||||||
|
uuid: node.uuid,
|
||||||
|
clickEventCount: Array.isArray(button.clickEvents) ? button.clickEvents.length : 0,
|
||||||
|
clickEvents: Array.isArray(button.clickEvents)
|
||||||
|
? button.clickEvents.map(serializeEventHandler).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async bindButtonClickEvent(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Button node was not found.');
|
||||||
|
}
|
||||||
|
const button = node.getComponent(Button);
|
||||||
|
if (!button) {
|
||||||
|
throw new Error('Button component was not found on target node.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = findNode({
|
||||||
|
path: options.targetPath,
|
||||||
|
uuid: options.targetUuid,
|
||||||
|
name: options.targetName,
|
||||||
|
});
|
||||||
|
if (!target) {
|
||||||
|
throw new Error('Event target node was not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentName = String(options.componentName || '').trim();
|
||||||
|
const handlerName = String(options.handler || options.handlerName || '').trim();
|
||||||
|
if (!componentName || !handlerName) {
|
||||||
|
throw new Error('componentName and handler are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = findComponent(target, { componentName });
|
||||||
|
if (!component) {
|
||||||
|
throw new Error(`Target component was not found: ${componentName}`);
|
||||||
|
}
|
||||||
|
if (typeof component[handlerName] !== 'function') {
|
||||||
|
throw new Error(`Target component method was not found: ${componentName}.${handlerName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HandlerClass = getEventHandlerClass();
|
||||||
|
if (!HandlerClass) {
|
||||||
|
throw new Error('Cocos EventHandler class is unavailable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = Array.isArray(button.clickEvents) ? button.clickEvents : [];
|
||||||
|
const duplicate = existing.find((event) => (
|
||||||
|
event &&
|
||||||
|
event.target === target &&
|
||||||
|
event.component === componentName &&
|
||||||
|
event.handler === handlerName &&
|
||||||
|
String(event.customEventData || '') === String(options.customEventData || '')
|
||||||
|
));
|
||||||
|
if (duplicate && options.replace !== true) {
|
||||||
|
return {
|
||||||
|
bound: false,
|
||||||
|
duplicate: true,
|
||||||
|
node: getNodePath(node),
|
||||||
|
event: serializeEventHandler(duplicate),
|
||||||
|
clickEventCount: existing.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new HandlerClass();
|
||||||
|
event.target = target;
|
||||||
|
event.component = componentName;
|
||||||
|
event.handler = handlerName;
|
||||||
|
event.customEventData = String(options.customEventData || '');
|
||||||
|
|
||||||
|
button.clickEvents = options.replace === true
|
||||||
|
? existing.filter((item) => item !== duplicate).concat(event)
|
||||||
|
: existing.concat(event);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bound: true,
|
||||||
|
node: getNodePath(node),
|
||||||
|
uuid: node.uuid,
|
||||||
|
event: serializeEventHandler(event),
|
||||||
|
clickEventCount: button.clickEvents.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async invokeComponentMethod(options = {}) {
|
async invokeComponentMethod(options = {}) {
|
||||||
const node = findNode(options);
|
const node = findNode(options);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ const FORBIDDEN_NAMES = new Set([
|
|||||||
'.DS_Store'
|
'.DS_Store'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const FORBIDDEN_CONTENT_PATTERNS = [
|
||||||
|
['npm token', /\bnpm_[A-Za-z0-9]{20,}\b/],
|
||||||
|
['GitHub token', /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/],
|
||||||
|
['MCP token', /\bmcp_[A-Za-z0-9_-]{32,}\b/],
|
||||||
|
['private key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/]
|
||||||
|
];
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const command = process.argv[2] || 'check';
|
const command = process.argv[2] || 'check';
|
||||||
const options = parseOptions(process.argv.slice(3));
|
const options = parseOptions(process.argv.slice(3));
|
||||||
@@ -252,6 +259,7 @@ function packageRelease(context) {
|
|||||||
const stagedFiles = collectFiles(stagingRoot)
|
const stagedFiles = collectFiles(stagingRoot)
|
||||||
.map((filePath) => path.relative(TEMP_DIR, filePath).split(path.sep).join('/'));
|
.map((filePath) => path.relative(TEMP_DIR, filePath).split(path.sep).join('/'));
|
||||||
validateArchivePaths(stagedFiles);
|
validateArchivePaths(stagedFiles);
|
||||||
|
validateArchiveContent(collectFiles(stagingRoot));
|
||||||
|
|
||||||
run('zip', ['-qr', zipPath, PACKAGE_DIR_NAME], { cwd: TEMP_DIR });
|
run('zip', ['-qr', zipPath, PACKAGE_DIR_NAME], { cwd: TEMP_DIR });
|
||||||
validateZipListing(zipPath);
|
validateZipListing(zipPath);
|
||||||
@@ -509,6 +517,30 @@ function validateZipListing(zipPath) {
|
|||||||
validateArchivePaths(listing);
|
validateArchivePaths(listing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateArchiveContent(filePaths) {
|
||||||
|
const bad = [];
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.size > 1024 * 1024) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const buffer = fs.readFileSync(filePath);
|
||||||
|
if (buffer.includes(0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const text = buffer.toString('utf8');
|
||||||
|
for (const [label, pattern] of FORBIDDEN_CONTENT_PATTERNS) {
|
||||||
|
if (pattern.test(text)) {
|
||||||
|
bad.push(`${path.relative(ROOT, filePath)} (${label})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bad.length > 0) {
|
||||||
|
throw new Error(`Release archive contains sensitive-looking content:\n- ${bad.join('\n- ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isForbiddenTrackedPath(relative) {
|
function isForbiddenTrackedPath(relative) {
|
||||||
const parts = relative.split('/');
|
const parts = relative.split('/');
|
||||||
return parts.some((part) => FORBIDDEN_TRACKED_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part));
|
return parts.some((part) => FORBIDDEN_TRACKED_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part));
|
||||||
|
|||||||
+2
-2
@@ -7,12 +7,12 @@
|
|||||||
"url": "https://github.com/FunplayAI/funplay-cocos-mcp",
|
"url": "https://github.com/FunplayAI/funplay-cocos-mcp",
|
||||||
"source": "github"
|
"source": "github"
|
||||||
},
|
},
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"registryType": "npm",
|
"registryType": "npm",
|
||||||
"identifier": "funplay-cocos-mcp",
|
"identifier": "funplay-cocos-mcp",
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"transport": {
|
"transport": {
|
||||||
"type": "stdio"
|
"type": "stdio"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const { collectUuidReferences } = require('../lib/tools/assets-advanced');
|
||||||
|
|
||||||
|
test('collectUuidReferences finds structured and literal UUID references', () => {
|
||||||
|
const refs = collectUuidReferences(JSON.stringify({
|
||||||
|
__type__: 'cc.Prefab',
|
||||||
|
sprite: { __uuid__: '2d3KcYpS5HCKb6wU0v5c9x' },
|
||||||
|
nested: [{ assetUuid: '550e8400-e29b-41d4-a716-446655440000' }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.equal(refs.some((ref) => ref.uuid === '2d3KcYpS5HCKb6wU0v5c9x' && ref.source === 'structured'), true);
|
||||||
|
assert.equal(refs.some((ref) => ref.uuid === '550e8400-e29b-41d4-a716-446655440000'), true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const {
|
||||||
|
broadcastEditorMessage,
|
||||||
|
getEditorPreference,
|
||||||
|
setEditorPreference,
|
||||||
|
tryEditorRequests,
|
||||||
|
tryEditorRequestsStatus,
|
||||||
|
} = require('../lib/tools/cocos-project');
|
||||||
|
|
||||||
|
test('tryEditorRequests returns the first successful editor message candidate', async () => {
|
||||||
|
const calls = [];
|
||||||
|
global.Editor = {
|
||||||
|
Message: {
|
||||||
|
request: async (channel, method, payload) => {
|
||||||
|
calls.push({ channel, method, payload });
|
||||||
|
if (method === 'bad') {
|
||||||
|
throw new Error('nope');
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{ channel: 'scene', method: 'bad' },
|
||||||
|
{ channel: 'scene', method: 'save-scene', args: [{ force: true }] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.method, 'save-scene');
|
||||||
|
assert.equal(calls.length, 2);
|
||||||
|
} finally {
|
||||||
|
delete global.Editor;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tryEditorRequestsStatus returns an unavailable payload instead of throwing', async () => {
|
||||||
|
global.Editor = {
|
||||||
|
Message: {
|
||||||
|
request: async () => {
|
||||||
|
throw new Error('missing');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tryEditorRequestsStatus([{ channel: 'builder', method: 'query-build-status' }]);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.available, false);
|
||||||
|
assert.equal(result.attempts.length, 1);
|
||||||
|
} finally {
|
||||||
|
delete global.Editor;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preference helpers and broadcast use available Editor APIs', () => {
|
||||||
|
const sent = [];
|
||||||
|
const store = new Map();
|
||||||
|
global.Editor = {
|
||||||
|
Message: {
|
||||||
|
send(channel, message, payload) {
|
||||||
|
sent.push({ channel, message, payload });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Profile: {
|
||||||
|
getProject(key) {
|
||||||
|
return store.get(key);
|
||||||
|
},
|
||||||
|
setProject(key, value) {
|
||||||
|
store.set(key, value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setEditorPreference('project', 'preview.port', 7456);
|
||||||
|
assert.equal(getEditorPreference('project', 'preview.port'), 7456);
|
||||||
|
|
||||||
|
const result = broadcastEditorMessage({ channel: 'scene', message: 'custom-event', payload: { ok: true } });
|
||||||
|
assert.equal(result.sent, true);
|
||||||
|
assert.deepEqual(sent[0], { channel: 'scene', message: 'custom-event', payload: { ok: true } });
|
||||||
|
} finally {
|
||||||
|
delete global.Editor;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
const {
|
||||||
|
assertJavascriptSafety,
|
||||||
|
inspectJavascriptSafety,
|
||||||
|
} = require('../lib/javascript-safety');
|
||||||
|
|
||||||
|
const PROJECT_PATH = path.resolve('/tmp/funplay-cocos-test-project');
|
||||||
|
|
||||||
|
test('JavaScript safety allows project-local write snippets by default', () => {
|
||||||
|
const result = inspectJavascriptSafety(
|
||||||
|
"fs.writeFileSync(path.join(context.projectPath, 'assets/generated.ts'), 'export {};');",
|
||||||
|
{ projectPath: PROJECT_PATH }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks delete operations', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => assertJavascriptSafety("fs.rmSync(path.join(context.projectPath, 'assets'), { recursive: true });", {
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
}),
|
||||||
|
/delete\/truncate/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks traversal and home path literals', () => {
|
||||||
|
const result = inspectJavascriptSafety(
|
||||||
|
"fs.writeFileSync('../outside.txt', 'x'); fs.writeFileSync('~/secret.txt', 'x');",
|
||||||
|
{ projectPath: PROJECT_PATH }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.violations.some((item) => item.includes('path traversal')), true);
|
||||||
|
assert.equal(result.violations.some((item) => item.includes('user-home')), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks absolute paths outside the project', () => {
|
||||||
|
const result = inspectJavascriptSafety("fs.writeFileSync('/tmp/outside.txt', 'x');", {
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.violations.some((item) => item.includes('absolute path outside')), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks child_process usage', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => assertJavascriptSafety("const cp = require('child_process'); cp.execSync('rm -rf /tmp/x');", {
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
}),
|
||||||
|
/child_process/
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ const os = require('node:os');
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const {
|
const {
|
||||||
|
createCocosMcpProjectSkill,
|
||||||
createProjectSkill,
|
createProjectSkill,
|
||||||
listProjectInstructions,
|
listProjectInstructions,
|
||||||
readProjectInstruction,
|
readProjectInstruction,
|
||||||
@@ -39,6 +40,16 @@ test('createProjectSkill writes a Codex project skill', () => {
|
|||||||
assert.equal(listed.skills.some((skill) => skill.path === result.path), true);
|
assert.equal(listed.skills.some((skill) => skill.path === result.path), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createCocosMcpProjectSkill writes the recommended MCP workflow skill', () => {
|
||||||
|
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-default-skill-'));
|
||||||
|
const result = createCocosMcpProjectSkill(projectPath);
|
||||||
|
|
||||||
|
assert.equal(result.path, '.codex/skills/funplay-cocos-mcp-workflow/SKILL.md');
|
||||||
|
const content = readProjectInstruction(projectPath, result.path).content;
|
||||||
|
assert.match(content, /Funplay Cocos MCP Workflow/);
|
||||||
|
assert.match(content, /inspect_asset_dependencies/);
|
||||||
|
});
|
||||||
|
|
||||||
test('project instruction helpers reject traversal outside the project', () => {
|
test('project instruction helpers reject traversal outside the project', () => {
|
||||||
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-safe-'));
|
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-safe-'));
|
||||||
assert.throws(
|
assert.throws(
|
||||||
|
|||||||
+67
-3
@@ -9,7 +9,7 @@ const {
|
|||||||
SUPPORTED_PROTOCOL_VERSIONS,
|
SUPPORTED_PROTOCOL_VERSIONS,
|
||||||
} = require('../lib/server');
|
} = require('../lib/server');
|
||||||
|
|
||||||
function createServer(toolRegistry = {}, config = {}) {
|
function createServer(toolRegistry = {}, config = {}, options = {}) {
|
||||||
return new McpServer({
|
return new McpServer({
|
||||||
config: { host: '127.0.0.1', port: 8765, ...config },
|
config: { host: '127.0.0.1', port: 8765, ...config },
|
||||||
toolRegistry: {
|
toolRegistry: {
|
||||||
@@ -28,8 +28,10 @@ function createServer(toolRegistry = {}, config = {}) {
|
|||||||
},
|
},
|
||||||
interactionLog: { add() {} },
|
interactionLog: { add() {} },
|
||||||
runtimeLog: { add() {} },
|
runtimeLog: { add() {} },
|
||||||
serverName: 'test-server',
|
serverName: options.serverName || 'test-server',
|
||||||
serverVersion: '0.0.0-test',
|
serverVersion: options.serverVersion || '0.0.0-test',
|
||||||
|
projectName: options.projectName || 'test-project',
|
||||||
|
projectIdentity: options.projectIdentity || 'test-project-id',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +106,7 @@ test('initialize negotiates the current MCP protocol version by default', async
|
|||||||
|
|
||||||
assert.equal(response.result.protocolVersion, MCP_PROTOCOL_VERSION);
|
assert.equal(response.result.protocolVersion, MCP_PROTOCOL_VERSION);
|
||||||
assert.equal(response.result.serverInfo.name, 'test-server');
|
assert.equal(response.result.serverInfo.name, 'test-server');
|
||||||
|
assert.equal(response.result.funplay.projectIdentity, 'test-project-id');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initialize can negotiate an older supported MCP protocol version', async () => {
|
test('initialize can negotiate an older supported MCP protocol version', async () => {
|
||||||
@@ -286,6 +289,67 @@ test('HTTP GET /tools returns debug tool metadata and curl examples', async () =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('HTTP GET /health returns project identity metadata', async () => {
|
||||||
|
const server = createServer({}, { port: 0 }, { projectIdentity: 'health-project-id' });
|
||||||
|
await server.start();
|
||||||
|
try {
|
||||||
|
const response = await httpGet(server.getPort(), '/health');
|
||||||
|
const payload = JSON.parse(response.body);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.projectName, 'test-project');
|
||||||
|
assert.equal(payload.projectIdentity, 'health-project-id');
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start attaches to an existing same-project listener on the configured port', async () => {
|
||||||
|
const owner = createServer({}, { port: 0 }, { projectIdentity: 'same-project' });
|
||||||
|
await owner.start();
|
||||||
|
const attached = createServer({}, { port: owner.getPort() }, { projectIdentity: 'same-project' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await attached.start();
|
||||||
|
|
||||||
|
assert.equal(attached.isRunning(), true);
|
||||||
|
assert.equal(attached.getPort(), owner.getPort());
|
||||||
|
assert.equal(attached.getAttachInfo().projectIdentity, 'same-project');
|
||||||
|
|
||||||
|
await attached.stop();
|
||||||
|
assert.equal(attached.isRunning(), false);
|
||||||
|
|
||||||
|
const response = await httpGet(owner.getPort(), '/health');
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
} finally {
|
||||||
|
if (attached.isRunning()) {
|
||||||
|
await attached.stop();
|
||||||
|
}
|
||||||
|
await owner.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start falls back instead of attaching to a different project listener', async () => {
|
||||||
|
const owner = createServer({}, { port: 0 }, { projectIdentity: 'owner-project' });
|
||||||
|
await owner.start();
|
||||||
|
const contender = createServer({}, { port: owner.getPort() }, { projectIdentity: 'other-project' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await contender.start();
|
||||||
|
|
||||||
|
assert.equal(contender.isRunning(), true);
|
||||||
|
assert.notEqual(contender.getPort(), owner.getPort());
|
||||||
|
assert.equal(contender.getAttachInfo(), null);
|
||||||
|
assert.equal(contender.getPortFallbackInfo().requestedPort, owner.getPort());
|
||||||
|
} finally {
|
||||||
|
if (contender.isRunning()) {
|
||||||
|
await contender.stop();
|
||||||
|
}
|
||||||
|
await owner.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('HTTP initialize can return an optional session id when sessions are enabled', async () => {
|
test('HTTP initialize can return an optional session id when sessions are enabled', async () => {
|
||||||
const server = createServer({}, { port: 0, enableSessions: true });
|
const server = createServer({}, { port: 0, enableSessions: true });
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const {
|
||||||
|
applyToolProfile,
|
||||||
|
createToolProfileSnapshot,
|
||||||
|
deleteToolProfile,
|
||||||
|
exportToolProfiles,
|
||||||
|
importToolProfiles,
|
||||||
|
normalizeSavedToolProfiles,
|
||||||
|
upsertToolProfile,
|
||||||
|
} = require('../lib/tool-profiles');
|
||||||
|
|
||||||
|
test('tool profiles normalize, upsert, and dedupe by name', () => {
|
||||||
|
const profiles = normalizeSavedToolProfiles([
|
||||||
|
{ name: 'QA', toolProfile: 'custom', enabledToolCategories: 'assets\nlogs' },
|
||||||
|
{ name: 'qa', toolProfile: 'full', disabledTools: ['delete_asset'] },
|
||||||
|
{ name: '' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(profiles.length, 1);
|
||||||
|
assert.equal(profiles[0].name, 'qa');
|
||||||
|
assert.equal(profiles[0].toolProfile, 'full');
|
||||||
|
|
||||||
|
const updated = upsertToolProfile(profiles, {
|
||||||
|
name: 'Prototype',
|
||||||
|
toolProfile: 'custom',
|
||||||
|
enabledToolCategories: ['ui'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(updated.length, 2);
|
||||||
|
assert.equal(updated.some((profile) => profile.name === 'Prototype'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tool profiles snapshot and apply exposure config', () => {
|
||||||
|
const snapshot = createToolProfileSnapshot({
|
||||||
|
toolProfile: 'custom',
|
||||||
|
enabledToolCategories: ['assets'],
|
||||||
|
disabledToolCategories: ['input'],
|
||||||
|
enabledTools: ['write_file'],
|
||||||
|
disabledTools: ['delete_asset'],
|
||||||
|
}, 'Asset QA');
|
||||||
|
|
||||||
|
const applied = applyToolProfile({ port: 8765 }, snapshot);
|
||||||
|
assert.equal(applied.port, 8765);
|
||||||
|
assert.equal(applied.activeToolProfileName, 'Asset QA');
|
||||||
|
assert.deepEqual(applied.enabledToolCategories, ['assets']);
|
||||||
|
assert.deepEqual(applied.disabledTools, ['delete_asset']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tool profiles import, export, and delete', () => {
|
||||||
|
const imported = importToolProfiles([], JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
profiles: [
|
||||||
|
{ name: 'Core QA', toolProfile: 'core' },
|
||||||
|
{ name: 'Debug', toolProfile: 'custom', enabledToolCategories: ['logs', 'diagnostics'] },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.equal(imported.length, 2);
|
||||||
|
assert.deepEqual(exportToolProfiles(imported).profiles.map((profile) => profile.name), ['Core QA', 'Debug']);
|
||||||
|
|
||||||
|
const remaining = deleteToolProfile(imported, 'Debug');
|
||||||
|
assert.deepEqual(remaining.map((profile) => profile.name), ['Core QA']);
|
||||||
|
});
|
||||||
@@ -21,11 +21,13 @@ function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-
|
|||||||
|
|
||||||
test('core profile exposes the documented focused tool set', () => {
|
test('core profile exposes the documented focused tool set', () => {
|
||||||
const tools = createRegistry('core').listTools();
|
const tools = createRegistry('core').listTools();
|
||||||
assert.equal(tools.length, 34);
|
assert.equal(tools.length, 37);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
|
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'validate_scene'), true);
|
assert.equal(tools.some((tool) => tool.name === 'validate_scene'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'inspect_asset_dependencies'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'get_build_status'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_performance_snapshot'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_performance_snapshot'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), true);
|
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
||||||
@@ -34,10 +36,14 @@ test('core profile exposes the documented focused tool set', () => {
|
|||||||
|
|
||||||
test('full profile exposes all built-in tools', () => {
|
test('full profile exposes all built-in tools', () => {
|
||||||
const tools = createRegistry('full').listTools();
|
const tools = createRegistry('full').listTools();
|
||||||
assert.equal(tools.length, 89);
|
assert.equal(tools.length, 101);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
|
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
|
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);
|
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'create_cocos_mcp_project_skill'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'bind_button_click_event'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'open_build_panel'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'broadcast_editor_message'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
||||||
});
|
});
|
||||||
@@ -98,3 +104,34 @@ test('callToolDetailed preserves screenshot image text while keeping structured
|
|||||||
assert.equal(result.value.data.image, true);
|
assert.equal(result.value.data.image, true);
|
||||||
assert.equal(result.value.data.mimeType, 'image/png');
|
assert.equal(result.value.data.mimeType, 'image/png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('execute_javascript safety checks block risky editor snippets by default', async () => {
|
||||||
|
const registry = createRegistry('core');
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => registry.callToolDetailed('execute_javascript', {
|
||||||
|
context: 'editor',
|
||||||
|
code: "fs.rmSync(path.join(context.projectPath, 'assets'), { recursive: true });",
|
||||||
|
}),
|
||||||
|
/JavaScript safety checks blocked/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('execute_javascript safety checks can be explicitly disabled per call', async () => {
|
||||||
|
let called = false;
|
||||||
|
const registry = createRegistry('core', path.resolve('/tmp/funplay-cocos-test-project'), {}, {
|
||||||
|
editorExecutor: async () => {
|
||||||
|
called = true;
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await registry.callToolDetailed('execute_javascript', {
|
||||||
|
context: 'editor',
|
||||||
|
code: "fs.rmSync(path.join(context.projectPath, 'assets'), { recursive: true });",
|
||||||
|
safety_checks: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(called, true);
|
||||||
|
assert.equal(result.value.ok, true);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user