Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63c18b0b5f | ||
|
|
c57cf140a8 |
@@ -75,3 +75,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
|
- name: Run release metadata checks
|
||||||
|
run: npm run release:check
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ Library/
|
|||||||
library/
|
library/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
.release-tmp/
|
||||||
|
releases/
|
||||||
|
|||||||
@@ -6,6 +6,31 @@ This project follows a simple changelog format inspired by [Keep a Changelog](ht
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.1] - 2026-05-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a documented release workflow and release checklist for Cocos extension publishing.
|
||||||
|
- Added release packaging scripts that generate a Cocos extension zip, release manifest, checksum file, and per-release README.
|
||||||
|
- Added CI validation for release metadata.
|
||||||
|
|
||||||
|
## [0.3.0] - 2026-05-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added MCP `outputSchema` and `annotations` to listed tools.
|
||||||
|
- Added a standard structured tool result envelope with `ok`, `tool`, `callId`, `summary`, `data`, and follow-up `refs`.
|
||||||
|
- Added prefab workflow tools: `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, and `revert_prefab_instance`.
|
||||||
|
- Added `get_performance_snapshot` and expanded `validate_scene` with scene scale/performance-oriented counters.
|
||||||
|
- Added project AI instruction tools: `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, and `create_project_skill`.
|
||||||
|
- Added tests for tool metadata, result envelopes, and project instruction helpers.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Expanded the default `core` profile from 28 tools to 34 tools.
|
||||||
|
- Expanded the `full` profile from 76 tools to 89 tools.
|
||||||
|
- Updated tool interaction logs to store concise result summaries from the standard envelope.
|
||||||
|
|
||||||
## [0.2.0] - 2026-05-20
|
## [0.2.0] - 2026-05-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ git clone https://github.com/FunplayAI/funplay-cocos-mcp.git extensions/funplay-
|
|||||||
|
|
||||||
Then restart Cocos Creator or reload extensions from the editor.
|
Then restart Cocos Creator or reload extensions from the editor.
|
||||||
|
|
||||||
|
For a non-git install, download `Funplay.CocosMcp.v<version>.zip` from the GitHub Releases page, unzip it, and move the extracted `funplay-cocos-mcp` folder into your project `extensions/` directory.
|
||||||
|
|
||||||
You can also install it globally by copying the folder into your Cocos Creator user extensions directory.
|
You can also install it globally by copying the folder into your Cocos Creator user extensions directory.
|
||||||
|
|
||||||
### 2. Start the MCP Server
|
### 2. Start the MCP Server
|
||||||
@@ -199,9 +201,10 @@ Try a higher-level prompt in your AI client:
|
|||||||
- This extension is **Editor-only**. It is meant to automate Cocos Creator, not to add runtime dependencies to your final game build.
|
- This extension is **Editor-only**. It is meant to automate Cocos Creator, not to add runtime dependencies to your final game build.
|
||||||
- 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.
|
||||||
- The default `core` profile exposes 28 high-signal tools. Switch to `full` for all 76 tools, or use `custom` to include/exclude tool categories and individual tools.
|
- 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 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`.
|
||||||
- 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.
|
||||||
@@ -218,7 +221,7 @@ Try a higher-level prompt in your AI client:
|
|||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **76 Built-in Tools** — Scene hierarchy, editor state, selection workflows, assets, UI creation, components, files, logs, script diagnostics, screenshots, runtime control, and input simulation
|
- **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
|
||||||
- **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 minimal `Funplay > MCP Server` panel for service management, update checks, tool exposure, and MCP client setup
|
- **Cocos Panel UI** — A minimal `Funplay > MCP Server` panel for service management, update checks, tool exposure, and MCP client setup
|
||||||
@@ -235,20 +238,20 @@ 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 28 tools | `core` focused tool profile |
|
| Default profile | `core` with 34 tools | `core` focused tool profile |
|
||||||
| Full profile | 76 tools plus `custom` exposure | 79 tools |
|
| Full profile | 89 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** — 28 tools in `core`, 76 tools in `full`, plus `custom` include/exclude rules
|
- **Tools** — 34 tools in `core`, 89 tools in `full`, plus `custom` include/exclude rules
|
||||||
- **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
|
||||||
|
|
||||||
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`, `set_selection`, `get_project_info`, `get_scene_info`, `get_hierarchy`, `list_scenes`, `open_scene`, `list_assets`, `inspect_asset`, `open_asset`, `select_asset`, `run_script_diagnostics`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `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_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`.
|
||||||
|
|
||||||
## Built-in Resources
|
## Built-in Resources
|
||||||
|
|
||||||
@@ -267,21 +270,22 @@ The default `core` set is intentionally small: `execute_javascript`, `execute_sc
|
|||||||
|
|
||||||
## Built-in Tools
|
## Built-in Tools
|
||||||
|
|
||||||
Funplay MCP for Cocos currently ships with **76 tool functions** in the `full` profile:
|
Funplay MCP for Cocos currently ships with **89 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 & 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`, `instantiate_prefab`, `get_editor_selection` |
|
| **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` |
|
||||||
| **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` |
|
| **Diagnostics & Logs** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
||||||
| **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** | `emit_node_event`, `simulate_button_click`, `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` |
|
||||||
@@ -360,12 +364,22 @@ The server speaks MCP-style HTTP JSON-RPC 2.0 and supports tools, resources, res
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Run a syntax check before publishing changes:
|
Run checks before publishing changes:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run check
|
npm run check
|
||||||
|
npm test
|
||||||
|
npm run release:check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To generate a GitHub Release-ready extension package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release:package
|
||||||
|
```
|
||||||
|
|
||||||
|
The package is written to `releases/<version>/` with a zip, manifest, checksum file, and release README. See [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) and [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md) for the full process.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License. See [LICENSE](./LICENSE).
|
MIT License. See [LICENSE](./LICENSE).
|
||||||
|
|||||||
+24
-10
@@ -51,6 +51,8 @@ git clone https://github.com/FunplayAI/funplay-cocos-mcp.git extensions/funplay-
|
|||||||
|
|
||||||
然后重启 Cocos Creator,或在编辑器里重新加载扩展。
|
然后重启 Cocos Creator,或在编辑器里重新加载扩展。
|
||||||
|
|
||||||
|
如果不想用 git 安装,可以从 GitHub Releases 下载 `Funplay.CocosMcp.v<version>.zip`,解压后把 `funplay-cocos-mcp` 目录移动到项目的 `extensions/` 目录。
|
||||||
|
|
||||||
你也可以把目录复制到 Cocos Creator 的全局用户扩展目录中。
|
你也可以把目录复制到 Cocos Creator 的全局用户扩展目录中。
|
||||||
|
|
||||||
### 2. 启动 MCP Server
|
### 2. 启动 MCP Server
|
||||||
@@ -199,9 +201,10 @@ url = "http://127.0.0.1:8765/"
|
|||||||
- 这是一个 **仅限 Editor** 的扩展,用于自动化 Cocos Creator,不会给最终游戏包添加运行时依赖。
|
- 这是一个 **仅限 Editor** 的扩展,用于自动化 Cocos Creator,不会给最终游戏包添加运行时依赖。
|
||||||
- MCP Server 默认监听 `http://127.0.0.1:8765/`。
|
- MCP Server 默认监听 `http://127.0.0.1:8765/`。
|
||||||
- 如果配置端口被占用,服务会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
|
- 如果配置端口被占用,服务会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
|
||||||
- 默认 `core` profile 暴露 28 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 76 个工具;也可以用 `custom` 按分类或工具名增删。
|
- 默认 `core` profile 暴露 34 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 89 个工具;也可以用 `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 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。
|
- 所有已暴露的 MCP 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。
|
||||||
- 文件工具和 `cocos://asset/path/...` 资源默认只能访问当前 Cocos 项目根目录内的路径。
|
- 文件工具和 `cocos://asset/path/...` 资源默认只能访问当前 Cocos 项目根目录内的路径。
|
||||||
- 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。
|
- 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。
|
||||||
@@ -218,7 +221,7 @@ url = "http://127.0.0.1:8765/"
|
|||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
|
|
||||||
- **76 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、资产、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制和输入模拟
|
- **89 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制和输入模拟
|
||||||
- **统一主工具** — `execute_javascript` 同时支持 `scene` 和 `editor` 两种上下文
|
- **统一主工具** — `execute_javascript` 同时支持 `scene` 和 `editor` 两种上下文
|
||||||
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
|
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
|
||||||
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露和 MCP 客户端配置
|
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露和 MCP 客户端配置
|
||||||
@@ -235,20 +238,20 @@ 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`,28 个工具 | 聚焦版 `core` 工具集 |
|
| 默认工具集 | `core`,34 个工具 | 聚焦版 `core` 工具集 |
|
||||||
| 完整工具集 | 76 个工具,并支持 `custom` 暴露 | 79 个工具 |
|
| 完整工具集 | 89 个工具,并支持 `custom` 暴露 | 79 个工具 |
|
||||||
| 客户端配置 | 一键配置面板 | 一键配置窗口 |
|
| 客户端配置 | 一键配置面板 | 一键配置窗口 |
|
||||||
|
|
||||||
## MCP 能力结构
|
## MCP 能力结构
|
||||||
|
|
||||||
当前包提供四层能力:
|
当前包提供四层能力:
|
||||||
|
|
||||||
- **Tools** — `core` 下 28 个工具,`full` 下 76 个工具,并支持 `custom` include/exclude 规则
|
- **Tools** — `core` 下 34 个工具,`full` 下 89 个工具,并支持 `custom` include/exclude 规则
|
||||||
- **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 交互历史
|
||||||
|
|
||||||
当前默认 `core` 工具集刻意保持精简,只包含:`execute_javascript`、`execute_scene_script`、`execute_editor_script`、`get_editor_state`、`get_tool_catalog`、`check_for_updates`、`get_selection`、`set_selection`、`get_project_info`、`get_scene_info`、`get_hierarchy`、`list_scenes`、`open_scene`、`list_assets`、`inspect_asset`、`open_asset`、`select_asset`、`run_script_diagnostics`、`get_recent_logs`、`search_project_logs`、`clear_logs`、`validate_scene`、`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_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`。
|
||||||
|
|
||||||
## 内置 Resources
|
## 内置 Resources
|
||||||
|
|
||||||
@@ -267,21 +270,22 @@ Funplay MCP for Cocos 延续 Funplay MCP for Unity 的设计原则,并针对 C
|
|||||||
|
|
||||||
## 内置工具
|
## 内置工具
|
||||||
|
|
||||||
Funplay MCP for Cocos 当前在 `full` profile 下提供 **76 个工具函数**:
|
Funplay MCP for Cocos 当前在 `full` profile 下提供 **89 个工具函数**:
|
||||||
|
|
||||||
| 分类 | 工具 |
|
| 分类 | 工具 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **脚本执行** | `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` |
|
||||||
| **项目与场景** | `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`, `instantiate_prefab`, `get_editor_selection` |
|
| **资产与 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` |
|
||||||
| **组件** | `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` |
|
| **诊断与日志** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
||||||
| **运行态** | `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`, `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` |
|
||||||
@@ -360,12 +364,22 @@ Cocos Creator Extension
|
|||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
发布改动前可以跑语法检查:
|
发布改动前可以跑检查:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run check
|
npm run check
|
||||||
|
npm test
|
||||||
|
npm run release:check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
生成可上传到 GitHub Release 的扩展包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release:package
|
||||||
|
```
|
||||||
|
|
||||||
|
产物会写入 `releases/<version>/`,包含 zip、manifest、checksum 和 release README。完整流程见 [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) 和 [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md)。
|
||||||
|
|
||||||
## 协议
|
## 协议
|
||||||
|
|
||||||
MIT License。详见 [LICENSE](./LICENSE)。
|
MIT License。详见 [LICENSE](./LICENSE)。
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Release Checklist
|
||||||
|
|
||||||
|
Use this checklist before publishing a new release of Funplay MCP for Cocos.
|
||||||
|
|
||||||
|
## 1. Repository Hygiene
|
||||||
|
|
||||||
|
- [ ] `git status` contains only intended release changes
|
||||||
|
- [ ] No tracked local junk is present (`.DS_Store`, `.idea/`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`)
|
||||||
|
- [ ] `package.json` version matches the intended release
|
||||||
|
- [ ] `CHANGELOG.md` includes the release notes for the target version
|
||||||
|
- [ ] `README.md` and `README_CN.md` match the current product behavior
|
||||||
|
|
||||||
|
## 2. Automated Verification
|
||||||
|
|
||||||
|
- [ ] `npm run check` passes
|
||||||
|
- [ ] `npm test` passes
|
||||||
|
- [ ] `npm run release:check` passes
|
||||||
|
- [ ] `npm run release:package` creates `releases/<version>/`
|
||||||
|
- [ ] `shasum -a 256 -c releases/<version>/SHA256SUMS.txt` passes
|
||||||
|
|
||||||
|
## 3. Package Contents
|
||||||
|
|
||||||
|
- [ ] The zip is named `Funplay.CocosMcp.v<version>.zip`
|
||||||
|
- [ ] The zip contains a single top-level `funplay-cocos-mcp/` folder
|
||||||
|
- [ ] The zip contains runtime files: `package.json`, `browser.js`, `scene.js`, `panel/`, and `lib/`
|
||||||
|
- [ ] 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-manifest.json` references the correct GitHub download URL
|
||||||
|
- [ ] `SHA256SUMS.txt` includes the zip, manifest, and release README
|
||||||
|
|
||||||
|
## 4. Cocos Smoke Test
|
||||||
|
|
||||||
|
- [ ] Test in a clean Cocos Creator `3.8+` project
|
||||||
|
- [ ] Install from the generated zip into `<project>/extensions/funplay-cocos-mcp`
|
||||||
|
- [ ] Restart Cocos Creator or reload extensions
|
||||||
|
- [ ] Open `Funplay > MCP Server`
|
||||||
|
- [ ] Start the MCP server successfully
|
||||||
|
- [ ] If the configured port is already in use, verify automatic fallback is reported clearly
|
||||||
|
- [ ] Run a read-only tool such as `get_project_info`
|
||||||
|
- [ ] Run a scene inspection tool such as `get_scene_info`
|
||||||
|
- [ ] Run a screenshot tool when the editor has a visible scene or preview
|
||||||
|
- [ ] Verify interaction logs appear in the MCP Server panel
|
||||||
|
|
||||||
|
## 5. MCP Client Verification
|
||||||
|
|
||||||
|
- [ ] Verify at least one primary client can connect (`Claude Code`, `Cursor`, `Codex`, etc.)
|
||||||
|
- [ ] Confirm `tools/list` returns the expected `core` profile tools
|
||||||
|
- [ ] Confirm a tool call succeeds end-to-end from the external client
|
||||||
|
- [ ] Verify one-click config output still matches the documented config snippets
|
||||||
|
|
||||||
|
## 6. GitHub Release Readiness
|
||||||
|
|
||||||
|
- [ ] CI passes on `main`
|
||||||
|
- [ ] Release commit message is `Release v<version>`
|
||||||
|
- [ ] Tag is `v<version>`
|
||||||
|
- [ ] GitHub Release title is `v<version>`
|
||||||
|
- [ ] GitHub Release includes the zip, manifest, checksum file, and release README
|
||||||
|
- [ ] Public GitHub Release page renders the release notes and assets correctly
|
||||||
|
|
||||||
|
## 7. Publish
|
||||||
|
|
||||||
|
- [ ] Commit the release changes
|
||||||
|
- [ ] Create and push the release tag
|
||||||
|
- [ ] Create or update the GitHub Release
|
||||||
|
- [ ] Upload generated release assets
|
||||||
|
- [ ] Verify the GitHub Release asset list
|
||||||
|
|
||||||
|
## 8. Post-Release
|
||||||
|
|
||||||
|
- [ ] Re-test installation from the public GitHub Release zip
|
||||||
|
- [ ] Check the update checker reports the new latest version
|
||||||
|
- [ ] Check README install instructions and download links
|
||||||
|
- [ ] Announce the release where appropriate
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
# Funplay Cocos MCP Release Workflow
|
||||||
|
|
||||||
|
This document records the release workflow for publishing Funplay MCP for Cocos to:
|
||||||
|
|
||||||
|
- Git tags
|
||||||
|
- GitHub Releases
|
||||||
|
- Downloadable Cocos Creator extension zip packages
|
||||||
|
|
||||||
|
## Published Identity
|
||||||
|
|
||||||
|
- GitHub repository: `https://github.com/FunplayAI/funplay-cocos-mcp`
|
||||||
|
- Git tag format: `v<version>`
|
||||||
|
- GitHub Release tag: `v<version>`
|
||||||
|
- Extension package asset: `Funplay.CocosMcp.v<version>.zip`
|
||||||
|
- Cocos extension folder name inside the zip: `funplay-cocos-mcp`
|
||||||
|
- Default local MCP endpoint: `http://127.0.0.1:8765/`
|
||||||
|
|
||||||
|
## Version Alignment Rule
|
||||||
|
|
||||||
|
Keep these versions aligned:
|
||||||
|
|
||||||
|
- `package.json` `version`
|
||||||
|
- `CHANGELOG.md` release section
|
||||||
|
- Git tag `v<version>`
|
||||||
|
- GitHub Release `v<version>`
|
||||||
|
- `releases/<version>/release-manifest.json` `version`
|
||||||
|
- Release zip filename `Funplay.CocosMcp.v<version>.zip`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
- `package.json`: `0.3.1`
|
||||||
|
- Git tag: `v0.3.1`
|
||||||
|
- GitHub Release: `v0.3.1`
|
||||||
|
- Release asset: `Funplay.CocosMcp.v0.3.1.zip`
|
||||||
|
|
||||||
|
## Files To Update For A New Release
|
||||||
|
|
||||||
|
Update:
|
||||||
|
|
||||||
|
1. `package.json`
|
||||||
|
- `"version": "<version>"`
|
||||||
|
2. `CHANGELOG.md`
|
||||||
|
- add a dated release notes block
|
||||||
|
|
||||||
|
Optional but recommended:
|
||||||
|
|
||||||
|
3. `README.md`
|
||||||
|
4. `README_CN.md`
|
||||||
|
5. GitHub Release notes text
|
||||||
|
|
||||||
|
## Release Steps
|
||||||
|
|
||||||
|
### 1. Verify Working Tree
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short --branch
|
||||||
|
```
|
||||||
|
|
||||||
|
The tree should contain only intentional release changes.
|
||||||
|
|
||||||
|
### 2. Update Versions And Notes
|
||||||
|
|
||||||
|
Update `package.json` and `CHANGELOG.md`.
|
||||||
|
|
||||||
|
Use semantic versions such as `0.3.1`, and keep release headings in this format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [0.3.1] - 2026-05-20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run Release Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release:verify
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs:
|
||||||
|
|
||||||
|
- JavaScript syntax checks
|
||||||
|
- Node.js tests
|
||||||
|
- release metadata validation
|
||||||
|
- release package generation
|
||||||
|
|
||||||
|
The generated local artifacts are written to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
releases/<version>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected contents:
|
||||||
|
|
||||||
|
- `Funplay.CocosMcp.v<version>.zip`
|
||||||
|
- `release-manifest.json`
|
||||||
|
- `SHA256SUMS.txt`
|
||||||
|
- `README.md`
|
||||||
|
|
||||||
|
### 4. Inspect The Package
|
||||||
|
|
||||||
|
The release script validates that every archive path stays under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
funplay-cocos-mcp/
|
||||||
|
```
|
||||||
|
|
||||||
|
The package must not contain local/build content such as:
|
||||||
|
|
||||||
|
- `.git/`
|
||||||
|
- `.github/`
|
||||||
|
- `.DS_Store`
|
||||||
|
- `node_modules/`
|
||||||
|
- `Library/`
|
||||||
|
- `Temp/`
|
||||||
|
- `dist/`
|
||||||
|
- `build/`
|
||||||
|
- `test/`
|
||||||
|
- `scripts/`
|
||||||
|
|
||||||
|
Verify checksums:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd releases/<version>
|
||||||
|
shasum -a 256 -c SHA256SUMS.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Commit, Tag, And Push
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Release v<version>"
|
||||||
|
git tag v<version>
|
||||||
|
git push origin main
|
||||||
|
git push origin v<version>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Create GitHub Release
|
||||||
|
|
||||||
|
Regenerate the final release artifacts from the tagged clean commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release:package -- --strict-tag
|
||||||
|
```
|
||||||
|
|
||||||
|
If creating a new release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh release create v<version> \
|
||||||
|
-R FunplayAI/funplay-cocos-mcp \
|
||||||
|
--title "v<version>" \
|
||||||
|
--notes-file /path/to/release-notes.md \
|
||||||
|
releases/<version>/Funplay.CocosMcp.v<version>.zip \
|
||||||
|
releases/<version>/release-manifest.json \
|
||||||
|
releases/<version>/SHA256SUMS.txt \
|
||||||
|
releases/<version>/README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
If the release already exists and only assets need to be replaced:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh release upload v<version> \
|
||||||
|
-R FunplayAI/funplay-cocos-mcp \
|
||||||
|
--clobber \
|
||||||
|
releases/<version>/Funplay.CocosMcp.v<version>.zip \
|
||||||
|
releases/<version>/release-manifest.json \
|
||||||
|
releases/<version>/SHA256SUMS.txt \
|
||||||
|
releases/<version>/README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Verify GitHub Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh release view v<version> \
|
||||||
|
-R FunplayAI/funplay-cocos-mcp \
|
||||||
|
--json url,assets,isDraft,isPrerelease,publishedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm the release has all four assets.
|
||||||
|
|
||||||
|
### 8. Post-Release Smoke Test
|
||||||
|
|
||||||
|
Test the package from the public GitHub Release:
|
||||||
|
|
||||||
|
1. Download `Funplay.CocosMcp.v<version>.zip`.
|
||||||
|
2. Unzip it.
|
||||||
|
3. Move `funplay-cocos-mcp` into a Cocos project `extensions/` directory.
|
||||||
|
4. Restart Cocos Creator or reload extensions.
|
||||||
|
5. Open `Funplay > MCP Server`.
|
||||||
|
6. Start the MCP server.
|
||||||
|
7. Connect an MCP client and call `get_project_info`.
|
||||||
|
|
||||||
|
## Current Verification Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release:verify
|
||||||
|
gh release view v<version> -R FunplayAI/funplay-cocos-mcp --json url,assets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Failure Cases
|
||||||
|
|
||||||
|
### Release validation says the changelog section is missing
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
|
||||||
|
- `CHANGELOG.md` does not contain `## [<version>] - YYYY-MM-DD`.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Add a dated release section before packaging.
|
||||||
|
|
||||||
|
### `zip` command is missing
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
|
||||||
|
- The local environment does not have the `zip` CLI installed.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Install `zip`, then rerun `npm run release:package`.
|
||||||
|
|
||||||
|
### GitHub Release upload replaces the wrong assets
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
|
||||||
|
- The version directory or release tag does not match `package.json` version.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Rerun `npm run release:check`.
|
||||||
|
- Confirm the command uses `releases/<version>/` and `v<version>`.
|
||||||
+269
@@ -0,0 +1,269 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { listAssets, queryAssetData, queryAssetInfo, queryAssetMeta } = require('./assets');
|
||||||
|
const { resolveProjectPath } = require('./path-safety');
|
||||||
|
|
||||||
|
function requestEditorMessage(channel, method, ...args) {
|
||||||
|
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
||||||
|
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
|
||||||
|
}
|
||||||
|
return Editor.Message.request(channel, method, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = path.isAbsolute(candidate)
|
||||||
|
? resolveProjectPath(projectPath, candidate)
|
||||||
|
: resolveProjectPath(projectPath, candidate);
|
||||||
|
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUuidReferences(value, refs = [], pointer = '') {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item, index) => collectUuidReferences(item, refs, `${pointer}/${index}`));
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, child] of Object.entries(value)) {
|
||||||
|
const childPointer = `${pointer}/${key}`;
|
||||||
|
if (
|
||||||
|
typeof child === 'string' &&
|
||||||
|
(key.toLowerCase().includes('uuid') || key === '__uuid__' || key === 'assetUuid' || key === 'prefabUuid')
|
||||||
|
) {
|
||||||
|
refs.push({ uuid: child, path: childPointer, key });
|
||||||
|
} else {
|
||||||
|
collectUuidReferences(child, refs, childPointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByJsonPath(target, jsonPath) {
|
||||||
|
const segments = String(jsonPath || '')
|
||||||
|
.replace(/^\//, '')
|
||||||
|
.split(/[/.]/)
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
let current = target;
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (current == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
current = current[segment];
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setByJsonPath(target, jsonPath, value) {
|
||||||
|
const segments = String(jsonPath || '')
|
||||||
|
.replace(/^\//, '')
|
||||||
|
.split(/[/.]/)
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!segments.length) {
|
||||||
|
throw new Error('jsonPath is required.');
|
||||||
|
}
|
||||||
|
let current = target;
|
||||||
|
for (let index = 0; index < segments.length - 1; index += 1) {
|
||||||
|
const segment = segments[index];
|
||||||
|
if (current[segment] == null || typeof current[segment] !== 'object') {
|
||||||
|
current[segment] = {};
|
||||||
|
}
|
||||||
|
current = current[segment];
|
||||||
|
}
|
||||||
|
current[segments[segments.length - 1]] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspectPrefab(projectPath, target) {
|
||||||
|
const info = await queryAssetInfo(target);
|
||||||
|
const meta = await queryAssetMeta(target).catch(() => null);
|
||||||
|
const data = await queryAssetData(target).catch(() => null);
|
||||||
|
const filePath = assetFilePath(projectPath, info);
|
||||||
|
const content = filePath ? fs.readFileSync(filePath, 'utf8') : '';
|
||||||
|
const parsed = content ? JSON.parse(content) : data;
|
||||||
|
const references = collectUuidReferences(parsed).slice(0, 500);
|
||||||
|
|
||||||
|
return {
|
||||||
|
info,
|
||||||
|
meta,
|
||||||
|
filePath: filePath ? path.relative(projectPath, filePath).replace(/\\/g, '/') : '',
|
||||||
|
referenceCount: references.length,
|
||||||
|
references,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validatePrefabReferences(projectPath, options = {}) {
|
||||||
|
const targets = options.target
|
||||||
|
? [options.target]
|
||||||
|
: (await listAssets({ pattern: options.pattern || 'db://assets/**', ccType: 'cc.Prefab' }))
|
||||||
|
.slice(0, Number.isFinite(options.limit) ? Math.max(1, Math.min(200, options.limit)) : 50)
|
||||||
|
.map((asset) => asset.uuid || asset.url)
|
||||||
|
.filter(Boolean);
|
||||||
|
const prefabs = [];
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const prefab = await inspectPrefab(projectPath, target);
|
||||||
|
const checked = [];
|
||||||
|
const missing = [];
|
||||||
|
for (const ref of prefab.references) {
|
||||||
|
try {
|
||||||
|
const info = await queryAssetInfo(ref.uuid);
|
||||||
|
checked.push({ ...ref, exists: true, asset: { uuid: info.uuid, url: info.url, type: info.type } });
|
||||||
|
} catch (error) {
|
||||||
|
missing.push({ ...ref, exists: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefabs.push({
|
||||||
|
target,
|
||||||
|
filePath: prefab.filePath,
|
||||||
|
referenceCount: prefab.referenceCount,
|
||||||
|
checkedCount: checked.length + missing.length,
|
||||||
|
missingCount: missing.length,
|
||||||
|
missing,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingCount = prefabs.reduce((sum, prefab) => sum + prefab.missingCount, 0);
|
||||||
|
return {
|
||||||
|
ok: missingCount === 0,
|
||||||
|
prefabCount: prefabs.length,
|
||||||
|
missingCount,
|
||||||
|
prefabs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function duplicatePrefab(projectPath, options = {}) {
|
||||||
|
const source = String(options.source || '').trim();
|
||||||
|
const target = String(options.target || '').trim();
|
||||||
|
if (!source || !target) {
|
||||||
|
throw new Error('source and target are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await queryAssetInfo(source);
|
||||||
|
const sourcePath = assetFilePath(projectPath, info);
|
||||||
|
if (!sourcePath) {
|
||||||
|
throw new Error(`Prefab source file was not found: ${source}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = resolveProjectPath(projectPath, target.endsWith('.prefab') ? target : `${target}.prefab`);
|
||||||
|
const assetsRoot = path.join(projectPath, 'assets');
|
||||||
|
const relativeToAssets = path.relative(assetsRoot, targetPath);
|
||||||
|
if (relativeToAssets.startsWith('..') || path.isAbsolute(relativeToAssets)) {
|
||||||
|
throw new Error('target must be inside the Cocos assets directory.');
|
||||||
|
}
|
||||||
|
if (fs.existsSync(targetPath) && options.overwrite !== true) {
|
||||||
|
throw new Error(`Target prefab already exists: ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||||
|
fs.copyFileSync(sourcePath, targetPath);
|
||||||
|
return {
|
||||||
|
duplicated: true,
|
||||||
|
source: path.relative(projectPath, sourcePath).replace(/\\/g, '/'),
|
||||||
|
target: path.relative(projectPath, targetPath).replace(/\\/g, '/'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editPrefabJson(projectPath, options = {}) {
|
||||||
|
const target = String(options.target || '').trim();
|
||||||
|
if (!target) {
|
||||||
|
throw new Error('target is required.');
|
||||||
|
}
|
||||||
|
const info = await queryAssetInfo(target);
|
||||||
|
const filePath = assetFilePath(projectPath, info);
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error(`Prefab file was not found: ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const original = fs.readFileSync(filePath, 'utf8');
|
||||||
|
let updated = original;
|
||||||
|
if (options.search !== undefined) {
|
||||||
|
const search = String(options.search);
|
||||||
|
if (!search) {
|
||||||
|
throw new Error('search must not be empty.');
|
||||||
|
}
|
||||||
|
if (!original.includes(search)) {
|
||||||
|
throw new Error('search text was not found in prefab file.');
|
||||||
|
}
|
||||||
|
updated = options.replaceAll
|
||||||
|
? original.split(search).join(String(options.replace || ''))
|
||||||
|
: original.replace(search, String(options.replace || ''));
|
||||||
|
} else {
|
||||||
|
const json = JSON.parse(original);
|
||||||
|
const value = JSON.parse(String(options.valueJson || 'null'));
|
||||||
|
setByJsonPath(json, options.jsonPath, value);
|
||||||
|
updated = JSON.stringify(json, null, 2) + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON.parse(updated);
|
||||||
|
if (options.createBackup) {
|
||||||
|
fs.writeFileSync(`${filePath}.bak`, original, 'utf8');
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, updated, 'utf8');
|
||||||
|
return {
|
||||||
|
edited: true,
|
||||||
|
path: path.relative(projectPath, filePath).replace(/\\/g, '/'),
|
||||||
|
oldValue: options.jsonPath ? getByJsonPath(JSON.parse(original), options.jsonPath) : undefined,
|
||||||
|
validation: await validatePrefabReferences(projectPath, { target }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPrefabInstance(nodeUuid) {
|
||||||
|
const uuid = String(nodeUuid || '').trim();
|
||||||
|
if (!uuid) {
|
||||||
|
throw new Error('node uuid is required.');
|
||||||
|
}
|
||||||
|
const result = await requestEditorMessage('scene', 'apply-prefab', uuid);
|
||||||
|
return { applied: true, uuid, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revertPrefabInstance(nodeUuid) {
|
||||||
|
const uuid = String(nodeUuid || '').trim();
|
||||||
|
if (!uuid) {
|
||||||
|
throw new Error('node uuid is required.');
|
||||||
|
}
|
||||||
|
const candidates = ['revert-prefab', 'restore-prefab'];
|
||||||
|
let lastError = null;
|
||||||
|
for (const method of candidates) {
|
||||||
|
try {
|
||||||
|
const result = await requestEditorMessage('scene', method, uuid);
|
||||||
|
return { reverted: true, uuid, method, result };
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError || new Error('No prefab revert editor message was available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
applyPrefabInstance,
|
||||||
|
duplicatePrefab,
|
||||||
|
editPrefabJson,
|
||||||
|
inspectPrefab,
|
||||||
|
revertPrefabInstance,
|
||||||
|
validatePrefabReferences,
|
||||||
|
};
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { resolveProjectPath } = require('./path-safety');
|
||||||
|
|
||||||
|
const KNOWN_INSTRUCTION_PATHS = [
|
||||||
|
'AGENTS.md',
|
||||||
|
'CLAUDE.md',
|
||||||
|
'GEMINI.md',
|
||||||
|
'.cursorrules',
|
||||||
|
'.windsurfrules',
|
||||||
|
'.github/copilot-instructions.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeSkillName(value) {
|
||||||
|
const normalized = String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9_-]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error('skillName is required.');
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statFile(filePath) {
|
||||||
|
try {
|
||||||
|
return fs.statSync(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listSkillFiles(projectPath) {
|
||||||
|
const skillRoot = resolveProjectPath(projectPath, '.codex/skills');
|
||||||
|
if (!fs.existsSync(skillRoot)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const skills = [];
|
||||||
|
const stack = [skillRoot];
|
||||||
|
while (stack.length) {
|
||||||
|
const current = stack.pop();
|
||||||
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||||
|
const fullPath = path.join(current, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
stack.push(fullPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.name === 'SKILL.md') {
|
||||||
|
const stat = statFile(fullPath);
|
||||||
|
skills.push({
|
||||||
|
path: path.relative(projectPath, fullPath).replace(/\\/g, '/'),
|
||||||
|
size: stat ? stat.size : 0,
|
||||||
|
mtime: stat ? stat.mtime.toISOString() : '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skills.sort((left, right) => left.path.localeCompare(right.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function listProjectInstructions(projectPath) {
|
||||||
|
const files = [];
|
||||||
|
for (const relativePath of KNOWN_INSTRUCTION_PATHS) {
|
||||||
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
||||||
|
const stat = statFile(fullPath);
|
||||||
|
if (stat && stat.isFile()) {
|
||||||
|
files.push({
|
||||||
|
path: relativePath,
|
||||||
|
size: stat.size,
|
||||||
|
mtime: stat.mtime.toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
skills: listSkillFiles(projectPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readProjectInstruction(projectPath, target) {
|
||||||
|
const relativePath = String(target || '').trim();
|
||||||
|
if (!relativePath) {
|
||||||
|
throw new Error('target is required.');
|
||||||
|
}
|
||||||
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
||||||
|
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
|
||||||
|
throw new Error(`Instruction file not found: ${relativePath}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: relativePath,
|
||||||
|
content: fs.readFileSync(fullPath, 'utf8'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeProjectInstruction(projectPath, options = {}) {
|
||||||
|
const relativePath = String(options.target || '').trim();
|
||||||
|
if (!relativePath) {
|
||||||
|
throw new Error('target is required.');
|
||||||
|
}
|
||||||
|
const content = String(options.content || '');
|
||||||
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
||||||
|
if (fs.existsSync(fullPath) && options.overwrite === false) {
|
||||||
|
throw new Error(`Instruction file already exists: ${relativePath}`);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, content, 'utf8');
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
return {
|
||||||
|
written: true,
|
||||||
|
path: relativePath,
|
||||||
|
size: stat.size,
|
||||||
|
mtime: stat.mtime.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProjectSkill(projectPath, options = {}) {
|
||||||
|
const skillName = normalizeSkillName(options.skillName);
|
||||||
|
const title = String(options.title || skillName).trim();
|
||||||
|
const description = String(options.description || `Project-specific workflow for ${title}.`).trim();
|
||||||
|
const body = String(options.instructions || '').trim() || [
|
||||||
|
`Use this skill for ${title} work in this Cocos project.`,
|
||||||
|
'',
|
||||||
|
'- Inspect the active scene and project context before editing.',
|
||||||
|
'- Prefer focused MCP tools before broad manual file edits.',
|
||||||
|
'- Run relevant validation tools after changes.',
|
||||||
|
].join('\n');
|
||||||
|
const relativePath = `.codex/skills/${skillName}/SKILL.md`;
|
||||||
|
const content = [
|
||||||
|
`# ${title}`,
|
||||||
|
'',
|
||||||
|
`Description: ${description}`,
|
||||||
|
'',
|
||||||
|
'## Instructions',
|
||||||
|
body,
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
return writeProjectInstruction(projectPath, {
|
||||||
|
target: relativePath,
|
||||||
|
content,
|
||||||
|
overwrite: options.overwrite !== false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
KNOWN_INSTRUCTION_PATHS,
|
||||||
|
createProjectSkill,
|
||||||
|
listProjectInstructions,
|
||||||
|
readProjectInstruction,
|
||||||
|
writeProjectInstruction,
|
||||||
|
};
|
||||||
+7
-2
@@ -570,10 +570,15 @@ class McpServer {
|
|||||||
}
|
}
|
||||||
return this.createResult(request.id, result);
|
return this.createResult(request.id, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.createResult(request.id, {
|
const result = {
|
||||||
content: textContent(error.message),
|
content: textContent(error.message),
|
||||||
isError: true,
|
isError: true,
|
||||||
});
|
};
|
||||||
|
const structured = structuredContent(error.toolEnvelope);
|
||||||
|
if (structured) {
|
||||||
|
result.structuredContent = structured;
|
||||||
|
}
|
||||||
|
return this.createResult(request.id, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+430
-4
@@ -1,5 +1,6 @@
|
|||||||
'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 {
|
const {
|
||||||
@@ -22,9 +23,24 @@ const {
|
|||||||
searchProjectLogs,
|
searchProjectLogs,
|
||||||
} = require('./logs');
|
} = require('./logs');
|
||||||
const { resolveProjectPath } = require('./path-safety');
|
const { resolveProjectPath } = require('./path-safety');
|
||||||
|
const {
|
||||||
|
createProjectSkill,
|
||||||
|
listProjectInstructions,
|
||||||
|
readProjectInstruction,
|
||||||
|
writeProjectInstruction,
|
||||||
|
} = require('./project-instructions');
|
||||||
|
const {
|
||||||
|
applyPrefabInstance,
|
||||||
|
duplicatePrefab,
|
||||||
|
editPrefabJson,
|
||||||
|
inspectPrefab,
|
||||||
|
revertPrefabInstance,
|
||||||
|
validatePrefabReferences,
|
||||||
|
} = require('./prefabs');
|
||||||
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
||||||
const { checkForUpdate } = require('./update-checker');
|
const { checkForUpdate } = require('./update-checker');
|
||||||
const { safeStringify } = require('./utils');
|
const { safeStringify } = require('./utils');
|
||||||
|
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
|
||||||
|
|
||||||
const TOOL_CATEGORY_RULES = [
|
const TOOL_CATEGORY_RULES = [
|
||||||
['updates', /update/],
|
['updates', /update/],
|
||||||
@@ -35,6 +51,7 @@ const TOOL_CATEGORY_RULES = [
|
|||||||
['files', /file|directory|exists|refresh_assets/],
|
['files', /file|directory|exists|refresh_assets/],
|
||||||
['assets', /asset|scene$|scenes|open_scene|run_scene_asset/],
|
['assets', /asset|scene$|scenes|open_scene|run_scene_asset/],
|
||||||
['prefabs', /prefab/],
|
['prefabs', /prefab/],
|
||||||
|
['instructions', /instruction|skill/],
|
||||||
['selection', /selection|select_/],
|
['selection', /selection|select_/],
|
||||||
['components', /component/],
|
['components', /component/],
|
||||||
['ui', /canvas|label|button|sprite/],
|
['ui', /canvas|label|button|sprite/],
|
||||||
@@ -57,6 +74,34 @@ function createSchema(properties, required) {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createOutputSchema(dataSchema = {}) {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
ok: { type: 'boolean', description: 'Whether the tool call completed successfully.' },
|
||||||
|
tool: { type: 'string', description: 'Tool name that produced this result.' },
|
||||||
|
callId: { type: 'string', description: 'Stable identifier for this tool call result.' },
|
||||||
|
timestamp: { type: 'string', description: 'ISO timestamp when the result envelope was produced.' },
|
||||||
|
summary: { type: 'string', description: 'Short human-readable result summary.' },
|
||||||
|
data: dataSchema,
|
||||||
|
refs: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Stable references discovered in the result for follow-up tool calls.',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: { type: 'string' },
|
||||||
|
id: { type: 'string' },
|
||||||
|
path: { type: 'string' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['ok', 'tool', 'callId', 'timestamp', 'data'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function inferToolCategory(toolName) {
|
function inferToolCategory(toolName) {
|
||||||
for (const [category, pattern] of TOOL_CATEGORY_RULES) {
|
for (const [category, pattern] of TOOL_CATEGORY_RULES) {
|
||||||
if (pattern.test(toolName)) {
|
if (pattern.test(toolName)) {
|
||||||
@@ -86,6 +131,26 @@ function toolCategory(tool) {
|
|||||||
return tool.category || inferToolCategory(tool.name);
|
return tool.category || inferToolCategory(tool.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inferToolAnnotations(tool) {
|
||||||
|
const name = tool.name;
|
||||||
|
const category = toolCategory(tool);
|
||||||
|
const readOnly = /^(get|list|inspect|find|read|search|check|validate|exists|capture)/.test(name);
|
||||||
|
const destructive = /(delete|remove|clear|replace|write|reset|set_|execute|run_scene|invoke|emit|simulate)/.test(name);
|
||||||
|
const idempotent = readOnly || /^(set|select|open|pause|resume|stop|refresh)/.test(name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: name
|
||||||
|
.split('_')
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(' '),
|
||||||
|
readOnlyHint: readOnly,
|
||||||
|
destructiveHint: readOnly ? false : destructive,
|
||||||
|
idempotentHint: idempotent,
|
||||||
|
openWorldHint: category === 'updates',
|
||||||
|
...(tool.annotations || {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isToolExposed(config, tool) {
|
function isToolExposed(config, tool) {
|
||||||
const profile = config && config.toolProfile === 'full'
|
const profile = config && config.toolProfile === 'full'
|
||||||
? 'full'
|
? 'full'
|
||||||
@@ -112,6 +177,115 @@ function isToolExposed(config, tool) {
|
|||||||
return exposed;
|
return exposed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hashObject(value) {
|
||||||
|
return crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(safeStringify(value))
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeResult(result) {
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
if (result.startsWith(IMAGE_DATA_URI_PREFIX)) {
|
||||||
|
return 'Image payload returned.';
|
||||||
|
}
|
||||||
|
return result.length > 160 ? `${result.slice(0, 160)}...` : result;
|
||||||
|
}
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
return String(result);
|
||||||
|
}
|
||||||
|
if (typeof result.summary === 'string') {
|
||||||
|
return result.summary;
|
||||||
|
}
|
||||||
|
for (const key of ['message', 'path', 'url', 'sceneName', 'projectName']) {
|
||||||
|
if (typeof result[key] === 'string' && result[key]) {
|
||||||
|
return `${key}: ${result[key]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(result.count)) {
|
||||||
|
return `count: ${result.count}`;
|
||||||
|
}
|
||||||
|
return 'Structured result returned.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEnvelopeData(result) {
|
||||||
|
if (typeof result === 'string' && result.startsWith(IMAGE_DATA_URI_PREFIX)) {
|
||||||
|
return {
|
||||||
|
image: true,
|
||||||
|
mimeType: 'image/png',
|
||||||
|
byteLength: Buffer.byteLength(result.slice(IMAGE_DATA_URI_PREFIX.length), 'base64'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRef(refs, type, id, extra = {}) {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = `${type}:${id}`;
|
||||||
|
if (refs.some((ref) => ref.key === key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refs.push({ key, type, id: String(id), ...extra });
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRefs(value, refs = [], depth = 0, seen = new WeakSet()) {
|
||||||
|
if (!value || depth > 5) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
collectRefs(item, refs, depth + 1, seen);
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
|
|
||||||
|
const uuid = value.uuid || value.prefabUuid || value.sceneUuid || value.assetUuid;
|
||||||
|
const pathValue = value.path || value.node || value.url;
|
||||||
|
if (uuid) {
|
||||||
|
addRef(refs, pathValue && String(pathValue).startsWith('db://') ? 'asset' : 'uuid', uuid, {
|
||||||
|
path: pathValue ? String(pathValue) : undefined,
|
||||||
|
name: value.name ? String(value.name) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (typeof pathValue === 'string' && pathValue) {
|
||||||
|
addRef(refs, pathValue.startsWith('db://') ? 'asset' : 'path', pathValue, {
|
||||||
|
name: value.name ? String(value.name) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of Object.values(value)) {
|
||||||
|
collectRefs(item, refs, depth + 1, seen);
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResultEnvelope(tool, args, result, options = {}) {
|
||||||
|
const data = normalizeEnvelopeData(result);
|
||||||
|
const refs = collectRefs(data).map(({ key, ...ref }) => ref);
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const summary = options.summary || summarizeResult(result);
|
||||||
|
const callId = `fp_${hashObject({ tool: tool.name, args: args || {}, result: data })}`;
|
||||||
|
return {
|
||||||
|
ok: options.ok !== false,
|
||||||
|
tool: tool.name,
|
||||||
|
callId,
|
||||||
|
timestamp,
|
||||||
|
summary,
|
||||||
|
data,
|
||||||
|
refs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeDiagnostics(result) {
|
function summarizeDiagnostics(result) {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
@@ -217,6 +391,17 @@ async function refreshAssets(projectPath, targetPath) {
|
|||||||
return 'File written outside assets directory; no asset-db refresh was needed.';
|
return 'File written outside assets directory; no asset-db refresh was needed.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveNodeUuid(sceneBridge, args) {
|
||||||
|
if (args && args.uuid) {
|
||||||
|
return String(args.uuid);
|
||||||
|
}
|
||||||
|
const inspected = await sceneBridge.call('inspectNode', args || {});
|
||||||
|
if (!inspected || !inspected.uuid) {
|
||||||
|
throw new Error('Target node uuid could not be resolved.');
|
||||||
|
}
|
||||||
|
return inspected.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runtimeLog, sceneBridge, editorExecutor }) {
|
function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runtimeLog, sceneBridge, editorExecutor }) {
|
||||||
const tools = [
|
const tools = [
|
||||||
{
|
{
|
||||||
@@ -351,6 +536,67 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema({}, []),
|
inputSchema: createSchema({}, []),
|
||||||
handler: async () => getCurrentSelection(),
|
handler: async () => getCurrentSelection(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'list_project_instructions',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] List project AI instruction files and local Codex project skills.',
|
||||||
|
inputSchema: createSchema({}, []),
|
||||||
|
handler: async () => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return listProjectInstructions(projectPath);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'read_project_instruction',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Read a project AI instruction file such as AGENTS.md, CLAUDE.md, or a .codex skill SKILL.md.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Project-relative instruction path.' },
|
||||||
|
},
|
||||||
|
['target']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return readProjectInstruction(projectPath, args.target);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'write_project_instruction',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create or update a project AI instruction file inside the Cocos project.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Project-relative instruction path.' },
|
||||||
|
content: { type: 'string', description: 'Instruction file content.' },
|
||||||
|
overwrite: { type: 'boolean', description: 'Allow overwriting an existing file. Defaults to true.' },
|
||||||
|
},
|
||||||
|
['target', 'content']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return writeProjectInstruction(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_project_skill',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a local Codex project skill under .codex/skills/{skillName}/SKILL.md.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
skillName: { type: 'string', description: 'Filesystem-safe project skill name.' },
|
||||||
|
title: { type: 'string', description: 'Human-readable skill title.' },
|
||||||
|
description: { type: 'string', description: 'Skill trigger description.' },
|
||||||
|
instructions: { type: 'string', description: 'Skill instructions body.' },
|
||||||
|
overwrite: { type: 'boolean', description: 'Allow overwriting an existing skill. Defaults to true.' },
|
||||||
|
},
|
||||||
|
['skillName']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return createProjectSkill(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'set_selection',
|
name: 'set_selection',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -537,6 +783,166 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
return { count: assets.length, prefabs: assets.slice(0, 200) };
|
return { count: assets.length, prefabs: assets.slice(0, 200) };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'inspect_prefab',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Inspect a prefab asset, its metadata, serialized file path, and UUID-like asset references.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Prefab uuid, db url, or project path.' },
|
||||||
|
},
|
||||||
|
['target']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return await inspectPrefab(projectPath, args.target);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'validate_prefab_references',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Validate prefab asset references by checking serialized UUID references against asset-db.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Optional prefab uuid, db url, or path. When omitted, scans prefab assets.' },
|
||||||
|
pattern: { type: 'string', description: 'Optional asset-db pattern used when scanning prefabs.' },
|
||||||
|
limit: { type: 'number', description: 'Maximum prefab assets to scan when target is omitted.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return await validatePrefabReferences(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duplicate_prefab',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a new prefab asset by duplicating an existing prefab file without copying its .meta UUID.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
source: { type: 'string', description: 'Source prefab uuid, db url, or project path.' },
|
||||||
|
target: { type: 'string', description: 'Project-relative target path under assets, with or without .prefab.' },
|
||||||
|
overwrite: { type: 'boolean', description: 'Overwrite target prefab if it already exists.' },
|
||||||
|
},
|
||||||
|
['source', 'target']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const result = await duplicatePrefab(projectPath, args);
|
||||||
|
return { ...result, refresh: await refreshAssets(projectPath, resolveProjectPath(projectPath, result.target)) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit_prefab_json',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Edit a prefab JSON file by JSON path assignment or literal search/replace, then validate references.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Prefab uuid, db url, or project path.' },
|
||||||
|
jsonPath: { type: 'string', description: 'JSON path such as /0/_name or 0._name when assigning valueJson.' },
|
||||||
|
valueJson: { type: 'string', description: 'JSON encoded value to assign at jsonPath.' },
|
||||||
|
search: { type: 'string', description: 'Literal text to search for instead of jsonPath assignment.' },
|
||||||
|
replace: { type: 'string', description: 'Replacement text for literal search.' },
|
||||||
|
replaceAll: { type: 'boolean', description: 'Replace all literal matches.' },
|
||||||
|
createBackup: { type: 'boolean', description: 'Create a .bak file before writing.' },
|
||||||
|
},
|
||||||
|
['target']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const result = await editPrefabJson(projectPath, args);
|
||||||
|
return { ...result, refresh: await refreshAssets(projectPath, resolveProjectPath(projectPath, result.path)) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_prefab_instance',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a linked prefab instance in the editor hierarchy using Cocos scene create-node when available.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
prefabUuid: { type: 'string', description: 'Prefab asset uuid, db url, or path.' },
|
||||||
|
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
||||||
|
name: { type: 'string', description: 'Optional override node name.' },
|
||||||
|
position: { type: 'object', description: 'Optional position {x,y,z}; fallback runtime path only.' },
|
||||||
|
},
|
||||||
|
['prefabUuid']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const info = await queryAssetInfo(args.prefabUuid);
|
||||||
|
const payload = {
|
||||||
|
assetUuid: info.uuid || args.prefabUuid,
|
||||||
|
unlinkPrefab: false,
|
||||||
|
};
|
||||||
|
if (args.parentPath) {
|
||||||
|
payload.parent = await resolveNodeUuid(sceneBridge, { path: args.parentPath });
|
||||||
|
}
|
||||||
|
if (args.name) {
|
||||||
|
payload.name = args.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.Editor && Editor.Message && typeof Editor.Message.request === 'function') {
|
||||||
|
try {
|
||||||
|
const createdUuid = await Editor.Message.request('scene', 'create-node', payload);
|
||||||
|
return {
|
||||||
|
created: true,
|
||||||
|
linkedPrefab: true,
|
||||||
|
prefabUuid: payload.assetUuid,
|
||||||
|
uuid: createdUuid,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
runtimeLog && runtimeLog.add('warn', `Linked prefab create-node failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sceneBridge.call('instantiatePrefab', {
|
||||||
|
...args,
|
||||||
|
prefabUuid: info.uuid || args.prefabUuid,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'inspect_prefab_instance',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('getPrefabInstanceInfo', args),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apply_prefab_instance',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => await applyPrefabInstance(await resolveNodeUuid(sceneBridge, args)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'revert_prefab_instance',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => await revertPrefabInstance(await resolveNodeUuid(sceneBridge, args)),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'instantiate_prefab',
|
name: 'instantiate_prefab',
|
||||||
profile: 'full',
|
profile: 'full',
|
||||||
@@ -1242,6 +1648,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
includeComponents: true,
|
includeComponents: true,
|
||||||
}).catch((error) => ({ ok: false, error: error.message }));
|
}).catch((error) => ({ ok: false, error: error.message }));
|
||||||
const runtime = await sceneBridge.call('getRuntimeState', {}).catch((error) => ({ ok: false, error: error.message }));
|
const runtime = await sceneBridge.call('getRuntimeState', {}).catch((error) => ({ ok: false, error: error.message }));
|
||||||
|
const performance = await sceneBridge.call('getPerformanceSnapshot', {}).catch((error) => ({ ok: false, error: error.message }));
|
||||||
const diagnostics = args.includeScriptDiagnostics === false
|
const diagnostics = args.includeScriptDiagnostics === false
|
||||||
? null
|
? null
|
||||||
: summarizeDiagnostics(await runScriptDiagnostics(projectPath, args).catch((error) => ({ ok: false, summary: error.message, diagnostics: [] })));
|
: summarizeDiagnostics(await runScriptDiagnostics(projectPath, args).catch((error) => ({ ok: false, summary: error.message, diagnostics: [] })));
|
||||||
@@ -1250,14 +1657,22 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
: searchProjectLogs(projectPath, { query: 'error', limit: 20 }).matches;
|
: searchProjectLogs(projectPath, { query: 'error', limit: 20 }).matches;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: !scene.error && !runtime.error && (!diagnostics || diagnostics.ok) && (!logErrors || logErrors.length === 0),
|
ok: !scene.error && !runtime.error && !performance.error && (!diagnostics || diagnostics.ok) && (!logErrors || logErrors.length === 0),
|
||||||
scene,
|
scene,
|
||||||
runtime,
|
runtime,
|
||||||
|
performance,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
logErrors,
|
logErrors,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'get_performance_snapshot',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Return scene scale and runtime performance-oriented counters such as node/component counts, UI counts, depth, memory, and warnings.',
|
||||||
|
inputSchema: createSchema({}, []),
|
||||||
|
handler: async (args) => sceneBridge.call('getPerformanceSnapshot', args),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'get_runtime_state',
|
name: 'get_runtime_state',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -1580,6 +1995,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
inputSchema: tool.inputSchema,
|
inputSchema: tool.inputSchema,
|
||||||
|
outputSchema: tool.outputSchema || createOutputSchema(tool.dataSchema),
|
||||||
|
annotations: inferToolAnnotations(tool),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
listToolCatalog() {
|
listToolCatalog() {
|
||||||
@@ -1589,6 +2006,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: tool.description,
|
description: tool.description,
|
||||||
profile: tool.profile,
|
profile: tool.profile,
|
||||||
category: toolCategory(tool),
|
category: toolCategory(tool),
|
||||||
|
annotations: inferToolAnnotations(tool),
|
||||||
|
outputSchema: tool.outputSchema || createOutputSchema(tool.dataSchema),
|
||||||
enabled: isToolExposed(config || {}, tool),
|
enabled: isToolExposed(config || {}, tool),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -1604,14 +2023,21 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await tool.handler(args || {});
|
const result = await tool.handler(args || {});
|
||||||
const output = toOutput(result);
|
const envelope = createResultEnvelope(tool, args || {}, result);
|
||||||
interactionLog.add(name, 'success', output.slice(0, 500));
|
const output = typeof result === 'string' && result.startsWith(IMAGE_DATA_URI_PREFIX)
|
||||||
|
? result
|
||||||
|
: toOutput(envelope);
|
||||||
|
interactionLog.add(name, 'success', envelope.summary.slice(0, 500));
|
||||||
return {
|
return {
|
||||||
value: result,
|
value: envelope,
|
||||||
text: output,
|
text: output,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
interactionLog.add(name, 'error', error.message);
|
interactionLog.add(name, 'error', error.message);
|
||||||
|
error.toolEnvelope = createResultEnvelope(tool, args || {}, { message: error.message }, {
|
||||||
|
ok: false,
|
||||||
|
summary: error.message,
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-3
@@ -1,14 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "funplay-cocos-mcp",
|
"name": "funplay-cocos-mcp",
|
||||||
"package_version": 2,
|
"package_version": 2,
|
||||||
"version": "0.2.0",
|
"version": "0.3.1",
|
||||||
"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",
|
||||||
"main": "browser.js",
|
"main": "browser.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "node --check browser.js && node --check scene.js && node --check panel/index.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/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/update-checker.js && node --check lib/utils.js",
|
"check": "node --check browser.js && node --check scene.js && node --check panel/index.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/update-checker.js && node --check lib/utils.js && node --check scripts/release.js",
|
||||||
"test": "node --test"
|
"test": "node --test",
|
||||||
|
"release:check": "node scripts/release.js check",
|
||||||
|
"release:package": "node scripts/release.js package",
|
||||||
|
"release:verify": "npm run check && npm test && npm run release:check && npm run release:package"
|
||||||
},
|
},
|
||||||
"panels": {
|
"panels": {
|
||||||
"default": {
|
"default": {
|
||||||
|
|||||||
@@ -308,6 +308,97 @@ function findComponentsByClass(componentClass) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPrefabInfo(node) {
|
||||||
|
const prefab = node && node._prefab;
|
||||||
|
if (!prefab) {
|
||||||
|
return {
|
||||||
|
linked: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = prefab.asset || prefab._asset || null;
|
||||||
|
return {
|
||||||
|
linked: Boolean(asset || prefab.fileId || prefab.root),
|
||||||
|
fileId: prefab.fileId || '',
|
||||||
|
asset: asset
|
||||||
|
? {
|
||||||
|
name: asset.name || '',
|
||||||
|
uuid: asset.uuid || asset._uuid || '',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
instance: prefab.instance ? plain(prefab.instance) : null,
|
||||||
|
sync: prefab.sync,
|
||||||
|
rawKeys: Object.keys(prefab).slice(0, 50),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSceneStats() {
|
||||||
|
const stats = {
|
||||||
|
nodeCount: 0,
|
||||||
|
activeNodeCount: 0,
|
||||||
|
inactiveNodeCount: 0,
|
||||||
|
maxDepth: 0,
|
||||||
|
componentCount: 0,
|
||||||
|
componentsByType: {},
|
||||||
|
prefabInstanceCount: 0,
|
||||||
|
uiTransformCount: 0,
|
||||||
|
canvasCount: 0,
|
||||||
|
cameraCount: 0,
|
||||||
|
labelCount: 0,
|
||||||
|
spriteCount: 0,
|
||||||
|
buttonCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function visit(node, depth) {
|
||||||
|
if (node !== getScene()) {
|
||||||
|
stats.nodeCount += 1;
|
||||||
|
stats.maxDepth = Math.max(stats.maxDepth, depth);
|
||||||
|
if (node.active) stats.activeNodeCount += 1;
|
||||||
|
else stats.inactiveNodeCount += 1;
|
||||||
|
if (node._prefab) stats.prefabInstanceCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const component of node.components || []) {
|
||||||
|
const name = component && component.constructor ? component.constructor.name : 'UnknownComponent';
|
||||||
|
stats.componentCount += 1;
|
||||||
|
stats.componentsByType[name] = (stats.componentsByType[name] || 0) + 1;
|
||||||
|
if (component instanceof UITransform) stats.uiTransformCount += 1;
|
||||||
|
if (component instanceof Canvas) stats.canvasCount += 1;
|
||||||
|
if (component instanceof Camera) stats.cameraCount += 1;
|
||||||
|
if (component instanceof Label) stats.labelCount += 1;
|
||||||
|
if (component instanceof Sprite) stats.spriteCount += 1;
|
||||||
|
if (component instanceof Button) stats.buttonCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of node.children) {
|
||||||
|
visit(child, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(getScene(), 0);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSceneWarnings(stats) {
|
||||||
|
const warnings = [];
|
||||||
|
if (stats.nodeCount === 0) {
|
||||||
|
warnings.push({ severity: 'warn', code: 'empty_scene', message: 'The active scene has no child nodes.' });
|
||||||
|
}
|
||||||
|
if (stats.cameraCount === 0) {
|
||||||
|
warnings.push({ severity: 'warn', code: 'missing_camera', message: 'No Camera component was found in the active scene.' });
|
||||||
|
}
|
||||||
|
if (stats.nodeCount > 500) {
|
||||||
|
warnings.push({ severity: 'info', code: 'large_node_count', message: `Scene has ${stats.nodeCount} nodes.` });
|
||||||
|
}
|
||||||
|
if (stats.maxDepth > 12) {
|
||||||
|
warnings.push({ severity: 'info', code: 'deep_hierarchy', message: `Scene hierarchy depth is ${stats.maxDepth}.` });
|
||||||
|
}
|
||||||
|
if (stats.labelCount > 80) {
|
||||||
|
warnings.push({ severity: 'info', code: 'many_labels', message: `Scene has ${stats.labelCount} Label components.` });
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
function getScheduler() {
|
function getScheduler() {
|
||||||
return typeof director.getScheduler === 'function' ? director.getScheduler() : null;
|
return typeof director.getScheduler === 'function' ? director.getScheduler() : null;
|
||||||
}
|
}
|
||||||
@@ -1149,6 +1240,45 @@ exports.methods = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getPerformanceSnapshot() {
|
||||||
|
const scheduler = getScheduler();
|
||||||
|
const stats = collectSceneStats();
|
||||||
|
const memory = typeof performance !== 'undefined' && performance.memory
|
||||||
|
? {
|
||||||
|
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
||||||
|
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||||
|
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sceneName: getScene().name,
|
||||||
|
runtime: {
|
||||||
|
paused: typeof director.isPaused === 'function' ? director.isPaused() : false,
|
||||||
|
timeScale: scheduler && typeof scheduler.getTimeScale === 'function' ? scheduler.getTimeScale() : 1,
|
||||||
|
totalFrames: typeof director.getTotalFrames === 'function' ? director.getTotalFrames() : undefined,
|
||||||
|
},
|
||||||
|
stats,
|
||||||
|
memory,
|
||||||
|
warnings: buildSceneWarnings(stats),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPrefabInstanceInfo(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Target node was not found.');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
node: {
|
||||||
|
name: node.name,
|
||||||
|
path: getNodePath(node),
|
||||||
|
uuid: node.uuid,
|
||||||
|
},
|
||||||
|
prefab: getPrefabInfo(node),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async pauseRuntime() {
|
async pauseRuntime() {
|
||||||
if (typeof director.pause === 'function') {
|
if (typeof director.pause === 'function') {
|
||||||
director.pause();
|
director.pause();
|
||||||
|
|||||||
@@ -0,0 +1,505 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const childProcess = require('child_process');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
const PACKAGE_DIR_NAME = 'funplay-cocos-mcp';
|
||||||
|
const RELEASES_DIR = path.join(ROOT, 'releases');
|
||||||
|
const TEMP_DIR = path.join(ROOT, '.release-tmp');
|
||||||
|
const ZIP_PREFIX = 'Funplay.CocosMcp';
|
||||||
|
const REPOSITORY_URL = 'https://github.com/FunplayAI/funplay-cocos-mcp';
|
||||||
|
|
||||||
|
const REQUIRED_REPO_FILES = [
|
||||||
|
'package.json',
|
||||||
|
'README.md',
|
||||||
|
'README_CN.md',
|
||||||
|
'RELEASE_WORKFLOW.md',
|
||||||
|
'RELEASE_CHECKLIST.md',
|
||||||
|
'CHANGELOG.md',
|
||||||
|
'CONTRIBUTING.md',
|
||||||
|
'LICENSE',
|
||||||
|
'browser.js',
|
||||||
|
'scene.js',
|
||||||
|
'panel/index.js',
|
||||||
|
'lib/server.js',
|
||||||
|
'lib/tool-registry.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
const PACKAGE_INCLUDES = [
|
||||||
|
'package.json',
|
||||||
|
'README.md',
|
||||||
|
'README_CN.md',
|
||||||
|
'CHANGELOG.md',
|
||||||
|
'CONTRIBUTING.md',
|
||||||
|
'LICENSE',
|
||||||
|
'browser.js',
|
||||||
|
'scene.js',
|
||||||
|
'panel',
|
||||||
|
'lib'
|
||||||
|
];
|
||||||
|
|
||||||
|
const FORBIDDEN_TRACKED_SEGMENTS = new Set([
|
||||||
|
'.idea',
|
||||||
|
'node_modules',
|
||||||
|
'Library',
|
||||||
|
'library',
|
||||||
|
'Temp',
|
||||||
|
'temp',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'coverage',
|
||||||
|
'releases',
|
||||||
|
'.release-tmp'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const FORBIDDEN_ARCHIVE_SEGMENTS = new Set([
|
||||||
|
'.git',
|
||||||
|
'.github',
|
||||||
|
'.idea',
|
||||||
|
'node_modules',
|
||||||
|
'Library',
|
||||||
|
'library',
|
||||||
|
'Temp',
|
||||||
|
'temp',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'coverage',
|
||||||
|
'releases',
|
||||||
|
'.release-tmp',
|
||||||
|
'scripts',
|
||||||
|
'test'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const FORBIDDEN_NAMES = new Set([
|
||||||
|
'.DS_Store'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const command = process.argv[2] || 'check';
|
||||||
|
const options = parseOptions(process.argv.slice(3));
|
||||||
|
|
||||||
|
if (command === 'check') {
|
||||||
|
const context = checkRelease(options);
|
||||||
|
console.log(`Release check passed for v${context.version}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === 'package') {
|
||||||
|
const context = checkRelease(options);
|
||||||
|
const artifacts = packageRelease(context);
|
||||||
|
console.log(`Release package ready: ${path.relative(ROOT, artifacts.releaseDir)}`);
|
||||||
|
console.log(`- ${artifacts.zipName}`);
|
||||||
|
console.log('- release-manifest.json');
|
||||||
|
console.log('- SHA256SUMS.txt');
|
||||||
|
console.log('- README.md');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printUsage();
|
||||||
|
process.exitCode = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOptions(args) {
|
||||||
|
const options = {
|
||||||
|
version: '',
|
||||||
|
strictTag: false
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i += 1) {
|
||||||
|
const arg = args[i];
|
||||||
|
if (arg === '--version' && args[i + 1]) {
|
||||||
|
options.version = args[i + 1];
|
||||||
|
i += 1;
|
||||||
|
} else if (arg === '--strict-tag') {
|
||||||
|
options.strictTag = true;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown release option: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRelease(options = {}) {
|
||||||
|
const errors = [];
|
||||||
|
const packageJson = readJson(path.join(ROOT, 'package.json'), errors);
|
||||||
|
const version = options.version || (packageJson && packageJson.version) || '';
|
||||||
|
const tag = `v${version}`;
|
||||||
|
|
||||||
|
if (!packageJson) {
|
||||||
|
throwErrors(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.version && options.version !== packageJson.version) {
|
||||||
|
errors.push(`--version ${options.version} does not match package.json version ${packageJson.version}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageJson.name !== 'funplay-cocos-mcp') {
|
||||||
|
errors.push('package.json name must be funplay-cocos-mcp.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(packageJson.package_version) || packageJson.package_version <= 0) {
|
||||||
|
errors.push('package.json package_version must be a positive integer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packageJson.main || !fs.existsSync(path.join(ROOT, packageJson.main))) {
|
||||||
|
errors.push('package.json main must point to an existing file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
||||||
|
errors.push(`package.json version must be semver-like, got: ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const relative of REQUIRED_REPO_FILES) {
|
||||||
|
if (!fs.existsSync(path.join(ROOT, relative))) {
|
||||||
|
errors.push(`Missing required repository file: ${relative}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelogPath = path.join(ROOT, 'CHANGELOG.md');
|
||||||
|
const changelog = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, 'utf8') : '';
|
||||||
|
if (version && !new RegExp(`^## \\[${escapeRegExp(version)}\\] - \\d{4}-\\d{2}-\\d{2}`, 'm').test(changelog)) {
|
||||||
|
errors.push(`CHANGELOG.md is missing a dated ## [${version}] release section.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackedFiles = gitLines(['ls-files']);
|
||||||
|
const forbiddenTracked = trackedFiles.filter(isForbiddenTrackedPath);
|
||||||
|
if (forbiddenTracked.length > 0) {
|
||||||
|
errors.push(`Tracked local/build junk must not be committed:\n- ${forbiddenTracked.join('\n- ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const relative of PACKAGE_INCLUDES) {
|
||||||
|
const fullPath = path.join(ROOT, relative);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
errors.push(`Package include path is missing: ${relative}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.strictTag && !gitTagExists(tag)) {
|
||||||
|
errors.push(`Git tag ${tag} does not exist. Create it before publishing.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throwErrors(errors);
|
||||||
|
|
||||||
|
return {
|
||||||
|
packageJson,
|
||||||
|
version,
|
||||||
|
tag,
|
||||||
|
changelogNotes: extractChangelogNotes(changelog, version),
|
||||||
|
gitCommit: gitText(['rev-parse', 'HEAD']).trim(),
|
||||||
|
gitDirty: gitText(['status', '--porcelain']).trim() !== ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageRelease(context) {
|
||||||
|
ensureCommand('zip');
|
||||||
|
|
||||||
|
const releaseDir = path.join(RELEASES_DIR, context.version);
|
||||||
|
const stagingRoot = path.join(TEMP_DIR, PACKAGE_DIR_NAME);
|
||||||
|
const zipName = `${ZIP_PREFIX}.v${context.version}.zip`;
|
||||||
|
const zipPath = path.join(releaseDir, zipName);
|
||||||
|
|
||||||
|
fs.rmSync(releaseDir, { recursive: true, force: true });
|
||||||
|
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(releaseDir, { recursive: true });
|
||||||
|
fs.mkdirSync(stagingRoot, { recursive: true });
|
||||||
|
|
||||||
|
for (const relative of PACKAGE_INCLUDES) {
|
||||||
|
copyIntoPackage(relative, stagingRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stagedFiles = collectFiles(stagingRoot)
|
||||||
|
.map((filePath) => path.relative(TEMP_DIR, filePath).split(path.sep).join('/'));
|
||||||
|
validateArchivePaths(stagedFiles);
|
||||||
|
|
||||||
|
run('zip', ['-qr', zipPath, PACKAGE_DIR_NAME], { cwd: TEMP_DIR });
|
||||||
|
validateZipListing(zipPath);
|
||||||
|
|
||||||
|
const zipSha256 = sha256File(zipPath);
|
||||||
|
const zipSize = fs.statSync(zipPath).size;
|
||||||
|
const manifest = buildManifest(context, {
|
||||||
|
zipName,
|
||||||
|
zipSha256,
|
||||||
|
zipSize,
|
||||||
|
fileCount: stagedFiles.length
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestPath = path.join(releaseDir, 'release-manifest.json');
|
||||||
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||||
|
|
||||||
|
const readmePath = path.join(releaseDir, 'README.md');
|
||||||
|
fs.writeFileSync(readmePath, buildReleaseReadme(context, manifest));
|
||||||
|
|
||||||
|
const checksums = [
|
||||||
|
checksumLine(zipPath, zipName),
|
||||||
|
checksumLine(manifestPath, 'release-manifest.json'),
|
||||||
|
checksumLine(readmePath, 'README.md')
|
||||||
|
].join('');
|
||||||
|
fs.writeFileSync(path.join(releaseDir, 'SHA256SUMS.txt'), checksums);
|
||||||
|
|
||||||
|
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
releaseDir,
|
||||||
|
zipName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyIntoPackage(relative, stagingRoot) {
|
||||||
|
const source = path.join(ROOT, relative);
|
||||||
|
const destination = path.join(stagingRoot, relative);
|
||||||
|
fs.cpSync(source, destination, {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
filter(sourcePath) {
|
||||||
|
const name = path.basename(sourcePath);
|
||||||
|
if (FORBIDDEN_NAMES.has(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const relativeSource = path.relative(ROOT, sourcePath).split(path.sep);
|
||||||
|
return !relativeSource.some((part) => FORBIDDEN_ARCHIVE_SEGMENTS.has(part));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildManifest(context, artifact) {
|
||||||
|
return {
|
||||||
|
version: context.version,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
repository: {
|
||||||
|
url: REPOSITORY_URL,
|
||||||
|
source: 'github'
|
||||||
|
},
|
||||||
|
git: {
|
||||||
|
tag: context.tag,
|
||||||
|
commit: context.gitCommit,
|
||||||
|
dirty: context.gitDirty
|
||||||
|
},
|
||||||
|
package: {
|
||||||
|
name: context.packageJson.name,
|
||||||
|
version: context.packageJson.version,
|
||||||
|
main: context.packageJson.main,
|
||||||
|
packageVersion: context.packageJson.package_version
|
||||||
|
},
|
||||||
|
artifacts: {
|
||||||
|
extensionZip: {
|
||||||
|
file: artifact.zipName,
|
||||||
|
sha256: artifact.zipSha256,
|
||||||
|
sizeBytes: artifact.zipSize,
|
||||||
|
fileCount: artifact.fileCount,
|
||||||
|
installDirectory: 'extensions/funplay-cocos-mcp',
|
||||||
|
githubDownloadUrl: `${REPOSITORY_URL}/releases/download/${context.tag}/${artifact.zipName}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notes: firstMeaningfulLine(context.changelogNotes)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReleaseReadme(context, manifest) {
|
||||||
|
const zip = manifest.artifacts.extensionZip;
|
||||||
|
return `# Funplay MCP for Cocos ${context.tag}
|
||||||
|
|
||||||
|
This folder contains the generated release artifacts for Funplay MCP for Cocos ${context.tag}.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
- \`${zip.file}\` - Cocos Creator extension package.
|
||||||
|
- \`release-manifest.json\` - Machine-readable release metadata.
|
||||||
|
- \`SHA256SUMS.txt\` - SHA-256 checksums for release artifacts.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Unzip \`${zip.file}\`.
|
||||||
|
2. Move the extracted \`${PACKAGE_DIR_NAME}\` folder into your Cocos project \`extensions/\` directory.
|
||||||
|
3. Restart Cocos Creator or reload extensions.
|
||||||
|
4. Open \`Funplay > MCP Server\`.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
shasum -a 256 -c SHA256SUMS.txt
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateArchivePaths(paths) {
|
||||||
|
const bad = [];
|
||||||
|
const prefix = `${PACKAGE_DIR_NAME}/`;
|
||||||
|
|
||||||
|
for (const archivePath of paths) {
|
||||||
|
const normalized = archivePath.replace(/\\/g, '/');
|
||||||
|
const parts = normalized.split('/').filter(Boolean);
|
||||||
|
if (!normalized.startsWith(prefix)) {
|
||||||
|
bad.push(`${archivePath} (must stay under ${PACKAGE_DIR_NAME}/)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (parts.some((part) => part === '..' || part === '.')) {
|
||||||
|
bad.push(`${archivePath} (contains unsafe relative path segments)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (parts.some((part) => FORBIDDEN_ARCHIVE_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part))) {
|
||||||
|
bad.push(`${archivePath} (contains forbidden release content)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bad.length > 0) {
|
||||||
|
throw new Error(`Release archive contains invalid paths:\n- ${bad.join('\n- ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateZipListing(zipPath) {
|
||||||
|
const result = childProcess.spawnSync('unzip', ['-Z1', zipPath], {
|
||||||
|
cwd: ROOT,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error && result.error.code === 'ENOENT') {
|
||||||
|
console.warn('Warning: unzip is not available; skipped zip listing validation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`Failed to inspect ${zipPath}:\n${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listing = result.stdout
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
validateArchivePaths(listing);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isForbiddenTrackedPath(relative) {
|
||||||
|
const parts = relative.split('/');
|
||||||
|
return parts.some((part) => FORBIDDEN_TRACKED_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(filePath, errors) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${path.relative(ROOT, filePath)} is not valid JSON: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFiles(directory) {
|
||||||
|
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
||||||
|
const files = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(directory, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...collectFiles(fullPath));
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractChangelogNotes(changelog, version) {
|
||||||
|
const heading = new RegExp(`^## \\[${escapeRegExp(version)}\\] - \\d{4}-\\d{2}-\\d{2}\\s*$`, 'm');
|
||||||
|
const match = heading.exec(changelog);
|
||||||
|
if (!match) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const start = match.index + match[0].length;
|
||||||
|
const rest = changelog.slice(start);
|
||||||
|
const next = rest.search(/^## /m);
|
||||||
|
return (next >= 0 ? rest.slice(0, next) : rest).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstMeaningfulLine(text) {
|
||||||
|
const line = text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.find((value) => value && !value.startsWith('###') && !value.startsWith('-'));
|
||||||
|
if (line) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
const bullet = text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.find((value) => value.startsWith('- '));
|
||||||
|
return bullet ? bullet.slice(2) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function checksumLine(filePath, displayName) {
|
||||||
|
return `${sha256File(filePath)} ${displayName}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256File(filePath) {
|
||||||
|
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCommand(name) {
|
||||||
|
const result = childProcess.spawnSync(name, ['-v'], { encoding: 'utf8' });
|
||||||
|
if (result.error && result.error.code === 'ENOENT') {
|
||||||
|
throw new Error(`Required command not found: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
const result = childProcess.spawnSync(command, args, {
|
||||||
|
cwd: options.cwd || ROOT,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`${command} ${args.join(' ')} failed:\n${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
return result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitLines(args) {
|
||||||
|
const text = gitText(args);
|
||||||
|
return text ? text.split(/\r?\n/).filter(Boolean) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitText(args) {
|
||||||
|
const result = childProcess.spawnSync('git', args, {
|
||||||
|
cwd: ROOT,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
if (result.error || result.status !== 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitTagExists(tag) {
|
||||||
|
const result = childProcess.spawnSync('git', ['rev-parse', '-q', '--verify', `refs/tags/${tag}`], {
|
||||||
|
cwd: ROOT,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(value) {
|
||||||
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwErrors(errors) {
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`Release validation failed:\n- ${errors.join('\n- ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printUsage() {
|
||||||
|
console.error(`Usage:
|
||||||
|
node scripts/release.js check [--version <version>] [--strict-tag]
|
||||||
|
node scripts/release.js package [--version <version>] [--strict-tag]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
const {
|
||||||
|
createProjectSkill,
|
||||||
|
listProjectInstructions,
|
||||||
|
readProjectInstruction,
|
||||||
|
writeProjectInstruction,
|
||||||
|
} = require('../lib/project-instructions');
|
||||||
|
|
||||||
|
test('project instruction helpers list, read, and write safe project files', () => {
|
||||||
|
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-'));
|
||||||
|
const write = writeProjectInstruction(projectPath, {
|
||||||
|
target: 'AGENTS.md',
|
||||||
|
content: '# Agent Notes\n',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(write.written, true);
|
||||||
|
assert.equal(readProjectInstruction(projectPath, 'AGENTS.md').content, '# Agent Notes\n');
|
||||||
|
|
||||||
|
const listed = listProjectInstructions(projectPath);
|
||||||
|
assert.equal(listed.files.some((file) => file.path === 'AGENTS.md'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createProjectSkill writes a Codex project skill', () => {
|
||||||
|
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-skill-'));
|
||||||
|
const result = createProjectSkill(projectPath, {
|
||||||
|
skillName: 'scene qa',
|
||||||
|
title: 'Scene QA',
|
||||||
|
description: 'Validate Cocos scenes.',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.path, '.codex/skills/scene-qa/SKILL.md');
|
||||||
|
const listed = listProjectInstructions(projectPath);
|
||||||
|
assert.equal(listed.skills.some((skill) => skill.path === result.path), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('project instruction helpers reject traversal outside the project', () => {
|
||||||
|
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-safe-'));
|
||||||
|
assert.throws(
|
||||||
|
() => writeProjectInstruction(projectPath, { target: '../AGENTS.md', content: 'x' }),
|
||||||
|
/outside the Cocos project/
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ const path = require('node:path');
|
|||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const { createToolRegistry } = require('../lib/tool-registry');
|
const { createToolRegistry } = require('../lib/tool-registry');
|
||||||
|
|
||||||
function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-test-project'), configExtras = {}) {
|
function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-test-project'), configExtras = {}, overrides = {}) {
|
||||||
return createToolRegistry({
|
return createToolRegistry({
|
||||||
getRuntimeContext: () => ({
|
getRuntimeContext: () => ({
|
||||||
config: { toolProfile: profile, ...configExtras },
|
config: { toolProfile: profile, ...configExtras },
|
||||||
@@ -13,31 +13,42 @@ function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-
|
|||||||
version: '0.0.0-test',
|
version: '0.0.0-test',
|
||||||
}),
|
}),
|
||||||
interactionLog: { add() {} },
|
interactionLog: { add() {} },
|
||||||
runtimeLog: { list: () => [], clear: () => 0 },
|
runtimeLog: { add() {}, list: () => [], clear: () => 0 },
|
||||||
sceneBridge: { call: async () => ({ ok: true }) },
|
sceneBridge: overrides.sceneBridge || { call: async () => ({ ok: true }) },
|
||||||
editorExecutor: async () => ({ ok: true }),
|
editorExecutor: overrides.editorExecutor || (async () => ({ ok: true })),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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, 28);
|
assert.equal(tools.length, 34);
|
||||||
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 === 'get_performance_snapshot'), 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);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
|
assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
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, 76);
|
assert.equal(tools.length, 89);
|
||||||
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 === 'create_project_skill'), 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tool definitions include MCP outputSchema and annotations', () => {
|
||||||
|
const tool = createRegistry('core').listTools().find((item) => item.name === 'get_project_info');
|
||||||
|
assert.equal(tool.outputSchema.type, 'object');
|
||||||
|
assert.equal(tool.outputSchema.properties.ok.type, 'boolean');
|
||||||
|
assert.equal(tool.annotations.readOnlyHint, true);
|
||||||
|
});
|
||||||
|
|
||||||
test('custom profile can expose a category and disable a specific tool', () => {
|
test('custom profile can expose a category and disable a specific tool', () => {
|
||||||
const tools = createRegistry('custom', path.resolve('/tmp/funplay-cocos-test-project'), {
|
const tools = createRegistry('custom', path.resolve('/tmp/funplay-cocos-test-project'), {
|
||||||
enabledToolCategories: ['files'],
|
enabledToolCategories: ['files'],
|
||||||
@@ -69,6 +80,21 @@ test('file tools reject writes outside the project root', async () => {
|
|||||||
test('callToolDetailed preserves structured values and text output', async () => {
|
test('callToolDetailed preserves structured values and text output', async () => {
|
||||||
const registry = createRegistry('core');
|
const registry = createRegistry('core');
|
||||||
const result = await registry.callToolDetailed('get_project_info', {});
|
const result = await registry.callToolDetailed('get_project_info', {});
|
||||||
assert.equal(result.value.projectPath, path.resolve('/tmp/funplay-cocos-test-project'));
|
assert.equal(result.value.ok, true);
|
||||||
|
assert.equal(result.value.tool, 'get_project_info');
|
||||||
|
assert.equal(result.value.data.projectPath, path.resolve('/tmp/funplay-cocos-test-project'));
|
||||||
|
assert.match(result.value.callId, /^fp_/);
|
||||||
assert.match(result.text, /projectPath/);
|
assert.match(result.text, /projectPath/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('callToolDetailed preserves screenshot image text while keeping structured envelope small', async () => {
|
||||||
|
const dataUri = 'data:image/png;base64,AAAA';
|
||||||
|
const registry = createRegistry('core', path.resolve('/tmp/funplay-cocos-test-project'), {}, {
|
||||||
|
editorExecutor: async () => dataUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await registry.callToolDetailed('execute_javascript', { context: 'editor', code: 'return image;' });
|
||||||
|
assert.equal(result.text, dataUri);
|
||||||
|
assert.equal(result.value.data.image, true);
|
||||||
|
assert.equal(result.value.data.mimeType, 'image/png');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user