From 6405dced7da8681d29a3a89f0576561d51ebcf58 Mon Sep 17 00:00:00 2001
From: winlifes <110321103+Winlifes@users.noreply.github.com>
Date: Wed, 10 Jun 2026 20:26:09 -0700
Subject: [PATCH] Release v0.4.0
---
CHANGELOG.md | 33 ++++
README.md | 42 ++--
README_CN.md | 44 +++--
RELEASE_CHECKLIST.md | 1 +
browser.js | 25 ++-
docs/TOOLS.md | 44 ++++-
lib/config.js | 24 +++
lib/javascript-safety.js | 114 +++++++++++
lib/project-instructions.js | 18 ++
lib/server.js | 138 ++++++++++++-
lib/tool-profiles.js | 155 +++++++++++++++
lib/tool-registry.js | 63 +++++-
lib/tools/assets-advanced.js | 221 +++++++++++++++++++++
lib/tools/cocos-project.js | 245 +++++++++++++++++++++++
lib/tools/scene-events.js | 45 +++++
package.json | 4 +-
panel/index.js | 319 +++++++++++++++++++++++++++++-
scene.js | 113 +++++++++++
scripts/release.js | 32 +++
server.json | 4 +-
test/assets-advanced.test.js | 16 ++
test/cocos-project-tools.test.js | 89 +++++++++
test/javascript-safety.test.js | 58 ++++++
test/project-instructions.test.js | 11 ++
test/server.test.js | 70 ++++++-
test/tool-profiles.test.js | 66 +++++++
test/tool-registry.test.js | 41 +++-
27 files changed, 1983 insertions(+), 52 deletions(-)
create mode 100644 lib/javascript-safety.js
create mode 100644 lib/tool-profiles.js
create mode 100644 lib/tools/assets-advanced.js
create mode 100644 lib/tools/cocos-project.js
create mode 100644 lib/tools/scene-events.js
create mode 100644 test/assets-advanced.test.js
create mode 100644 test/cocos-project-tools.test.js
create mode 100644 test/javascript-safety.test.js
create mode 100644 test/tool-profiles.test.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9be8827..3ac8f9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,9 +6,42 @@ This project follows a simple changelog format inspired by [Keep a Changelog](ht
## [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 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
diff --git a/README.md b/README.md
index 04d6c2f..c99408a 100644
--- a/README.md
+++ b/README.md
@@ -65,16 +65,17 @@ Funplay > MCP Server
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:
- Enable or disable the MCP server
- Change the server port
- 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
- 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
- Copy quick `curl` commands for `/health` and `/tools`
- 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.
- 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.
-- 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.
- 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`.
+- `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.
- 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.
@@ -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
- **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
-- **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
## 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
- **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
@@ -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 |
| Primary execution tool | `execute_javascript` | `execute_code` |
| Primary language | JavaScript in scene/editor contexts | C# in Unity editor/runtime contexts |
-| Default profile | `core` with 34 tools | `core` focused tool profile |
-| Full profile | 89 tools plus `custom` exposure | 79 tools |
+| Default profile | `core` with 37 tools | `core` focused tool profile |
+| Full profile | 101 tools plus `custom` exposure | 79 tools |
| Client setup | One-click config panel | One-click config window |
## MCP Capabilities
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
- **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
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
@@ -307,24 +309,25 @@ The default `core` set is intentionally small: `execute_javascript`, `execute_sc
## 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 |
|----------|-------|
| **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` |
-| **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` |
| **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` |
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
| **Camera** | `list_cameras`, `create_camera`, `set_camera_properties` |
| **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` |
| **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` |
-| **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` |
## Primary Tool Examples
@@ -365,8 +368,11 @@ Place `funplay-cocos-mcp.config.json` in the Cocos project root:
"enabledTools": [],
"disabledTools": [],
"enableSessions": false,
+ "executeJavascriptSafetyChecks": true,
"autostart": true,
- "maxInteractionLogEntries": 50
+ "maxInteractionLogEntries": 50,
+ "activeToolProfileName": "",
+ "savedToolProfiles": []
}
```
@@ -376,7 +382,7 @@ Environment variables are also supported:
- `COCOS_MCP_PORT`
- `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
@@ -394,6 +400,12 @@ Cocos Creator Extension
│ └─ Minimal MCP Server panel
└─ lib/
├─ assets, diagnostics, screenshots, input
+ ├─ tool-profiles, javascript-safety
+ ├─ tools/
+ │ ├─ files
+ │ ├─ assets-advanced
+ │ ├─ cocos-project
+ │ └─ scene-events
└─ server, resources, prompts, tool registry
```
diff --git a/README_CN.md b/README_CN.md
index fa97fb0..d1537b8 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -65,16 +65,17 @@ Funplay > MCP Server
服务默认运行在 `http://127.0.0.1:8765/`。
-如果配置端口已被占用,扩展会自动回退到下一个可用本地端口,并在一键客户端配置时使用实际运行端口。
+如果配置端口已被占用,扩展会先检查已有 listener 是否属于同一个 Cocos 项目;同项目 listener 会被安全复用,无关 listener 才会自动回退到下一个可用本地端口。
面板刻意保持精简:
- 启用或停用 MCP Server
- 修改服务端口
- 在 `core` / `full` / `custom` 工具暴露模式之间切换
+- 保存、套用、导入和导出命名工具 profile
- 检查当前安装版本是否落后于 GitHub 最新 Release
- 查看最近工具调用和运行日志预览
-- 按工具分类或单个工具调整暴露范围
+- 通过分类控制或单个工具名调整暴露范围
- 一键配置 AI 客户端,并随目标客户端预览对应配置
- 复制 `/health` 和 `/tools` 的快速 `curl` 排障命令
- 需要时再展开 Debug Output
@@ -234,12 +235,13 @@ curl http://127.0.0.1:8765/tools
- 这是一个 **仅限 Editor** 的扩展,用于自动化 Cocos Creator,不会给最终游戏包添加运行时依赖。
- MCP Server 默认监听 `http://127.0.0.1:8765/`。
-- 如果配置端口被占用,服务会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
+- 如果配置端口被占用,服务会先通过项目身份识别同项目已有 listener;无法确认同项目时才会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
- `GET /health` 和 `GET /tools` 是只读调试端点,方便不用 MCP 客户端也能快速检查本地服务。
-- 默认 `core` profile 暴露 34 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 89 个工具;也可以用 `custom` 按分类或工具名增删。
+- 默认 `core` profile 暴露 37 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 101 个工具;也可以用 `custom` 按分类或工具名增删。
- 面板提供手动更新检查,会对比当前安装版本和 GitHub 最新 Release。
- 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。
+- `execute_javascript` 安全检查默认开启,会拦截明显高风险的文件系统和 shell 模式,例如删除/截断调用、原始写入流、路径穿越、用户/系统绝对路径和 `child_process`。这是防护栏,不是完整沙箱;确认风险后可在单次调用中显式传入 `safety_checks: false`。
- 所有已暴露的 MCP 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。
- 文件工具和 `cocos://asset/path/...` 资源默认只能访问当前 Cocos 项目根目录内的路径。
- 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。
@@ -251,12 +253,12 @@ curl http://127.0.0.1:8765/tools
- **嵌入式 Cocos 扩展** — Cocos 侧不需要单独 Python 守护进程或外部 bridge
- **一键客户端配置** — 在 Cocos Creator 内直接配置 Claude Code、Cursor、VS Code、Trae、Kiro、Codex
- **内建项目上下文** — 直接暴露项目、场景、选择、脚本诊断、日志和交互历史资源
-- **默认聚焦,必要时全量** — `core` 降低工具列表噪音,需要时切到 `full` 暴露全部工具;`custom` 可按分类或工具名调整
+- **默认聚焦,必要时全量** — `core` 降低工具列表噪音,需要时切到 `full` 暴露全部工具;`custom` 与保存的 profile 可按分类或工具名调整并恢复
- **可视化验证** — 截图和输入模拟让 AI 能验证 UI 与玩法改动
## 核心特性
-- **89 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制和输入模拟
+- **101 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、资产依赖、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制、构建/预览辅助、编辑器偏好、事件绑定和输入模拟
- **统一主工具** — `execute_javascript` 同时支持 `scene` 和 `editor` 两种上下文
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
- **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 |
| 主执行工具 | `execute_javascript` | `execute_code` |
| 主语言 | 场景/编辑器上下文中的 JavaScript | Unity 编辑器/运行态中的 C# |
-| 默认工具集 | `core`,34 个工具 | 聚焦版 `core` 工具集 |
-| 完整工具集 | 89 个工具,并支持 `custom` 暴露 | 79 个工具 |
+| 默认工具集 | `core`,37 个工具 | 聚焦版 `core` 工具集 |
+| 完整工具集 | 101 个工具,并支持 `custom` 暴露 | 79 个工具 |
| 客户端配置 | 一键配置面板 | 一键配置窗口 |
## MCP 能力结构
当前包提供四层能力:
-- **Tools** — `core` 下 34 个工具,`full` 下 89 个工具,并支持 `custom` include/exclude 规则
+- **Tools** — `core` 下 37 个工具,`full` 下 101 个工具,并支持 `custom` include/exclude 规则和命名工具 profile
- **Primary execution** — `execute_javascript` 用于场景/运行态和编辑器/browser 自动化
- **Prompts** — `fix_script_errors`、`create_playable_prototype`、`scene_validation`、`auto_wire_scene`
- **Resources** — 项目上下文、场景摘要、当前选择、脚本诊断、资产选择、日志和 MCP 交互历史
自动生成的工具参考文档见 [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
@@ -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` |
| **编辑器状态** | `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` |
| **节点编辑** | `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` |
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
| **相机** | `list_cameras`, `create_camera`, `set_camera_properties` |
| **动画** | `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` |
| **诊断与日志** | `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` |
-| **交互** | `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` |
## 主工具示例
@@ -365,8 +368,11 @@ Editor 上下文脚本可以访问 `Editor`、`fs`、`path`、`os`、`require`
"enabledTools": [],
"disabledTools": [],
"enableSessions": false,
+ "executeJavascriptSafetyChecks": true,
"autostart": true,
- "maxInteractionLogEntries": 50
+ "maxInteractionLogEntries": 50,
+ "activeToolProfileName": "",
+ "savedToolProfiles": []
}
```
@@ -376,7 +382,7 @@ Editor 上下文脚本可以访问 `Editor`、`fs`、`path`、`os`、`require`
- `COCOS_MCP_PORT`
- `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
└─ lib/
├─ assets, diagnostics, screenshots, input
+ ├─ tool-profiles, javascript-safety
+ ├─ tools/
+ │ ├─ files
+ │ ├─ assets-advanced
+ │ ├─ cocos-project
+ │ └─ scene-events
└─ server, resources, prompts, tool registry
```
diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md
index 3a77c64..05f6d2c 100644
--- a/RELEASE_CHECKLIST.md
+++ b/RELEASE_CHECKLIST.md
@@ -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 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/`
+- [ ] Release packaging sensitive-content scan does not find npm/GitHub/MCP tokens or private keys
- [ ] `release-manifest.json` references the correct GitHub download URL
- [ ] `SHA256SUMS.txt` includes the zip, manifest, generated release notes, and release README
diff --git a/browser.js b/browser.js
index 99ce598..65ae41e 100644
--- a/browser.js
+++ b/browser.js
@@ -5,7 +5,7 @@ const fs = require('fs');
const os = require('os');
const manifest = require('./package.json');
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 { createToolRegistry } = require('./lib/tool-registry');
const { ResourceProvider } = require('./lib/resources');
@@ -13,6 +13,7 @@ const { PromptProvider } = require('./lib/prompts');
const { InteractionLog } = require('./lib/interaction-log');
const { RuntimeLog } = require('./lib/runtime-log');
const { checkForUpdate } = require('./lib/update-checker');
+const { normalizeSavedToolProfiles } = require('./lib/tool-profiles');
const EXTENSION_NAME = manifest.name || 'funplay-cocos-mcp';
const LOG_PREFIX = '[Funplay Cocos MCP]';
@@ -101,6 +102,7 @@ class ExtensionService {
config: this.config,
projectPath: getProjectPath(),
projectName: getProjectName(),
+ projectIdentity: getProjectIdentity(),
cocosVersion: getCocosVersion(),
packagePath: path.dirname(__filename),
});
@@ -134,6 +136,8 @@ class ExtensionService {
promptProvider: this.promptProvider,
serverName: `Funplay Cocos MCP - ${getProjectName()}`,
serverVersion: manifest.version || '0.0.0',
+ projectName: getProjectName(),
+ projectIdentity: getProjectIdentity(),
});
await this.server.start();
@@ -178,8 +182,13 @@ class ExtensionService {
const fallbackInfo = this.server && this.server.isRunning() && typeof this.server.getPortFallbackInfo === 'function'
? this.server.getPortFallbackInfo()
: null;
+ const attachInfo = this.server && this.server.isRunning() && typeof this.server.getAttachInfo === 'function'
+ ? this.server.getAttachInfo()
+ : null;
return {
running: Boolean(this.server && this.server.isRunning()),
+ attachedToExisting: Boolean(attachInfo),
+ attachInfo,
host: this.config.host,
port: effective.port,
requestedPort: this.config.port,
@@ -191,10 +200,14 @@ class ExtensionService {
enabledToolCategories: this.config.enabledToolCategories,
disabledToolCategories: this.config.disabledToolCategories,
enableSessions: this.config.enableSessions,
+ executeJavascriptSafetyChecks: this.config.executeJavascriptSafetyChecks,
autostart: this.config.autostart,
+ activeToolProfileName: this.config.activeToolProfileName,
+ savedToolProfiles: this.config.savedToolProfiles,
version: manifest.version || '0.0.0',
projectPath: getProjectPath(),
projectName: getProjectName(),
+ projectIdentity: getProjectIdentity(),
cocosVersion: getCocosVersion(),
url: effective.url,
};
@@ -416,6 +429,9 @@ class ExtensionService {
enableSessions: partialConfig && typeof partialConfig.enableSessions === 'boolean'
? partialConfig.enableSessions
: this.config.enableSessions,
+ executeJavascriptSafetyChecks: partialConfig && typeof partialConfig.executeJavascriptSafetyChecks === 'boolean'
+ ? partialConfig.executeJavascriptSafetyChecks
+ : this.config.executeJavascriptSafetyChecks,
autostart: partialConfig && typeof partialConfig.autostart === 'boolean'
? partialConfig.autostart
: this.config.autostart,
@@ -425,6 +441,12 @@ class ExtensionService {
lastClientTargetId: partialConfig && partialConfig.lastClientTargetId
? String(partialConfig.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;
@@ -435,6 +457,7 @@ class ExtensionService {
nextConfig.port !== this.config.port ||
nextConfig.toolProfile !== this.config.toolProfile ||
nextConfig.enableSessions !== this.config.enableSessions ||
+ nextConfig.executeJavascriptSafetyChecks !== this.config.executeJavascriptSafetyChecks ||
JSON.stringify(nextConfig.enabledTools) !== JSON.stringify(this.config.enabledTools) ||
JSON.stringify(nextConfig.disabledTools) !== JSON.stringify(this.config.disabledTools) ||
JSON.stringify(nextConfig.enabledToolCategories) !== JSON.stringify(this.config.enabledToolCategories) ||
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
index b157330..b448fcf 100644
--- a/docs/TOOLS.md
+++ b/docs/TOOLS.md
@@ -2,18 +2,18 @@
-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 | Tool Count | Purpose |
|---|---:|---|
-| `core` | 34 | Focused default surface for common editor automation. |
-| `full` | 89 | All built-in tools, including destructive and low-level helpers. |
+| `core` | 37 | Focused default surface for common editor automation. |
+| `full` | 101 | All built-in tools, including destructive and low-level helpers. |
## 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
@@ -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. |
| `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_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. |
@@ -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. |
| `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
| 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. |
| `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_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
| 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 |
|---|---|---|---|
-| `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_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. |
@@ -103,6 +128,7 @@ Generated from `lib/tool-registry.js`. The default `core` profile exposes 34 too
| 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. |
| `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. |
@@ -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. |
| `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
| 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 |
|---|---|---|---|
-| `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. |
| `pause_runtime` | `full` | stateful | [core] Pause Cocos director game logic execution. |
| `resume_runtime` | `full` | stateful | [core] Resume Cocos director game logic execution. |
diff --git a/lib/config.js b/lib/config.js
index b61ea57..f6c3689 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -1,7 +1,9 @@
'use strict';
+const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
+const { normalizeSavedToolProfiles } = require('./tool-profiles');
const DEFAULTS = {
host: '127.0.0.1',
@@ -12,9 +14,12 @@ const DEFAULTS = {
enabledToolCategories: [],
disabledToolCategories: [],
enableSessions: false,
+ executeJavascriptSafetyChecks: true,
autostart: true,
maxInteractionLogEntries: 50,
lastClientTargetId: 'claude_code',
+ activeToolProfileName: '',
+ savedToolProfiles: [],
};
function getProjectPath() {
@@ -28,6 +33,19 @@ function getProjectName() {
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() {
if (global.Editor && Editor.App) {
if (typeof Editor.App.version === 'string' && Editor.App.version) {
@@ -106,11 +124,16 @@ function loadConfig() {
enabledToolCategories: normalizeStringList(fileConfig.enabledToolCategories).map((item) => item.toLowerCase()),
disabledToolCategories: normalizeStringList(fileConfig.disabledToolCategories).map((item) => item.toLowerCase()),
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,
maxInteractionLogEntries: Number.isInteger(fileConfig.maxInteractionLogEntries)
? Math.max(10, Math.min(500, fileConfig.maxInteractionLogEntries))
: DEFAULTS.maxInteractionLogEntries,
lastClientTargetId: normalizeClientTargetId(fileConfig.lastClientTargetId),
+ activeToolProfileName: typeof fileConfig.activeToolProfileName === 'string' ? fileConfig.activeToolProfileName : '',
+ savedToolProfiles: normalizeSavedToolProfiles(fileConfig.savedToolProfiles),
configPath,
configError: fileConfig.__error || '',
};
@@ -120,6 +143,7 @@ module.exports = {
DEFAULTS,
getProjectPath,
getProjectName,
+ getProjectIdentity,
getCocosVersion,
loadConfig,
normalizeProfile,
diff --git a/lib/javascript-safety.js b/lib/javascript-safety.js
new file mode 100644
index 0000000..ad0fe83
--- /dev/null
+++ b/lib/javascript-safety.js
@@ -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,
+};
diff --git a/lib/project-instructions.js b/lib/project-instructions.js
index a20c282..e2a051d 100644
--- a/lib/project-instructions.js
+++ b/lib/project-instructions.js
@@ -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 = {
KNOWN_INSTRUCTION_PATHS,
+ createCocosMcpProjectSkill,
createProjectSkill,
listProjectInstructions,
readProjectInstruction,
diff --git a/lib/server.js b/lib/server.js
index e11e627..4a01df4 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -84,7 +84,11 @@ class McpServer {
this.runtimeLog = options.runtimeLog;
this.serverName = options.serverName;
this.serverVersion = options.serverVersion;
+ this.projectName = options.projectName || '';
+ this.projectIdentity = options.projectIdentity || '';
this.server = null;
+ this.attached = false;
+ this.attachedInfo = null;
this.actualPort = null;
this.portFallbackInfo = null;
this.negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
@@ -93,10 +97,13 @@ class McpServer {
}
isRunning() {
- return Boolean(this.server && this.server.listening);
+ return Boolean(this.attached || (this.server && this.server.listening));
}
getPort() {
+ if (this.attached && this.actualPort) {
+ return this.actualPort;
+ }
if (this.server && typeof this.server.address === 'function') {
const address = this.server.address();
if (address && typeof address.port === 'number') {
@@ -114,6 +121,10 @@ class McpServer {
return this.portFallbackInfo;
}
+ getAttachInfo() {
+ return this.attachedInfo;
+ }
+
log(level, message) {
if (this.runtimeLog && typeof this.runtimeLog.add === 'function') {
this.runtimeLog.add(level, message);
@@ -137,13 +148,21 @@ class McpServer {
this.actualPort = null;
this.portFallbackInfo = null;
+ this.attached = false;
+ this.attachedInfo = null;
const requestHandler = async (request, response) => {
try {
const requestUrl = new URL(request.url || '/', 'http://localhost');
if (request.method === 'GET' && requestUrl.pathname === '/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') {
@@ -288,7 +307,12 @@ class McpServer {
return;
} catch (error) {
lastError = error;
+ candidate.removeAllListeners();
if (error && error.code === 'EADDRINUSE' && port < 65535 && attempt < MAX_PORT_FALLBACK_ATTEMPTS) {
+ if (await this.tryAttachToExisting(port)) {
+ return;
+ }
+
const nextPort = port + 1;
this.log(
'warn',
@@ -299,7 +323,6 @@ class McpServer {
continue;
}
- candidate.removeAllListeners();
break;
}
}
@@ -311,6 +334,15 @@ class McpServer {
}
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) {
this.log('info', 'Stop skipped: server object is empty.');
return;
@@ -321,6 +353,7 @@ class McpServer {
this.server = null;
this.actualPort = null;
this.portFallbackInfo = null;
+ this.attachedInfo = null;
await new Promise((resolve, reject) => {
active.close((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) {
return new Promise((resolve, reject) => {
const onError = (error) => {
@@ -559,6 +686,11 @@ class McpServer {
name: this.serverName,
version: this.serverVersion,
},
+ funplay: {
+ server: 'funplay-cocos-mcp',
+ projectName: this.projectName,
+ projectIdentity: this.projectIdentity,
+ },
capabilities: {
tools: {},
resources: {},
diff --git a/lib/tool-profiles.js b/lib/tool-profiles.js
new file mode 100644
index 0000000..0ca4911
--- /dev/null
+++ b/lib/tool-profiles.js
@@ -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,
+};
diff --git a/lib/tool-registry.js b/lib/tool-registry.js
index 5aa84f1..91911dc 100644
--- a/lib/tool-registry.js
+++ b/lib/tool-registry.js
@@ -23,6 +23,7 @@ const {
} = require('./logs');
const { resolveProjectPath } = require('./path-safety');
const {
+ createCocosMcpProjectSkill,
createProjectSkill,
listProjectInstructions,
readProjectInstruction,
@@ -36,14 +37,22 @@ const {
revertPrefabInstance,
validatePrefabReferences,
} = require('./prefabs');
+const { createAssetsAdvancedTools } = require('./tools/assets-advanced');
+const { createCocosProjectTools } = require('./tools/cocos-project');
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
+const { createSceneEventTools } = require('./tools/scene-events');
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
const { checkForUpdate } = require('./update-checker');
+const { assertJavascriptSafety } = require('./javascript-safety');
const { safeStringify } = require('./utils');
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
const TOOL_CATEGORY_RULES = [
['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/],
['logs', /log/],
['diagnostics', /diagnostic|validate/],
@@ -306,6 +315,30 @@ function toOutput(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) {
if (args && 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.' },
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.' },
+ safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
},
['context', 'code']
),
handler: async (args) => {
const context = String(args.context || '').toLowerCase();
+ const runtimeContext = getRuntimeContext();
+ assertToolJavascriptSafety(args, runtimeContext);
if (context === 'scene') {
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.' },
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']
),
- 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',
@@ -366,10 +406,12 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
{
code: { type: 'string', description: 'JavaScript code to execute inside the editor context.' },
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']
),
handler: async (args) => {
+ assertToolJavascriptSafety(args, getRuntimeContext());
if (typeof editorExecutor !== 'function') {
throw new Error('Editor JavaScript execution is unavailable.');
}
@@ -512,6 +554,22 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
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',
profile: 'core',
@@ -656,6 +714,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
inputSchema: createSchema({}, []),
handler: async () => getRuntimeContext(),
},
+ ...createCocosProjectTools({ createSchema }),
{
name: 'list_scenes',
profile: 'core',
@@ -961,6 +1020,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
return selectAsset(info.uuid || args.target);
},
},
+ ...createAssetsAdvancedTools({ createSchema, getRuntimeContext }),
{
name: 'get_editor_selection',
profile: 'full',
@@ -1463,6 +1523,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
),
handler: async (args) => sceneBridge.call('simulateButtonClick', args),
},
+ ...createSceneEventTools({ createSchema, sceneBridge }),
{
name: 'invoke_component_method',
profile: 'full',
diff --git a/lib/tools/assets-advanced.js b/lib/tools/assets-advanced.js
new file mode 100644
index 0000000..7a8a59d
--- /dev/null
+++ b/lib/tools/assets-advanced.js
@@ -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,
+};
diff --git a/lib/tools/cocos-project.js b/lib/tools/cocos-project.js
new file mode 100644
index 0000000..7f7b9ac
--- /dev/null
+++ b/lib/tools/cocos-project.js
@@ -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,
+};
diff --git a/lib/tools/scene-events.js b/lib/tools/scene-events.js
new file mode 100644
index 0000000..c902e15
--- /dev/null
+++ b/lib/tools/scene-events.js
@@ -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,
+};
diff --git a/package.json b/package.json
index cb5033c..71324d0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "funplay-cocos-mcp",
"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.",
"author": "Funplay",
"license": "MIT",
@@ -49,7 +49,7 @@
"access": "public"
},
"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",
"docs:generate": "node scripts/generate-tool-docs.js",
"docs:check": "node scripts/generate-tool-docs.js --check",
diff --git a/panel/index.js b/panel/index.js
index bfb806f..0b36fc3 100644
--- a/panel/index.js
+++ b/panel/index.js
@@ -51,6 +51,10 @@ module.exports = Editor.Panel.define({
Changes auto-save. Port/profile changes restart the server when needed.
@@ -64,6 +68,19 @@ module.exports = Editor.Panel.define({