Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e06ba79d8 | ||
|
|
b65a4f22c3 | ||
|
|
a9b539ca9c | ||
|
|
6405dced7d | ||
|
|
03e5ab8dfe | ||
|
|
be8c1d624f | ||
|
|
668898f0c6 | ||
|
|
87cfa48cda | ||
|
|
e3dc45468b |
@@ -76,5 +76,11 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
|
- name: Check generated docs
|
||||||
|
run: npm run docs:check
|
||||||
|
|
||||||
- name: Run release metadata checks
|
- name: Run release metadata checks
|
||||||
run: npm run release:check
|
run: npm run release:check
|
||||||
|
|
||||||
|
- name: Run npm package dry-run
|
||||||
|
run: npm run pack:dry-run
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
.mcpregistry_*
|
||||||
node_modules/
|
node_modules/
|
||||||
temp/
|
temp/
|
||||||
Temp/
|
Temp/
|
||||||
|
|||||||
@@ -6,6 +6,76 @@ This project follows a simple changelog format inspired by [Keep a Changelog](ht
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-06-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added project identity metadata to `/health` and MCP `initialize` responses so clients and duplicate listeners can verify the active Cocos project safely.
|
||||||
|
- Added same-project listener attach behavior before port fallback, preventing accidental attachment to a different Cocos project on the same port.
|
||||||
|
- Added default-on JavaScript safety checks for `execute_javascript`, `execute_scene_script`, and `execute_editor_script`, with per-call `safety_checks` overrides.
|
||||||
|
- Added named tool profiles in the Cocos panel, including save, apply, delete, import, and export workflows.
|
||||||
|
- Added category-level tool exposure controls in the panel for quick enable, disable, and clear actions.
|
||||||
|
- Added asset dependency tools: `inspect_asset_dependencies` and `validate_asset_dependencies`.
|
||||||
|
- Added Cocos project/editor tools: `get_build_status`, `open_build_panel`, `run_project_preview`, `save_current_scene`, `get_editor_preference`, `set_editor_preference`, and `broadcast_editor_message`.
|
||||||
|
- Added Button event binding tools: `list_button_click_events` and `bind_button_click_event`.
|
||||||
|
- Added `create_cocos_mcp_project_skill` for generating a recommended local Codex workflow skill.
|
||||||
|
- Added release package sensitive-content scanning for npm/GitHub/MCP token-like values and private keys.
|
||||||
|
|
||||||
|
### Optimized
|
||||||
|
|
||||||
|
- Improved tool exposure UX for larger projects by making custom profiles reusable and shareable.
|
||||||
|
- Expanded generated tool documentation and README coverage for the new 37-tool `core` profile and 101-tool `full` profile.
|
||||||
|
- Improved release packaging confidence with checksum verification and content scanning.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Expanded the default `core` profile from 34 tools to 37 tools.
|
||||||
|
- Expanded the `full` profile from 89 tools to 101 tools.
|
||||||
|
- Split more tool implementations into focused modules under `lib/tools/`, including advanced assets, Cocos project/editor helpers, and scene event helpers.
|
||||||
|
- Persisted `executeJavascriptSafetyChecks`, active tool profile names, and saved tool profiles in project configuration.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the GitHub Release workflow so release pages use generated English changelog-style `RELEASE_NOTES.md` instead of the artifact installation README.
|
||||||
|
- Fixed release package scanning false positives around normal MCP server config names while preserving credential detection.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Added guardrails for JavaScript execution against obvious risky file-system and shell patterns, including delete/truncate calls, raw writable streams, path traversal, user/system absolute paths, and `child_process`.
|
||||||
|
|
||||||
|
## [0.3.3] - 2026-05-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added generated tool reference documentation in `docs/TOOLS.md`.
|
||||||
|
- Added `docs:generate` and `docs:check` scripts to keep tool counts, profiles, categories, and descriptions synchronized with `lib/tool-registry.js`.
|
||||||
|
- Added a read-only `GET /tools` debug endpoint with curl examples for quick local troubleshooting.
|
||||||
|
- Added panel activity previews for recent tool calls and runtime logs.
|
||||||
|
- Added panel curl copy actions for `/health` and `/tools`.
|
||||||
|
|
||||||
|
### Optimized
|
||||||
|
|
||||||
|
- Added CI and release validation for generated tool documentation.
|
||||||
|
- Improved the MCP client config panel so the preview follows the selected client target.
|
||||||
|
- Improved the panel information architecture around troubleshooting and maintenance: version, update status, tool profile, client config, recent calls, and logs.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refined tool category inference so `get_tool_catalog` is grouped with project/context tools.
|
||||||
|
- Split file-system tools and asset refresh helpers into `lib/tools/files.js` as the first registry modularization step.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- No runtime bug fixes in this release; this release focuses on product polish, debugging ergonomics, and maintainability.
|
||||||
|
|
||||||
|
## [0.3.2] - 2026-05-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an npm-installable `funplay-cocos-mcp` stdio wrapper that bridges MCP clients to the local Cocos HTTP endpoint.
|
||||||
|
- Added MCP Registry metadata in `server.json`, including npm package ownership metadata via `mcpName`.
|
||||||
|
- Added wrapper tests, npm pack dry-run verification, and registry validation scripts.
|
||||||
|
|
||||||
## [0.3.1] - 2026-05-20
|
## [0.3.1] - 2026-05-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -88,6 +88,35 @@ When adding a new tool:
|
|||||||
6. Add clear input schema descriptions so AI clients can choose the tool correctly.
|
6. Add clear input schema descriptions so AI clients can choose the tool correctly.
|
||||||
7. Test the tool with `tools/list` and `tools/call`.
|
7. Test the tool with `tools/list` and `tools/call`.
|
||||||
|
|
||||||
|
### Asset Creation via Template Files
|
||||||
|
|
||||||
|
When a tool creates a new Cocos asset with a complex serialized format
|
||||||
|
(scene, prefab, animation clip, material, etc.), **store a template file in
|
||||||
|
`resources/` and pass its content to `asset-db: create-asset`** rather than
|
||||||
|
hard-coding JSON strings in source.
|
||||||
|
|
||||||
|
Why:
|
||||||
|
|
||||||
|
- Cocos serialized formats are version-specific and contain many required
|
||||||
|
sub-objects (e.g. a scene needs `cc.SceneGlobals` → `cc.AmbientInfo`,
|
||||||
|
`cc.ShadowsInfo`, `cc.SkyboxInfo`, `cc.FogInfo`, `cc.OctreeInfo`,
|
||||||
|
`cc.SkinInfo`, `cc.LightProbeInfo`, `cc.PostSettingsInfo`). Missing or
|
||||||
|
misnamed classes cause silent import failures or runtime errors.
|
||||||
|
- A real template file can be created/verified in the Cocos editor and
|
||||||
|
updated by simply replacing the file.
|
||||||
|
- `asset-db: create-asset(url, content)` handles `.meta` generation and
|
||||||
|
import in one step — no need for `fs.writeFileSync` + `refreshAssets`.
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
|
||||||
|
1. Create the template file at `resources/template.<ext>` (e.g.
|
||||||
|
`resources/template.scene`).
|
||||||
|
2. Add `"resources/"` to the `files` array in `package.json` so it ships
|
||||||
|
in the npm package.
|
||||||
|
3. In the tool handler, read the template with `fs.readFileSync`, optionally
|
||||||
|
replace `_name` fields, then call `create-asset` with the content string.
|
||||||
|
4. Export a pure helper (e.g. `buildSceneContent`) for unit testing.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation is bilingual:
|
The documentation is bilingual:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<a href="#"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
<a href="#"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
||||||
<a href="#"><img src="https://img.shields.io/badge/MCP-Compatible-green" alt="MCP Compatible"></a>
|
<a href="#"><img src="https://img.shields.io/badge/MCP-Compatible-green" alt="MCP Compatible"></a>
|
||||||
<a href="#"><img src="https://img.shields.io/badge/Platform-Editor%20Only-orange" alt="Editor Only"></a>
|
<a href="#"><img src="https://img.shields.io/badge/Platform-Editor%20Only-orange" alt="Editor Only"></a>
|
||||||
|
<a href="https://store.cocos.com/app/detail/8913"><img src="https://img.shields.io/badge/Cocos%20Store-Install-brightgreen" alt="Install from Cocos Store"></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="./README_CN.md">中文</a> | English
|
<a href="./README_CN.md">中文</a> | English
|
||||||
@@ -41,7 +42,9 @@ If you just want to connect quickly, do these three things:
|
|||||||
|
|
||||||
### 1. Install as a Cocos Creator Extension
|
### 1. Install as a Cocos Creator Extension
|
||||||
|
|
||||||
Clone or copy this repository into your Cocos project extension directory:
|
Recommended install: use the official [Cocos Store page](https://store.cocos.com/app/detail/8913) and install the extension into your target Cocos Creator project.
|
||||||
|
|
||||||
|
For local development or unreleased builds, clone or copy this repository into your Cocos project extension directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/your-cocos-project
|
cd /path/to/your-cocos-project
|
||||||
@@ -65,16 +68,19 @@ Funplay > MCP Server
|
|||||||
|
|
||||||
The server runs on `http://127.0.0.1:8765/` by default.
|
The server runs on `http://127.0.0.1:8765/` by default.
|
||||||
|
|
||||||
If the configured port is already occupied, the extension automatically falls back to the next available local port and uses the actual running port for one-click MCP client configuration.
|
If the configured port is already occupied, the extension first checks whether the existing listener belongs to the same Cocos project. Same-project listeners are reused safely; unrelated listeners trigger automatic fallback to the next available local port.
|
||||||
|
|
||||||
The panel is intentionally small:
|
The panel is intentionally small:
|
||||||
|
|
||||||
- Enable or disable the MCP server
|
- Enable or disable the MCP server
|
||||||
- Change the server port
|
- Change the server port
|
||||||
- Switch tool exposure between `core`, `full`, and `custom`
|
- Switch tool exposure between `core`, `full`, and `custom`
|
||||||
|
- Save, apply, import, and export named tool profiles
|
||||||
- Check the installed version against the latest GitHub release
|
- Check the installed version against the latest GitHub release
|
||||||
- Tune tool exposure by category or individual tool
|
- Inspect recent tool calls and runtime log previews
|
||||||
- Configure AI clients with one click
|
- Tune tool exposure by category controls or individual tool names
|
||||||
|
- Configure AI clients with one click and preview the selected client config
|
||||||
|
- Copy quick `curl` commands for `/health` and `/tools`
|
||||||
- Expand debug output only when needed
|
- Expand debug output only when needed
|
||||||
|
|
||||||
### 3. Configure Your AI Client
|
### 3. Configure Your AI Client
|
||||||
@@ -179,6 +185,31 @@ url = "http://127.0.0.1:8765/"
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Optional: npm stdio Wrapper
|
||||||
|
|
||||||
|
If your MCP client prefers a local `stdio` command, install the npm wrapper after starting the Cocos editor server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g funplay-cocos-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Example MCP client entry:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"funplay_cocos": {
|
||||||
|
"command": "funplay-cocos-mcp",
|
||||||
|
"env": {
|
||||||
|
"FUNPLAY_COCOS_MCP_URL": "http://127.0.0.1:8765/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The wrapper bridges stdio MCP traffic to the embedded Cocos HTTP endpoint. You can also run it with `npx funplay-cocos-mcp --url http://127.0.0.1:8765/`.
|
||||||
|
|
||||||
### 4. Verify the Connection
|
### 4. Verify the Connection
|
||||||
|
|
||||||
Open your AI client and try a few safe requests first:
|
Open your AI client and try a few safe requests first:
|
||||||
@@ -190,6 +221,13 @@ Open your AI client and try a few safe requests first:
|
|||||||
|
|
||||||
If these work, the MCP server, resources, prompts, and primary execution tool are connected correctly.
|
If these work, the MCP server, resources, prompts, and primary execution tool are connected correctly.
|
||||||
|
|
||||||
|
For local transport debugging, the panel can copy these commands, or you can run them directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:8765/health
|
||||||
|
curl http://127.0.0.1:8765/tools
|
||||||
|
```
|
||||||
|
|
||||||
### 5. Start Building
|
### 5. Start Building
|
||||||
|
|
||||||
Try a higher-level prompt in your AI client:
|
Try a higher-level prompt in your AI client:
|
||||||
@@ -201,10 +239,12 @@ 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 34 high-signal tools. Switch to `full` for all 89 tools, or use `custom` to include/exclude tool categories and individual tools.
|
- `GET /health` and `GET /tools` are read-only debug endpoints for quick local checks outside an MCP client.
|
||||||
|
- The default `core` profile exposes 37 high-signal tools. Switch to `full` for all 101 tools, or use `custom` to include/exclude tool categories and individual tools.
|
||||||
- The panel includes a manual update check against the latest GitHub release.
|
- The panel includes a manual update check against the latest GitHub release.
|
||||||
- Streamable HTTP responses follow the MCP transport requirements for `Accept`, `MCP-Protocol-Version`, JSON-RPC notifications/responses, and optional `Mcp-Session-Id` sessions.
|
- Streamable HTTP responses follow the MCP transport requirements for `Accept`, `MCP-Protocol-Version`, JSON-RPC notifications/responses, and optional `Mcp-Session-Id` sessions.
|
||||||
- Tool listings include MCP `outputSchema` and `annotations`; structured tool results use a standard envelope with `ok`, `tool`, `callId`, `summary`, `data`, and follow-up `refs`.
|
- Tool listings include MCP `outputSchema` and `annotations`; structured tool results use a standard envelope with `ok`, `tool`, `callId`, `summary`, `data`, and follow-up `refs`.
|
||||||
|
- `execute_javascript` safety checks are enabled by default. They block obvious risky filesystem and shell patterns such as delete/truncate calls, raw writable streams, path traversal, user/system absolute paths, and `child_process`. This is a guardrail, not a full sandbox; a call can explicitly pass `safety_checks: false` when you have reviewed the risk.
|
||||||
- All exposed MCP tools execute directly. There is no extra approval toggle inside the Cocos extension.
|
- All exposed MCP tools execute directly. There is no extra approval toggle inside the Cocos extension.
|
||||||
- File tools and `cocos://asset/path/...` resources are restricted to the active Cocos project root.
|
- File tools and `cocos://asset/path/...` resources are restricted to the active Cocos project root.
|
||||||
- The recommended workflow is `execute_javascript` first, then focused helper tools for screenshots, diagnostics, assets, and inspection.
|
- The recommended workflow is `execute_javascript` first, then focused helper tools for screenshots, diagnostics, assets, and inspection.
|
||||||
@@ -216,15 +256,15 @@ Try a higher-level prompt in your AI client:
|
|||||||
- **Embedded Cocos Extension** — No separate Python daemon or external bridge process is required for the Cocos-side plugin
|
- **Embedded Cocos Extension** — No separate Python daemon or external bridge process is required for the Cocos-side plugin
|
||||||
- **One-Click Client Configuration** — Configure Claude Code, Cursor, VS Code, Trae, Kiro, and Codex directly from Cocos Creator
|
- **One-Click Client Configuration** — Configure Claude Code, Cursor, VS Code, Trae, Kiro, and Codex directly from Cocos Creator
|
||||||
- **Project Context Built In** — Exposes live project, scene, selection, script diagnostics, logs, and interaction-history resources
|
- **Project Context Built In** — Exposes live project, scene, selection, script diagnostics, logs, and interaction-history resources
|
||||||
- **Focused by Default, Full When Needed** — `core` reduces tool-list noise; `full` exposes every available tool; `custom` lets you tune by category or tool
|
- **Focused by Default, Full When Needed** — `core` reduces tool-list noise; `full` exposes every available tool; `custom` plus saved profiles lets you tune and restore tool exposure by category or tool
|
||||||
- **Visual Validation** — Scene/editor/preview screenshots and input simulation help AI verify UI and gameplay changes
|
- **Visual Validation** — Scene/editor/preview screenshots and input simulation help AI verify UI and gameplay changes
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **89 Built-in Tools** — Scene hierarchy, editor state, selection workflows, prefabs, assets, project instructions, UI creation, components, files, logs, script diagnostics, screenshots, runtime control, and input simulation
|
- **101 Built-in Tools** — Scene hierarchy, editor state, selection workflows, prefabs, assets, asset dependencies, project instructions, UI creation, components, files, logs, script diagnostics, screenshots, runtime control, build/preview helpers, editor preferences, event binding, and input simulation
|
||||||
- **Primary Unified Tool** — `execute_javascript` supports both `scene` and `editor` contexts
|
- **Primary Unified Tool** — `execute_javascript` supports both `scene` and `editor` contexts
|
||||||
- **Resources & Prompts** — Live project/log resources plus reusable workflows like script fixing, scene validation, and playable prototype creation
|
- **Resources & Prompts** — Live project/log resources plus reusable workflows like script fixing, scene validation, and playable prototype creation
|
||||||
- **Cocos Panel UI** — A minimal `Funplay > MCP Server` panel for service management, update checks, tool exposure, and MCP client setup
|
- **Cocos Panel UI** — A compact `Funplay > MCP Server` panel for service management, update checks, tool exposure, recent activity, logs, curl diagnostics, and MCP client setup
|
||||||
- **Screenshot and Input Support** — Capture editor/scene/game/preview screenshots and send Electron-level mouse/keyboard events
|
- **Screenshot and Input Support** — Capture editor/scene/game/preview screenshots and send Electron-level mouse/keyboard events
|
||||||
- **Vendor Agnostic** — Works with any AI client that supports MCP over HTTP JSON-RPC
|
- **Vendor Agnostic** — Works with any AI client that supports MCP over HTTP JSON-RPC
|
||||||
|
|
||||||
@@ -238,20 +278,22 @@ Funplay MCP for Cocos follows the same design principles as Funplay MCP for Unit
|
|||||||
| Embedded server | Built-in HTTP MCP server | Built-in HTTP MCP server |
|
| Embedded server | Built-in HTTP MCP server | Built-in HTTP MCP server |
|
||||||
| Primary execution tool | `execute_javascript` | `execute_code` |
|
| Primary execution tool | `execute_javascript` | `execute_code` |
|
||||||
| Primary language | JavaScript in scene/editor contexts | C# in Unity editor/runtime contexts |
|
| Primary language | JavaScript in scene/editor contexts | C# in Unity editor/runtime contexts |
|
||||||
| Default profile | `core` with 34 tools | `core` focused tool profile |
|
| Default profile | `core` with 37 tools | `core` focused tool profile |
|
||||||
| Full profile | 89 tools plus `custom` exposure | 79 tools |
|
| Full profile | 101 tools plus `custom` exposure | 79 tools |
|
||||||
| Client setup | One-click config panel | One-click config window |
|
| Client setup | One-click config panel | One-click config window |
|
||||||
|
|
||||||
## MCP Capabilities
|
## MCP Capabilities
|
||||||
|
|
||||||
The current package exposes four capability layers:
|
The current package exposes four capability layers:
|
||||||
|
|
||||||
- **Tools** — 34 tools in `core`, 89 tools in `full`, plus `custom` include/exclude rules
|
- **Tools** — 37 tools in `core`, 101 tools in `full`, plus `custom` include/exclude rules and saved tool profiles
|
||||||
- **Primary execution** — `execute_javascript` for scene/runtime and editor/browser automation
|
- **Primary execution** — `execute_javascript` for scene/runtime and editor/browser automation
|
||||||
- **Prompts** — `fix_script_errors`, `create_playable_prototype`, `scene_validation`, and `auto_wire_scene`
|
- **Prompts** — `fix_script_errors`, `create_playable_prototype`, `scene_validation`, and `auto_wire_scene`
|
||||||
- **Resources** — project context, scene summaries, current selection, script diagnostics, asset selection, logs, and MCP interaction history
|
- **Resources** — project context, scene summaries, current selection, script diagnostics, asset selection, logs, and MCP interaction history
|
||||||
|
|
||||||
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`.
|
For the generated tool reference, including categories, profiles, and read/mutation hints, see [docs/TOOLS.md](./docs/TOOLS.md).
|
||||||
|
|
||||||
|
The default `core` set is intentionally small: `execute_javascript`, `execute_scene_script`, `execute_editor_script`, `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `list_project_instructions`, `read_project_instruction`, `set_selection`, `get_project_info`, `get_build_status`, `get_scene_info`, `get_hierarchy`, `list_scenes`, `open_scene`, `inspect_prefab`, `validate_prefab_references`, `inspect_prefab_instance`, `list_assets`, `inspect_asset`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `open_asset`, `select_asset`, `run_script_diagnostics`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot`, `get_script_diagnostic_context`, `get_runtime_state`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_preview_screenshot`, and `list_editor_windows`.
|
||||||
|
|
||||||
## Built-in Resources
|
## Built-in Resources
|
||||||
|
|
||||||
@@ -270,24 +312,25 @@ The default `core` set is intentionally small: `execute_javascript`, `execute_sc
|
|||||||
|
|
||||||
## Built-in Tools
|
## Built-in Tools
|
||||||
|
|
||||||
Funplay MCP for Cocos currently ships with **89 tool functions** in the `full` profile:
|
Funplay MCP for Cocos currently ships with **101 tool functions** in the `full` profile:
|
||||||
|
|
||||||
| Category | Tools |
|
| Category | Tools |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| **Script Execution** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
| **Script Execution** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
||||||
| **Editor State** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
| **Editor State** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
||||||
| **Project Instructions** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill` |
|
| **Project Instructions** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill`, `create_cocos_mcp_project_skill` |
|
||||||
| **Project & Scene** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
| **Project & Scene** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
||||||
| **Node Editing** | `create_node`, `delete_node`, `set_node_transform` |
|
| **Node Editing** | `create_node`, `delete_node`, `set_node_transform` |
|
||||||
| **Assets & Prefabs** | `list_assets`, `inspect_asset`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
| **Assets & Prefabs** | `list_assets`, `inspect_asset`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
||||||
| **Components** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
| **Components** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
||||||
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
||||||
| **Camera** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
| **Camera** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
||||||
| **Animation** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
| **Animation** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
||||||
| **Files** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
| **Files** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
||||||
| **Diagnostics & Logs** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
| **Diagnostics & Logs** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
||||||
|
| **Build & Editor** | `get_build_status`, `open_build_panel`, `run_project_preview`, `save_current_scene`, `get_editor_preference`, `set_editor_preference`, `broadcast_editor_message` |
|
||||||
| **Runtime** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
| **Runtime** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
||||||
| **Interaction** | `emit_node_event`, `simulate_button_click`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
| **Interaction & Events** | `emit_node_event`, `simulate_button_click`, `list_button_click_events`, `bind_button_click_event`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
||||||
| **Screenshots & Windows** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
| **Screenshots & Windows** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
||||||
|
|
||||||
## Primary Tool Examples
|
## Primary Tool Examples
|
||||||
@@ -328,8 +371,11 @@ Place `funplay-cocos-mcp.config.json` in the Cocos project root:
|
|||||||
"enabledTools": [],
|
"enabledTools": [],
|
||||||
"disabledTools": [],
|
"disabledTools": [],
|
||||||
"enableSessions": false,
|
"enableSessions": false,
|
||||||
|
"executeJavascriptSafetyChecks": true,
|
||||||
"autostart": true,
|
"autostart": true,
|
||||||
"maxInteractionLogEntries": 50
|
"maxInteractionLogEntries": 50,
|
||||||
|
"activeToolProfileName": "",
|
||||||
|
"savedToolProfiles": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -339,7 +385,7 @@ Environment variables are also supported:
|
|||||||
- `COCOS_MCP_PORT`
|
- `COCOS_MCP_PORT`
|
||||||
- `COCOS_MCP_PROFILE`
|
- `COCOS_MCP_PROFILE`
|
||||||
|
|
||||||
`toolProfile: "custom"` starts from the `core` set, then adds `enabledToolCategories` / `enabledTools` and removes `disabledToolCategories` / `disabledTools`. `enableSessions` is off by default because this server does not need cross-request client state for normal editor automation.
|
`toolProfile: "custom"` starts from the `core` set, then adds `enabledToolCategories` / `enabledTools` and removes `disabledToolCategories` / `disabledTools`. The panel can save these exposure settings as named `savedToolProfiles` for quick restore or sharing. `enableSessions` is off by default because this server does not need cross-request client state for normal editor automation.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -357,10 +403,16 @@ Cocos Creator Extension
|
|||||||
│ └─ Minimal MCP Server panel
|
│ └─ Minimal MCP Server panel
|
||||||
└─ lib/
|
└─ lib/
|
||||||
├─ assets, diagnostics, screenshots, input
|
├─ assets, diagnostics, screenshots, input
|
||||||
|
├─ tool-profiles, javascript-safety
|
||||||
|
├─ tools/
|
||||||
|
│ ├─ files
|
||||||
|
│ ├─ assets-advanced
|
||||||
|
│ ├─ cocos-project
|
||||||
|
│ └─ scene-events
|
||||||
└─ server, resources, prompts, tool registry
|
└─ server, resources, prompts, tool registry
|
||||||
```
|
```
|
||||||
|
|
||||||
The server speaks MCP-style HTTP JSON-RPC 2.0 and supports tools, resources, resource templates, prompts, and health checks.
|
The server speaks MCP-style HTTP JSON-RPC 2.0 and supports tools, resources, resource templates, prompts, health checks, and a read-only `/tools` debug endpoint.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -369,7 +421,15 @@ Run checks before publishing changes:
|
|||||||
```bash
|
```bash
|
||||||
npm run check
|
npm run check
|
||||||
npm test
|
npm test
|
||||||
|
npm run docs:check
|
||||||
npm run release:check
|
npm run release:check
|
||||||
|
npm run pack:dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Regenerate the tool reference after changing `lib/tool-registry.js`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run docs:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
To generate a GitHub Release-ready extension package:
|
To generate a GitHub Release-ready extension package:
|
||||||
@@ -380,6 +440,12 @@ 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.
|
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.
|
||||||
|
|
||||||
|
Validate MCP Registry metadata before publishing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run registry:validate
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License. See [LICENSE](./LICENSE).
|
MIT License. See [LICENSE](./LICENSE).
|
||||||
|
|||||||
+86
-20
@@ -8,6 +8,7 @@
|
|||||||
<a href="#"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
<a href="#"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
||||||
<a href="#"><img src="https://img.shields.io/badge/MCP-Compatible-green" alt="MCP Compatible"></a>
|
<a href="#"><img src="https://img.shields.io/badge/MCP-Compatible-green" alt="MCP Compatible"></a>
|
||||||
<a href="#"><img src="https://img.shields.io/badge/Platform-Editor%20Only-orange" alt="Editor Only"></a>
|
<a href="#"><img src="https://img.shields.io/badge/Platform-Editor%20Only-orange" alt="Editor Only"></a>
|
||||||
|
<a href="https://store.cocos.com/app/detail/8913"><img src="https://img.shields.io/badge/Cocos%20Store-Install-brightgreen" alt="Install from Cocos Store"></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
中文 | <a href="./README.md">English</a>
|
中文 | <a href="./README.md">English</a>
|
||||||
@@ -41,7 +42,9 @@ Funplay MCP for Cocos 是一个采用 MIT 协议的 Cocos Creator 扩展,它
|
|||||||
|
|
||||||
### 1. 安装为 Cocos Creator 扩展
|
### 1. 安装为 Cocos Creator 扩展
|
||||||
|
|
||||||
把仓库 clone 或复制到 Cocos 项目的扩展目录:
|
推荐安装方式:通过官方 [Cocos Store 页面](https://store.cocos.com/app/detail/8913) 安装到目标 Cocos Creator 项目。
|
||||||
|
|
||||||
|
如果要本地开发或使用未发布版本,可以把仓库 clone 或复制到 Cocos 项目的扩展目录:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/your-cocos-project
|
cd /path/to/your-cocos-project
|
||||||
@@ -65,16 +68,19 @@ Funplay > MCP Server
|
|||||||
|
|
||||||
服务默认运行在 `http://127.0.0.1:8765/`。
|
服务默认运行在 `http://127.0.0.1:8765/`。
|
||||||
|
|
||||||
如果配置端口已被占用,扩展会自动回退到下一个可用本地端口,并在一键客户端配置时使用实际运行端口。
|
如果配置端口已被占用,扩展会先检查已有 listener 是否属于同一个 Cocos 项目;同项目 listener 会被安全复用,无关 listener 才会自动回退到下一个可用本地端口。
|
||||||
|
|
||||||
面板刻意保持精简:
|
面板刻意保持精简:
|
||||||
|
|
||||||
- 启用或停用 MCP Server
|
- 启用或停用 MCP Server
|
||||||
- 修改服务端口
|
- 修改服务端口
|
||||||
- 在 `core` / `full` / `custom` 工具暴露模式之间切换
|
- 在 `core` / `full` / `custom` 工具暴露模式之间切换
|
||||||
|
- 保存、套用、导入和导出命名工具 profile
|
||||||
- 检查当前安装版本是否落后于 GitHub 最新 Release
|
- 检查当前安装版本是否落后于 GitHub 最新 Release
|
||||||
- 按工具分类或单个工具调整暴露范围
|
- 查看最近工具调用和运行日志预览
|
||||||
- 一键配置 AI 客户端
|
- 通过分类控制或单个工具名调整暴露范围
|
||||||
|
- 一键配置 AI 客户端,并随目标客户端预览对应配置
|
||||||
|
- 复制 `/health` 和 `/tools` 的快速 `curl` 排障命令
|
||||||
- 需要时再展开 Debug Output
|
- 需要时再展开 Debug Output
|
||||||
|
|
||||||
### 3. 配置 AI 客户端
|
### 3. 配置 AI 客户端
|
||||||
@@ -179,6 +185,31 @@ url = "http://127.0.0.1:8765/"
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### 可选:npm stdio Wrapper
|
||||||
|
|
||||||
|
如果 MCP 客户端更适合使用本地 `stdio` 命令,可以在启动 Cocos 编辑器内置服务后安装 npm wrapper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g funplay-cocos-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
示例 MCP 客户端配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"funplay_cocos": {
|
||||||
|
"command": "funplay-cocos-mcp",
|
||||||
|
"env": {
|
||||||
|
"FUNPLAY_COCOS_MCP_URL": "http://127.0.0.1:8765/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个 wrapper 会把 stdio MCP 流量桥接到 Cocos 内置 HTTP endpoint。也可以直接运行 `npx funplay-cocos-mcp --url http://127.0.0.1:8765/`。
|
||||||
|
|
||||||
### 4. 验证连接
|
### 4. 验证连接
|
||||||
|
|
||||||
先在 AI 客户端里试几个安全请求:
|
先在 AI 客户端里试几个安全请求:
|
||||||
@@ -190,6 +221,13 @@ url = "http://127.0.0.1:8765/"
|
|||||||
|
|
||||||
如果这些都正常返回,说明 MCP server、resources、prompts 和主执行工具已经连通。
|
如果这些都正常返回,说明 MCP server、resources、prompts 和主执行工具已经连通。
|
||||||
|
|
||||||
|
如果要排查本地传输链路,面板可以直接复制下面的命令,也可以手动执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:8765/health
|
||||||
|
curl http://127.0.0.1:8765/tools
|
||||||
|
```
|
||||||
|
|
||||||
### 5. 开始构建
|
### 5. 开始构建
|
||||||
|
|
||||||
可以在 AI 客户端里尝试:
|
可以在 AI 客户端里尝试:
|
||||||
@@ -200,11 +238,13 @@ 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/`。
|
||||||
- 如果配置端口被占用,服务会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
|
- 如果配置端口被占用,服务会先通过项目身份识别同项目已有 listener;无法确认同项目时才会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
|
||||||
- 默认 `core` profile 暴露 34 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 89 个工具;也可以用 `custom` 按分类或工具名增删。
|
- `GET /health` 和 `GET /tools` 是只读调试端点,方便不用 MCP 客户端也能快速检查本地服务。
|
||||||
|
- 默认 `core` profile 暴露 37 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 101 个工具;也可以用 `custom` 按分类或工具名增删。
|
||||||
- 面板提供手动更新检查,会对比当前安装版本和 GitHub 最新 Release。
|
- 面板提供手动更新检查,会对比当前安装版本和 GitHub 最新 Release。
|
||||||
- Streamable HTTP 响应已补齐 MCP 传输层要求,包括 `Accept`、`MCP-Protocol-Version`、JSON-RPC notification/response,以及可选 `Mcp-Session-Id` session。
|
- Streamable HTTP 响应已补齐 MCP 传输层要求,包括 `Accept`、`MCP-Protocol-Version`、JSON-RPC notification/response,以及可选 `Mcp-Session-Id` session。
|
||||||
- 工具列表会包含 MCP `outputSchema` 和 `annotations`;结构化工具结果统一使用包含 `ok`、`tool`、`callId`、`summary`、`data`、`refs` 的标准 envelope。
|
- 工具列表会包含 MCP `outputSchema` 和 `annotations`;结构化工具结果统一使用包含 `ok`、`tool`、`callId`、`summary`、`data`、`refs` 的标准 envelope。
|
||||||
|
- `execute_javascript` 安全检查默认开启,会拦截明显高风险的文件系统和 shell 模式,例如删除/截断调用、原始写入流、路径穿越、用户/系统绝对路径和 `child_process`。这是防护栏,不是完整沙箱;确认风险后可在单次调用中显式传入 `safety_checks: false`。
|
||||||
- 所有已暴露的 MCP 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。
|
- 所有已暴露的 MCP 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。
|
||||||
- 文件工具和 `cocos://asset/path/...` 资源默认只能访问当前 Cocos 项目根目录内的路径。
|
- 文件工具和 `cocos://asset/path/...` 资源默认只能访问当前 Cocos 项目根目录内的路径。
|
||||||
- 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。
|
- 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。
|
||||||
@@ -216,15 +256,15 @@ url = "http://127.0.0.1:8765/"
|
|||||||
- **嵌入式 Cocos 扩展** — Cocos 侧不需要单独 Python 守护进程或外部 bridge
|
- **嵌入式 Cocos 扩展** — Cocos 侧不需要单独 Python 守护进程或外部 bridge
|
||||||
- **一键客户端配置** — 在 Cocos Creator 内直接配置 Claude Code、Cursor、VS Code、Trae、Kiro、Codex
|
- **一键客户端配置** — 在 Cocos Creator 内直接配置 Claude Code、Cursor、VS Code、Trae、Kiro、Codex
|
||||||
- **内建项目上下文** — 直接暴露项目、场景、选择、脚本诊断、日志和交互历史资源
|
- **内建项目上下文** — 直接暴露项目、场景、选择、脚本诊断、日志和交互历史资源
|
||||||
- **默认聚焦,必要时全量** — `core` 降低工具列表噪音,需要时切到 `full` 暴露全部工具;`custom` 可按分类或工具名调整
|
- **默认聚焦,必要时全量** — `core` 降低工具列表噪音,需要时切到 `full` 暴露全部工具;`custom` 与保存的 profile 可按分类或工具名调整并恢复
|
||||||
- **可视化验证** — 截图和输入模拟让 AI 能验证 UI 与玩法改动
|
- **可视化验证** — 截图和输入模拟让 AI 能验证 UI 与玩法改动
|
||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
|
|
||||||
- **89 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制和输入模拟
|
- **101 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、资产依赖、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制、构建/预览辅助、编辑器偏好、事件绑定和输入模拟
|
||||||
- **统一主工具** — `execute_javascript` 同时支持 `scene` 和 `editor` 两种上下文
|
- **统一主工具** — `execute_javascript` 同时支持 `scene` 和 `editor` 两种上下文
|
||||||
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
|
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
|
||||||
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露和 MCP 客户端配置
|
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露、最近活动、日志、curl 排障和 MCP 客户端配置
|
||||||
- **截图与输入支持** — 支持编辑器/场景/Game/Preview 截图,以及 Electron 级鼠标键盘事件
|
- **截图与输入支持** — 支持编辑器/场景/Game/Preview 截图,以及 Electron 级鼠标键盘事件
|
||||||
- **厂商无关** — 兼容任意支持 HTTP JSON-RPC MCP 的 AI 客户端
|
- **厂商无关** — 兼容任意支持 HTTP JSON-RPC MCP 的 AI 客户端
|
||||||
|
|
||||||
@@ -238,20 +278,22 @@ Funplay MCP for Cocos 延续 Funplay MCP for Unity 的设计原则,并针对 C
|
|||||||
| 内置服务 | 内嵌 HTTP MCP Server | 内嵌 HTTP MCP Server |
|
| 内置服务 | 内嵌 HTTP MCP Server | 内嵌 HTTP MCP Server |
|
||||||
| 主执行工具 | `execute_javascript` | `execute_code` |
|
| 主执行工具 | `execute_javascript` | `execute_code` |
|
||||||
| 主语言 | 场景/编辑器上下文中的 JavaScript | Unity 编辑器/运行态中的 C# |
|
| 主语言 | 场景/编辑器上下文中的 JavaScript | Unity 编辑器/运行态中的 C# |
|
||||||
| 默认工具集 | `core`,34 个工具 | 聚焦版 `core` 工具集 |
|
| 默认工具集 | `core`,37 个工具 | 聚焦版 `core` 工具集 |
|
||||||
| 完整工具集 | 89 个工具,并支持 `custom` 暴露 | 79 个工具 |
|
| 完整工具集 | 101 个工具,并支持 `custom` 暴露 | 79 个工具 |
|
||||||
| 客户端配置 | 一键配置面板 | 一键配置窗口 |
|
| 客户端配置 | 一键配置面板 | 一键配置窗口 |
|
||||||
|
|
||||||
## MCP 能力结构
|
## MCP 能力结构
|
||||||
|
|
||||||
当前包提供四层能力:
|
当前包提供四层能力:
|
||||||
|
|
||||||
- **Tools** — `core` 下 34 个工具,`full` 下 89 个工具,并支持 `custom` include/exclude 规则
|
- **Tools** — `core` 下 37 个工具,`full` 下 101 个工具,并支持 `custom` include/exclude 规则和命名工具 profile
|
||||||
- **Primary execution** — `execute_javascript` 用于场景/运行态和编辑器/browser 自动化
|
- **Primary execution** — `execute_javascript` 用于场景/运行态和编辑器/browser 自动化
|
||||||
- **Prompts** — `fix_script_errors`、`create_playable_prototype`、`scene_validation`、`auto_wire_scene`
|
- **Prompts** — `fix_script_errors`、`create_playable_prototype`、`scene_validation`、`auto_wire_scene`
|
||||||
- **Resources** — 项目上下文、场景摘要、当前选择、脚本诊断、资产选择、日志和 MCP 交互历史
|
- **Resources** — 项目上下文、场景摘要、当前选择、脚本诊断、资产选择、日志和 MCP 交互历史
|
||||||
|
|
||||||
当前默认 `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`。
|
自动生成的工具参考文档见 [docs/TOOLS.md](./docs/TOOLS.md),里面包含工具分类、profile 和读写/变更提示。
|
||||||
|
|
||||||
|
当前默认 `core` 工具集刻意保持精简,只包含:`execute_javascript`、`execute_scene_script`、`execute_editor_script`、`get_editor_state`、`get_tool_catalog`、`check_for_updates`、`get_selection`、`list_project_instructions`、`read_project_instruction`、`set_selection`、`get_project_info`、`get_build_status`、`get_scene_info`、`get_hierarchy`、`list_scenes`、`open_scene`、`inspect_prefab`、`validate_prefab_references`、`inspect_prefab_instance`、`list_assets`、`inspect_asset`、`inspect_asset_dependencies`、`validate_asset_dependencies`、`open_asset`、`select_asset`、`run_script_diagnostics`、`get_recent_logs`、`search_project_logs`、`clear_logs`、`validate_scene`、`get_performance_snapshot`、`get_script_diagnostic_context`、`get_runtime_state`、`capture_editor_screenshot`、`capture_scene_screenshot`、`capture_preview_screenshot`、`list_editor_windows`。
|
||||||
|
|
||||||
## 内置 Resources
|
## 内置 Resources
|
||||||
|
|
||||||
@@ -270,24 +312,25 @@ Funplay MCP for Cocos 延续 Funplay MCP for Unity 的设计原则,并针对 C
|
|||||||
|
|
||||||
## 内置工具
|
## 内置工具
|
||||||
|
|
||||||
Funplay MCP for Cocos 当前在 `full` profile 下提供 **89 个工具函数**:
|
Funplay MCP for Cocos 当前在 `full` profile 下提供 **101 个工具函数**:
|
||||||
|
|
||||||
| 分类 | 工具 |
|
| 分类 | 工具 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **脚本执行** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
| **脚本执行** | `execute_javascript`, `execute_scene_script`, `execute_editor_script` |
|
||||||
| **编辑器状态** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
| **编辑器状态** | `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `set_selection`, `get_editor_selection` |
|
||||||
| **项目指令** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill` |
|
| **项目指令** | `list_project_instructions`, `read_project_instruction`, `write_project_instruction`, `create_project_skill`, `create_cocos_mcp_project_skill` |
|
||||||
| **项目与场景** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
| **项目与场景** | `get_project_info`, `get_scene_info`, `get_hierarchy`, `find_nodes`, `inspect_node`, `list_scenes`, `open_scene`, `run_scene_asset` |
|
||||||
| **节点编辑** | `create_node`, `delete_node`, `set_node_transform` |
|
| **节点编辑** | `create_node`, `delete_node`, `set_node_transform` |
|
||||||
| **资产与 Prefab** | `list_assets`, `inspect_asset`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
| **资产与 Prefab** | `list_assets`, `inspect_asset`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `open_asset`, `select_asset`, `delete_asset`, `list_prefabs`, `inspect_prefab`, `validate_prefab_references`, `duplicate_prefab`, `edit_prefab_json`, `create_prefab_instance`, `inspect_prefab_instance`, `apply_prefab_instance`, `revert_prefab_instance`, `instantiate_prefab` |
|
||||||
| **组件** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
| **组件** | `list_components`, `inspect_component`, `add_component`, `remove_component`, `set_component_property`, `reset_component_property` |
|
||||||
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
| **UI** | `create_canvas`, `create_label`, `create_button`, `create_sprite` |
|
||||||
| **相机** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
| **相机** | `list_cameras`, `create_camera`, `set_camera_properties` |
|
||||||
| **动画** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
| **动画** | `list_animations`, `add_animation_clip`, `play_animation`, `stop_animation` |
|
||||||
| **文件** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
| **文件** | `read_file`, `get_file_snippet`, `write_file`, `replace_in_file`, `search_files`, `list_directory`, `exists`, `refresh_assets` |
|
||||||
| **诊断与日志** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
| **诊断与日志** | `run_script_diagnostics`, `get_script_diagnostic_context`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot` |
|
||||||
|
| **构建与编辑器** | `get_build_status`, `open_build_panel`, `run_project_preview`, `save_current_scene`, `get_editor_preference`, `set_editor_preference`, `broadcast_editor_message` |
|
||||||
| **运行态** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
| **运行态** | `get_runtime_state`, `pause_runtime`, `resume_runtime`, `set_time_scale` |
|
||||||
| **交互** | `emit_node_event`, `simulate_button_click`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
| **交互与事件** | `emit_node_event`, `simulate_button_click`, `list_button_click_events`, `bind_button_click_event`, `invoke_component_method`, `simulate_mouse_click`, `simulate_mouse_drag`, `simulate_key_press`, `simulate_key_combo`, `simulate_preview_input` |
|
||||||
| **截图与窗口** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
| **截图与窗口** | `capture_desktop_screenshot`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_game_screenshot`, `capture_preview_screenshot`, `list_editor_windows` |
|
||||||
|
|
||||||
## 主工具示例
|
## 主工具示例
|
||||||
@@ -328,8 +371,11 @@ Editor 上下文脚本可以访问 `Editor`、`fs`、`path`、`os`、`require`
|
|||||||
"enabledTools": [],
|
"enabledTools": [],
|
||||||
"disabledTools": [],
|
"disabledTools": [],
|
||||||
"enableSessions": false,
|
"enableSessions": false,
|
||||||
|
"executeJavascriptSafetyChecks": true,
|
||||||
"autostart": true,
|
"autostart": true,
|
||||||
"maxInteractionLogEntries": 50
|
"maxInteractionLogEntries": 50,
|
||||||
|
"activeToolProfileName": "",
|
||||||
|
"savedToolProfiles": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -339,7 +385,7 @@ Editor 上下文脚本可以访问 `Editor`、`fs`、`path`、`os`、`require`
|
|||||||
- `COCOS_MCP_PORT`
|
- `COCOS_MCP_PORT`
|
||||||
- `COCOS_MCP_PROFILE`
|
- `COCOS_MCP_PROFILE`
|
||||||
|
|
||||||
`toolProfile: "custom"` 会从 `core` 集合开始,再加入 `enabledToolCategories` / `enabledTools`,并移除 `disabledToolCategories` / `disabledTools`。`enableSessions` 默认关闭,因为常规编辑器自动化不需要跨请求客户端状态。
|
`toolProfile: "custom"` 会从 `core` 集合开始,再加入 `enabledToolCategories` / `enabledTools`,并移除 `disabledToolCategories` / `disabledTools`。面板可以把这些暴露设置保存为命名 `savedToolProfiles`,方便恢复或分享。`enableSessions` 默认关闭,因为常规编辑器自动化不需要跨请求客户端状态。
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
@@ -357,10 +403,16 @@ Cocos Creator Extension
|
|||||||
│ └─ Minimal MCP Server panel
|
│ └─ Minimal MCP Server panel
|
||||||
└─ lib/
|
└─ lib/
|
||||||
├─ assets, diagnostics, screenshots, input
|
├─ assets, diagnostics, screenshots, input
|
||||||
|
├─ tool-profiles, javascript-safety
|
||||||
|
├─ tools/
|
||||||
|
│ ├─ files
|
||||||
|
│ ├─ assets-advanced
|
||||||
|
│ ├─ cocos-project
|
||||||
|
│ └─ scene-events
|
||||||
└─ server, resources, prompts, tool registry
|
└─ server, resources, prompts, tool registry
|
||||||
```
|
```
|
||||||
|
|
||||||
服务使用 MCP 风格的 HTTP JSON-RPC 2.0,支持 tools、resources、resource templates、prompts 和 health check。
|
服务使用 MCP 风格的 HTTP JSON-RPC 2.0,支持 tools、resources、resource templates、prompts、health check 和只读 `/tools` 调试端点。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
@@ -369,7 +421,15 @@ Cocos Creator Extension
|
|||||||
```bash
|
```bash
|
||||||
npm run check
|
npm run check
|
||||||
npm test
|
npm test
|
||||||
|
npm run docs:check
|
||||||
npm run release:check
|
npm run release:check
|
||||||
|
npm run pack:dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
修改 `lib/tool-registry.js` 后可以重新生成工具参考:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run docs:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
生成可上传到 GitHub Release 的扩展包:
|
生成可上传到 GitHub Release 的扩展包:
|
||||||
@@ -380,6 +440,12 @@ npm run release:package
|
|||||||
|
|
||||||
产物会写入 `releases/<version>/`,包含 zip、manifest、checksum 和 release README。完整流程见 [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) 和 [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md)。
|
产物会写入 `releases/<version>/`,包含 zip、manifest、checksum 和 release README。完整流程见 [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) 和 [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md)。
|
||||||
|
|
||||||
|
发布 MCP Registry 前可以验证元数据:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run registry:validate
|
||||||
|
```
|
||||||
|
|
||||||
## 协议
|
## 协议
|
||||||
|
|
||||||
MIT License。详见 [LICENSE](./LICENSE)。
|
MIT License。详见 [LICENSE](./LICENSE)。
|
||||||
|
|||||||
+29
-5
@@ -14,19 +14,25 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
|
|||||||
|
|
||||||
- [ ] `npm run check` passes
|
- [ ] `npm run check` passes
|
||||||
- [ ] `npm test` passes
|
- [ ] `npm test` passes
|
||||||
|
- [ ] `npm run docs:check` passes
|
||||||
- [ ] `npm run release:check` passes
|
- [ ] `npm run release:check` passes
|
||||||
|
- [ ] `npm run pack:dry-run` passes
|
||||||
|
- [ ] `npm run registry:validate` passes when `mcp-publisher` is available
|
||||||
- [ ] `npm run release:package` creates `releases/<version>/`
|
- [ ] `npm run release:package` creates `releases/<version>/`
|
||||||
- [ ] `shasum -a 256 -c releases/<version>/SHA256SUMS.txt` passes
|
- [ ] `shasum -a 256 -c releases/<version>/SHA256SUMS.txt` passes
|
||||||
|
- [ ] `releases/<version>/RELEASE_NOTES.md` is organized by change type, such as Added/Optimized/Changed/Fixed
|
||||||
|
|
||||||
## 3. Package Contents
|
## 3. Package Contents
|
||||||
|
|
||||||
- [ ] The zip is named `Funplay.CocosMcp.v<version>.zip`
|
- [ ] The zip is named `Funplay.CocosMcp.v<version>.zip`
|
||||||
- [ ] The zip contains a single top-level `funplay-cocos-mcp/` folder
|
- [ ] 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 contains runtime files: `package.json`, `browser.js`, `scene.js`, `panel/`, and `lib/`
|
||||||
|
- [ ] The zip contains stdio wrapper metadata: `bin/funplay-cocos-mcp.js` and `server.json`
|
||||||
- [ ] The zip includes docs: `README.md`, `README_CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `LICENSE`
|
- [ ] The zip includes docs: `README.md`, `README_CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `LICENSE`
|
||||||
- [ ] The zip does not contain `.git/`, `.github/`, `.DS_Store`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`, `test/`, or `scripts/`
|
- [ ] The zip does not contain `.git/`, `.github/`, `.DS_Store`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`, `test/`, or `scripts/`
|
||||||
|
- [ ] Release packaging sensitive-content scan does not find npm/GitHub/MCP tokens or private keys
|
||||||
- [ ] `release-manifest.json` references the correct GitHub download URL
|
- [ ] `release-manifest.json` references the correct GitHub download URL
|
||||||
- [ ] `SHA256SUMS.txt` includes the zip, manifest, and release README
|
- [ ] `SHA256SUMS.txt` includes the zip, manifest, generated release notes, and release README
|
||||||
|
|
||||||
## 4. Cocos Smoke Test
|
## 4. Cocos Smoke Test
|
||||||
|
|
||||||
@@ -47,27 +53,45 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
|
|||||||
- [ ] Confirm `tools/list` returns the expected `core` profile tools
|
- [ ] Confirm `tools/list` returns the expected `core` profile tools
|
||||||
- [ ] Confirm a tool call succeeds end-to-end from the external client
|
- [ ] Confirm a tool call succeeds end-to-end from the external client
|
||||||
- [ ] Verify one-click config output still matches the documented config snippets
|
- [ ] Verify one-click config output still matches the documented config snippets
|
||||||
|
- [ ] Verify the stdio wrapper can connect with `funplay-cocos-mcp --url http://127.0.0.1:8765/`
|
||||||
|
|
||||||
## 6. GitHub Release Readiness
|
## 6. npm And MCP Registry Readiness
|
||||||
|
|
||||||
|
- [ ] `package.json` `name` is `funplay-cocos-mcp`
|
||||||
|
- [ ] `package.json` `version` matches `server.json` version
|
||||||
|
- [ ] `package.json` `mcpName` matches `server.json` name
|
||||||
|
- [ ] `package.json` `bin.funplay-cocos-mcp` points to `bin/funplay-cocos-mcp.js`
|
||||||
|
- [ ] `server.json` npm package identifier and version match `package.json`
|
||||||
|
- [ ] `server.json` npm transport type is `stdio`
|
||||||
|
- [ ] npm package dry-run includes `bin/`, `lib/`, `panel/`, `browser.js`, `scene.js`, and `server.json`
|
||||||
|
- [ ] npm credentials are available for `npm publish`
|
||||||
|
- [ ] MCP Registry credentials are available for `mcp-publisher publish`
|
||||||
|
|
||||||
|
## 7. GitHub Release Readiness
|
||||||
|
|
||||||
- [ ] CI passes on `main`
|
- [ ] CI passes on `main`
|
||||||
- [ ] Release commit message is `Release v<version>`
|
- [ ] Release commit message is `Release v<version>`
|
||||||
- [ ] Tag is `v<version>`
|
- [ ] Tag is `v<version>`
|
||||||
- [ ] GitHub Release title is `v<version>`
|
- [ ] GitHub Release title is `v<version>`
|
||||||
- [ ] GitHub Release includes the zip, manifest, checksum file, and release README
|
- [ ] GitHub Release includes the zip, manifest, checksum file, and release README
|
||||||
- [ ] Public GitHub Release page renders the release notes and assets correctly
|
- [ ] Public GitHub Release page uses `RELEASE_NOTES.md` and renders the release notes/assets correctly
|
||||||
|
|
||||||
## 7. Publish
|
## 8. Publish
|
||||||
|
|
||||||
- [ ] Commit the release changes
|
- [ ] Commit the release changes
|
||||||
- [ ] Create and push the release tag
|
- [ ] Create and push the release tag
|
||||||
- [ ] Create or update the GitHub Release
|
- [ ] Create or update the GitHub Release
|
||||||
- [ ] Upload generated release assets
|
- [ ] Upload generated release assets
|
||||||
- [ ] Verify the GitHub Release asset list
|
- [ ] Verify the GitHub Release asset list
|
||||||
|
- [ ] Publish npm package with `npm publish`
|
||||||
|
- [ ] Verify npm package with `npm view funplay-cocos-mcp@<version>`
|
||||||
|
- [ ] Publish MCP Registry metadata with `mcp-publisher publish server.json`
|
||||||
|
- [ ] Verify MCP Registry latest and specific-version endpoints
|
||||||
|
|
||||||
## 8. Post-Release
|
## 9. Post-Release
|
||||||
|
|
||||||
- [ ] Re-test installation from the public GitHub Release zip
|
- [ ] Re-test installation from the public GitHub Release zip
|
||||||
|
- [ ] Re-test stdio wrapper installation from npm
|
||||||
- [ ] Check the update checker reports the new latest version
|
- [ ] Check the update checker reports the new latest version
|
||||||
- [ ] Check README install instructions and download links
|
- [ ] Check README install instructions and download links
|
||||||
- [ ] Announce the release where appropriate
|
- [ ] Announce the release where appropriate
|
||||||
|
|||||||
+118
-6
@@ -5,6 +5,8 @@ This document records the release workflow for publishing Funplay MCP for Cocos
|
|||||||
- Git tags
|
- Git tags
|
||||||
- GitHub Releases
|
- GitHub Releases
|
||||||
- Downloadable Cocos Creator extension zip packages
|
- Downloadable Cocos Creator extension zip packages
|
||||||
|
- npm stdio wrapper package
|
||||||
|
- Official MCP Registry
|
||||||
|
|
||||||
## Published Identity
|
## Published Identity
|
||||||
|
|
||||||
@@ -13,6 +15,9 @@ This document records the release workflow for publishing Funplay MCP for Cocos
|
|||||||
- GitHub Release tag: `v<version>`
|
- GitHub Release tag: `v<version>`
|
||||||
- Extension package asset: `Funplay.CocosMcp.v<version>.zip`
|
- Extension package asset: `Funplay.CocosMcp.v<version>.zip`
|
||||||
- Cocos extension folder name inside the zip: `funplay-cocos-mcp`
|
- Cocos extension folder name inside the zip: `funplay-cocos-mcp`
|
||||||
|
- npm package id: `funplay-cocos-mcp`
|
||||||
|
- npm command: `funplay-cocos-mcp`
|
||||||
|
- MCP Registry server name: `io.github.FunplayAI/funplay-cocos-mcp`
|
||||||
- Default local MCP endpoint: `http://127.0.0.1:8765/`
|
- Default local MCP endpoint: `http://127.0.0.1:8765/`
|
||||||
|
|
||||||
## Version Alignment Rule
|
## Version Alignment Rule
|
||||||
@@ -25,6 +30,8 @@ Keep these versions aligned:
|
|||||||
- GitHub Release `v<version>`
|
- GitHub Release `v<version>`
|
||||||
- `releases/<version>/release-manifest.json` `version`
|
- `releases/<version>/release-manifest.json` `version`
|
||||||
- Release zip filename `Funplay.CocosMcp.v<version>.zip`
|
- Release zip filename `Funplay.CocosMcp.v<version>.zip`
|
||||||
|
- `server.json` top-level version
|
||||||
|
- `server.json` npm package version
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -32,6 +39,8 @@ Example:
|
|||||||
- Git tag: `v0.3.1`
|
- Git tag: `v0.3.1`
|
||||||
- GitHub Release: `v0.3.1`
|
- GitHub Release: `v0.3.1`
|
||||||
- Release asset: `Funplay.CocosMcp.v0.3.1.zip`
|
- Release asset: `Funplay.CocosMcp.v0.3.1.zip`
|
||||||
|
- npm package: `funplay-cocos-mcp@0.3.1`
|
||||||
|
- MCP Registry: `0.3.1`
|
||||||
|
|
||||||
## Files To Update For A New Release
|
## Files To Update For A New Release
|
||||||
|
|
||||||
@@ -41,12 +50,17 @@ Update:
|
|||||||
- `"version": "<version>"`
|
- `"version": "<version>"`
|
||||||
2. `CHANGELOG.md`
|
2. `CHANGELOG.md`
|
||||||
- add a dated release notes block
|
- add a dated release notes block
|
||||||
|
3. `server.json`
|
||||||
|
- top-level `"version"`
|
||||||
|
- npm package `"version"`
|
||||||
|
4. `docs/TOOLS.md`
|
||||||
|
- regenerate with `npm run docs:generate` after tool registry changes
|
||||||
|
|
||||||
Optional but recommended:
|
Optional but recommended:
|
||||||
|
|
||||||
3. `README.md`
|
5. `README.md`
|
||||||
4. `README_CN.md`
|
6. `README_CN.md`
|
||||||
5. GitHub Release notes text
|
7. GitHub Release notes text
|
||||||
|
|
||||||
## Release Steps
|
## Release Steps
|
||||||
|
|
||||||
@@ -78,7 +92,9 @@ This runs:
|
|||||||
|
|
||||||
- JavaScript syntax checks
|
- JavaScript syntax checks
|
||||||
- Node.js tests
|
- Node.js tests
|
||||||
|
- generated tool documentation validation
|
||||||
- release metadata validation
|
- release metadata validation
|
||||||
|
- npm package dry-run validation
|
||||||
- release package generation
|
- release package generation
|
||||||
|
|
||||||
The generated local artifacts are written to:
|
The generated local artifacts are written to:
|
||||||
@@ -122,6 +138,19 @@ cd releases/<version>
|
|||||||
shasum -a 256 -c SHA256SUMS.txt
|
shasum -a 256 -c SHA256SUMS.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 4.5 Validate npm And MCP Registry Metadata
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run pack:dry-run
|
||||||
|
npm run registry:validate
|
||||||
|
```
|
||||||
|
|
||||||
|
The npm package must include the stdio wrapper command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx --yes ./funplay-cocos-mcp-<version>.tgz --version
|
||||||
|
```
|
||||||
|
|
||||||
### 5. Commit, Tag, And Push
|
### 5. Commit, Tag, And Push
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -146,7 +175,7 @@ If creating a new release:
|
|||||||
gh release create v<version> \
|
gh release create v<version> \
|
||||||
-R FunplayAI/funplay-cocos-mcp \
|
-R FunplayAI/funplay-cocos-mcp \
|
||||||
--title "v<version>" \
|
--title "v<version>" \
|
||||||
--notes-file /path/to/release-notes.md \
|
--notes-file releases/<version>/RELEASE_NOTES.md \
|
||||||
releases/<version>/Funplay.CocosMcp.v<version>.zip \
|
releases/<version>/Funplay.CocosMcp.v<version>.zip \
|
||||||
releases/<version>/release-manifest.json \
|
releases/<version>/release-manifest.json \
|
||||||
releases/<version>/SHA256SUMS.txt \
|
releases/<version>/SHA256SUMS.txt \
|
||||||
@@ -165,6 +194,14 @@ gh release upload v<version> \
|
|||||||
releases/<version>/README.md
|
releases/<version>/README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the release body needs to be refreshed without replacing assets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh release edit v<version> \
|
||||||
|
-R FunplayAI/funplay-cocos-mcp \
|
||||||
|
--notes-file releases/<version>/RELEASE_NOTES.md
|
||||||
|
```
|
||||||
|
|
||||||
### 7. Verify GitHub Release
|
### 7. Verify GitHub Release
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -173,9 +210,53 @@ gh release view v<version> \
|
|||||||
--json url,assets,isDraft,isPrerelease,publishedAt
|
--json url,assets,isDraft,isPrerelease,publishedAt
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm the release has all four assets.
|
Confirm the release has all four assets and the public release body is organized by change type.
|
||||||
|
|
||||||
### 8. Post-Release Smoke Test
|
### 8. Publish To npm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm publish
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the published package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm view funplay-cocos-mcp@<version> version bin mcpName
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `package.json` `mcpName` must match `server.json` `name`.
|
||||||
|
- If `npm publish` returns `ENEEDAUTH`, run `npm adduser` with a publishing account and retry.
|
||||||
|
- If the package name already exists under another owner, choose a scoped package name and update both `package.json` and `server.json`.
|
||||||
|
|
||||||
|
### 9. Publish To MCP Registry
|
||||||
|
|
||||||
|
Log in if needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp-publisher login github
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp-publisher publish server.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify latest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.FunplayAI/funplay-cocos-mcp&version=latest"
|
||||||
|
```
|
||||||
|
|
||||||
|
Check a specific version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://registry.modelcontextprotocol.io/v0.1/servers/io.github.FunplayAI%2Ffunplay-cocos-mcp/versions/<version>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Post-Release Smoke Test
|
||||||
|
|
||||||
Test the package from the public GitHub Release:
|
Test the package from the public GitHub Release:
|
||||||
|
|
||||||
@@ -186,12 +267,16 @@ Test the package from the public GitHub Release:
|
|||||||
5. Open `Funplay > MCP Server`.
|
5. Open `Funplay > MCP Server`.
|
||||||
6. Start the MCP server.
|
6. Start the MCP server.
|
||||||
7. Connect an MCP client and call `get_project_info`.
|
7. Connect an MCP client and call `get_project_info`.
|
||||||
|
8. Install the npm wrapper with `npm install -g funplay-cocos-mcp`.
|
||||||
|
9. Connect an MCP client through the `funplay-cocos-mcp` command and call `tools/list`.
|
||||||
|
|
||||||
## Current Verification Commands
|
## Current Verification Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run release:verify
|
npm run release:verify
|
||||||
|
npm run registry:validate
|
||||||
gh release view v<version> -R FunplayAI/funplay-cocos-mcp --json url,assets
|
gh release view v<version> -R FunplayAI/funplay-cocos-mcp --json url,assets
|
||||||
|
npm view funplay-cocos-mcp@<version> version bin mcpName
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Failure Cases
|
## Common Failure Cases
|
||||||
@@ -226,3 +311,30 @@ Fix:
|
|||||||
|
|
||||||
- Rerun `npm run release:check`.
|
- Rerun `npm run release:check`.
|
||||||
- Confirm the command uses `releases/<version>/` and `v<version>`.
|
- Confirm the command uses `releases/<version>/` and `v<version>`.
|
||||||
|
|
||||||
|
### npm `ENEEDAUTH`
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
|
||||||
|
- The local machine is not logged in to npm.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm adduser
|
||||||
|
npm publish
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Registry `Package validation failed`
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
|
||||||
|
- npm package has not been published yet.
|
||||||
|
- `package.json` `mcpName` does not match `server.json` `name`.
|
||||||
|
- `server.json` package version does not match the npm package version.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Publish the npm package first.
|
||||||
|
- Rerun `npm run release:check`.
|
||||||
|
- Rerun `npm run registry:validate`.
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Funplay Cocos MCP — AI Agent Skill
|
||||||
|
|
||||||
|
> Use this skill when operating a Cocos Creator 3.8+ project through the Funplay Cocos MCP server.
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
An MCP server embedded inside the Cocos Creator editor. It exposes tools, resources, and prompts that let you inspect and manipulate a live Cocos project — scene hierarchy, nodes, components, assets, prefabs, scripts, screenshots, runtime state, and more.
|
||||||
|
|
||||||
|
- **Server URL**: `http://127.0.0.1:8765/` (default; may auto-shift if port is taken)
|
||||||
|
- **MCP server name**: `funplay_cocos`
|
||||||
|
- **Tool profiles**: `core` (38 tools, default), `full` (110 tools), `custom`
|
||||||
|
- **Health check**: `GET /health` — verify the server is alive before doing anything
|
||||||
|
|
||||||
|
## Core Philosophy: execute_javascript First
|
||||||
|
|
||||||
|
**`execute_javascript` is the primary tool.** It handles ~80% of all workflows in fewer, higher-leverage calls than chaining many narrow tools.
|
||||||
|
|
||||||
|
**Decision rule**: Start with `execute_javascript`. Only switch to a specialist tool when it is *strictly better* for the task:
|
||||||
|
|
||||||
|
| Task | Best tool | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Scene/node/component inspection & mutation | `execute_javascript` (scene) | One call can inspect, decide, and act |
|
||||||
|
| Editor automation, file ops, asset-db | `execute_javascript` (editor) | Full access to `Editor`, `fs`, `path`, `helpers` |
|
||||||
|
| TypeScript compile errors | `run_script_diagnostics` / `get_script_diagnostic_context` | Parsed diagnostics with source snippets |
|
||||||
|
| Visual verification | `capture_scene_screenshot` / `capture_preview_screenshot` | Image proof of results |
|
||||||
|
| Exact asset discovery | `list_assets` | Structured asset-db query with type filtering |
|
||||||
|
| Opening a specific scene | `open_scene` | Direct scene switching |
|
||||||
|
| Compact editor summary | `get_editor_state` | One-call snapshot of project + selection + windows |
|
||||||
|
| Validate scene health | `validate_scene` | Combined scene + runtime + diagnostics + logs check |
|
||||||
|
|
||||||
|
**Anti-pattern**: Calling `get_hierarchy` → `inspect_node` → `list_components` → `inspect_component` in 4 separate calls when one `execute_javascript` with `context="scene"` can do all of it and mutate in the same call.
|
||||||
|
|
||||||
|
## The Two Execution Contexts
|
||||||
|
|
||||||
|
### context="scene" — Live scene/runtime
|
||||||
|
|
||||||
|
Runs inside the active Cocos scene via the scene bridge.
|
||||||
|
|
||||||
|
**Injected variables:**
|
||||||
|
- `cc` — Cocos engine (use `cc.Sprite`, `cc.Node`, `cc.Vec3`, `cc.UITransform`, etc.)
|
||||||
|
- `scene` — Active scene root node (`cc.Node`, same as `director.getScene()`)
|
||||||
|
- `director` — `cc.director`
|
||||||
|
- `require` — Node.js require (with Cocos module paths)
|
||||||
|
- `Editor` — Editor global (if running in editor)
|
||||||
|
- `args` — User-passed args object
|
||||||
|
|
||||||
|
**Return patterns (any one):**
|
||||||
|
```javascript
|
||||||
|
// 1. Direct return
|
||||||
|
return { sceneName: scene.name, childCount: scene.children.length };
|
||||||
|
|
||||||
|
// 2. run function (async supported)
|
||||||
|
async function run(env) { return result; }
|
||||||
|
|
||||||
|
// 3. module.exports
|
||||||
|
module.exports = async (env) => result;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical rules:**
|
||||||
|
- Always use `cc.Sprite`, `cc.Node` — never bare `Sprite`, `Node`
|
||||||
|
- Do NOT redeclare `scene`, `director`, `cc` — they are already in scope
|
||||||
|
- `assetManager.loadAny` must use callback style — it returns `null` without a callback
|
||||||
|
|
||||||
|
### context="editor" — Editor/browser context
|
||||||
|
|
||||||
|
Runs in the Cocos editor main process with full Node.js access.
|
||||||
|
|
||||||
|
**Injected variables:**
|
||||||
|
- `require`, `Editor`, `args`, `context`, `helpers`, `fs`, `path`, `os`
|
||||||
|
|
||||||
|
**`helpers` object:**
|
||||||
|
- `helpers.getStatus()` — server status
|
||||||
|
- `helpers.listTools()` — all exposed MCP tools
|
||||||
|
- `helpers.readResource(uri)` — read an MCP resource
|
||||||
|
- `helpers.callTool(name, args)` — call another MCP tool
|
||||||
|
- `helpers.configureClient(targetId)` — configure an MCP client
|
||||||
|
|
||||||
|
**Common editor-context patterns:**
|
||||||
|
```javascript
|
||||||
|
// Query asset-db
|
||||||
|
const info = await Editor.Message.request("asset-db", "query-asset-info", "ASSET_UUID");
|
||||||
|
|
||||||
|
// Call a scene method
|
||||||
|
const result = await Editor.Message.request(
|
||||||
|
"scene", "execute-scene-script",
|
||||||
|
{ name: "funplay-cocos-mcp", method: "inspectNode", args: { uuid: "NODE_UUID" } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read project context via helper
|
||||||
|
const ctx = await helpers.readResource("cocos://project/context");
|
||||||
|
|
||||||
|
// Call another tool from editor context
|
||||||
|
const diag = await helpers.callTool("run_script_diagnostics", {});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Checks
|
||||||
|
|
||||||
|
`execute_javascript` has safety checks **enabled by default**. The following patterns are blocked:
|
||||||
|
|
||||||
|
- `fs.rm`, `fs.unlink`, `fs.truncate`, `fs.rmdir` and sync variants — **delete/truncate**
|
||||||
|
- `fs.createWriteStream`, `fs.openSync` — **raw write streams**
|
||||||
|
- `require("child_process")`, `exec`, `spawn`, etc. — **shell execution**
|
||||||
|
- `~`, `$HOME`, `%USERPROFILE%` — **user-home path literals**
|
||||||
|
- `../` — **path traversal**
|
||||||
|
- Absolute paths outside the Cocos project root
|
||||||
|
|
||||||
|
**To bypass** (only after reviewing the risk): pass `"safety_checks": false` in the tool args.
|
||||||
|
|
||||||
|
File tools and `cocos://asset/path/...` resources are sandboxed to the project root.
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### 1. Orient: Understand the current project state
|
||||||
|
|
||||||
|
```json
|
||||||
|
// One call for a compact snapshot
|
||||||
|
{ "context": "editor", "code": "return await helpers.readResource('cocos://project/context');" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the specialist tool:
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
→ `get_editor_state` returns project info, runtime status, selection, scene summary, and windows.
|
||||||
|
|
||||||
|
### 2. Inspect scene hierarchy
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"context": "scene",
|
||||||
|
"code": "function walk(n, d=0) { const r = { name: n.name, active: n.active, components: n.components.map(c=>c.constructor.name) }; if (d < 3 && n.children.length) r.children = n.children.map(c => walk(c, d+1)); return r; } return walk(scene);"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use `get_hierarchy` with `{ "rootPath": "Canvas", "maxDepth": 3, "includeComponents": true }` for a structured snapshot.
|
||||||
|
|
||||||
|
### 3. Create UI (login page example)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"context": "scene",
|
||||||
|
"code": "const { Node, UITransform, Sprite, Label, Button, Color, Vec3, find } = cc; const canvas = find('Canvas'); const bg = new Node('LoginBG'); bg.layer = canvas.layer; bg.addComponent(UITransform).setContentSize(720, 1280); bg.addComponent(Sprite).color = new Color(40, 40, 50, 255); canvas.addChild(bg); const title = new Node('Title'); title.layer = canvas.layer; title.addComponent(UITransform).setContentSize(400, 60); const tLabel = title.addComponent(Label); tLabel.string = 'Login'; tLabel.fontSize = 48; tLabel.color = Color.WHITE; title.setPosition(0, 400, 0); bg.addChild(title); return { created: ['LoginBG', 'Title'], canvas: canvas.name };"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After creation, verify:
|
||||||
|
1. `get_hierarchy` or `execute_javascript` to confirm structure
|
||||||
|
2. `capture_scene_screenshot` for visual proof
|
||||||
|
|
||||||
|
### 4. Set an asset reference (SpriteFrame, Texture, etc.)
|
||||||
|
|
||||||
|
**Cannot use `set_component_property` for asset references.** Use `execute_javascript` with `loadAny`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"context": "scene",
|
||||||
|
"code": "const { find, assetManager, SpriteFrame } = cc; const node = find('Canvas/Player/Sprite'); const sprite = node.getComponent('cc.Sprite'); const sf = await new Promise((resolve, reject) => { assetManager.loadAny({ uuid: 'SPRITEFRAME_UUID', type: SpriteFrame }, (err, asset) => { if (err) reject(err); else resolve(asset); }); }); sprite.spriteFrame = sf; return { node: node.name, spriteFrame: sf.name };"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Fix script errors
|
||||||
|
|
||||||
|
1. Run diagnostics: `run_script_diagnostics` → get error list
|
||||||
|
2. Get context: `get_script_diagnostic_context` → errors with source snippets
|
||||||
|
3. Fix the file: `execute_javascript` with `context="editor"` using `fs`/`path`, or use `replace_in_file`
|
||||||
|
4. Refresh assets: `refresh_assets`
|
||||||
|
5. Re-run diagnostics to confirm clean
|
||||||
|
|
||||||
|
### 6. Prefab workflow
|
||||||
|
|
||||||
|
1. Inspect: `inspect_prefab` with `{ "target": "db://assets/prefabs/Enemy.prefab" }`
|
||||||
|
2. Validate references: `validate_prefab_references`
|
||||||
|
3. Edit JSON directly: `edit_prefab_json` with `jsonPath` + `valueJson` or `search`/`replace`
|
||||||
|
4. Instantiate in scene: `create_prefab_instance` (linked) or `instantiate_prefab` (runtime)
|
||||||
|
5. After scene edits, apply back: `apply_prefab_instance`
|
||||||
|
|
||||||
|
### 7. Visual verification
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Scene panel screenshot
|
||||||
|
{ "fileName": "after-ui-creation" }
|
||||||
|
```
|
||||||
|
→ `capture_scene_screenshot`
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Preview/game screenshot
|
||||||
|
{ "fileName": "gameplay-check" }
|
||||||
|
```
|
||||||
|
→ `capture_preview_screenshot`
|
||||||
|
|
||||||
|
## MCP Resources Quick Reference
|
||||||
|
|
||||||
|
| Resource URI | What it gives you |
|
||||||
|
|---|---|
|
||||||
|
| `cocos://project/context` | Full project + editor context (project path, version, selection, scene) |
|
||||||
|
| `cocos://project/summary` | Project path, folder listing, script/prefab/scene counts |
|
||||||
|
| `cocos://scene/active` | Active scene summary (depth 3) |
|
||||||
|
| `cocos://selection/current` | Current node + asset selection |
|
||||||
|
| `cocos://errors/scripts` | Latest TypeScript diagnostics |
|
||||||
|
| `cocos://logs/editor` | Recent MCP runtime logs + tool interactions |
|
||||||
|
| `cocos://logs/project` | Tails of common project log files |
|
||||||
|
| `cocos://mcp/interactions` | Recent MCP tool call history |
|
||||||
|
| `cocos://mcp/execute-context` | Full execute_javascript variable & pattern guide |
|
||||||
|
|
||||||
|
**Resource templates:**
|
||||||
|
- `cocos://scene/node/{path}` — inspect a node by hierarchy path
|
||||||
|
- `cocos://asset/path/{relative_path}` — read a text/asset file by project-relative path
|
||||||
|
- `cocos://asset/info/{uuid_or_path}` — asset info + metadata by uuid or path
|
||||||
|
|
||||||
|
## MCP Prompts
|
||||||
|
|
||||||
|
Built-in workflow prompts (call via `prompts/get`):
|
||||||
|
|
||||||
|
| Prompt | Use when |
|
||||||
|
|---|---|
|
||||||
|
| `fix_script_errors` | Diagnosing and repairing TypeScript compile errors |
|
||||||
|
| `create_playable_prototype` | Building a gameplay prototype from scratch |
|
||||||
|
| `scene_validation` | Validating scene health, references, and runtime state |
|
||||||
|
| `auto_wire_scene` | Wiring up node references, event bindings, and component connections |
|
||||||
|
|
||||||
|
All prompts reinforce the `execute_javascript`-first philosophy.
|
||||||
|
|
||||||
|
## Key Pitfalls
|
||||||
|
|
||||||
|
1. **`set_component_property` cannot set asset references** (SpriteFrame, Texture, Material, Font, AudioClip). Use `execute_javascript` with `cc.assetManager.loadAny({uuid, type}, cb)`.
|
||||||
|
|
||||||
|
2. **`assetManager.loadAny(uuid)` without callback returns `null`** in scene context. Always use callback style:
|
||||||
|
```javascript
|
||||||
|
assetManager.loadAny({ uuid, type: SpriteFrame }, (err, asset) => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Custom script components require compilation** before `add_component` or `find` can locate them. If a script was just created or modified, wait for Cocos to recompile or trigger a refresh first.
|
||||||
|
|
||||||
|
4. **Node hierarchy paths are slash-separated from scene root**, e.g. `"Canvas/Player/Sprite"`. Use `get_hierarchy` to verify paths before passing them to tools.
|
||||||
|
|
||||||
|
5. **`create_node` accepts `parentPath`, not `parentUuid`**. Resolve the path first with `get_hierarchy` or `find_nodes`.
|
||||||
|
|
||||||
|
6. **Always use `cc.` prefix** in scene context: `cc.Sprite`, `cc.Node`, `cc.UITransform`, `cc.Label`, `cc.Button`, etc. Bare names like `Sprite` or `Node` will fail.
|
||||||
|
|
||||||
|
7. **New nodes need `.layer` set** to match their parent, otherwise they won't render. Set `node.layer = parent.layer` before adding to hierarchy.
|
||||||
|
|
||||||
|
8. **Safety checks block absolute paths outside the project**. If you need to read an external file, use `execute_javascript` with `context="editor"` and pass `safety_checks: false` only after verifying the code is safe.
|
||||||
|
|
||||||
|
## Tool Profile Management
|
||||||
|
|
||||||
|
- **`core`** (default): 38 high-signal tools. Start here.
|
||||||
|
- **`full`**: All 110 tools including UI creation, node editing, components, animation, camera, input simulation, file I/O, runtime control, and more.
|
||||||
|
- **`custom`**: Start from `core`, add categories/tools via `enabledToolCategories` / `enabledTools`, remove via `disabledToolCategories` / `disabledTools`.
|
||||||
|
|
||||||
|
Check current exposure: `get_tool_catalog` → see every tool with profile, category, and enabled state.
|
||||||
|
|
||||||
|
If you need a `full`-only tool (e.g. `create_node`, `add_component`, `write_file`), tell the user to switch to `full` profile in the MCP panel (`Funplay > MCP Server`).
|
||||||
|
|
||||||
|
## Quick Start Checklist for Agents
|
||||||
|
|
||||||
|
1. **Verify connection** — call `get_project_info` or `get_editor_state`
|
||||||
|
2. **Read context** — `cocos://project/context` resource or `get_editor_state`
|
||||||
|
3. **Do the work** — prefer `execute_javascript` (scene for runtime, editor for automation)
|
||||||
|
4. **Verify results** — `validate_scene`, `run_script_diagnostics`, or screenshot tools
|
||||||
|
5. **Report** — summarize what was done, what was verified, and any issues found
|
||||||
Executable
+356
@@ -0,0 +1,356 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DEFAULT_URL = 'http://127.0.0.1:8765/';
|
||||||
|
const DEFAULT_TIMEOUT_SECONDS = 120;
|
||||||
|
const ACCEPT_HEADER = 'application/json, text/event-stream';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
||||||
|
printHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFlag(args, '--version')) {
|
||||||
|
console.error(`funplay-cocos-mcp ${readPackageVersion()}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlText = getOption(args, '--url')
|
||||||
|
|| process.env.FUNPLAY_COCOS_MCP_URL
|
||||||
|
|| DEFAULT_URL;
|
||||||
|
const timeoutText = getOption(args, '--timeout-seconds')
|
||||||
|
|| process.env.FUNPLAY_COCOS_MCP_TIMEOUT_SECONDS
|
||||||
|
|| String(DEFAULT_TIMEOUT_SECONDS);
|
||||||
|
|
||||||
|
const endpoint = parseEndpoint(urlText);
|
||||||
|
if (!endpoint) {
|
||||||
|
console.error(`Invalid --url value: ${urlText}`);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutSeconds = Number.parseInt(timeoutText, 10);
|
||||||
|
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
||||||
|
console.error(`Invalid timeout value: ${timeoutText}`);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[Funplay Cocos MCP] Bridging stdio to ${endpoint.href}`);
|
||||||
|
await bridgeStdioToHttp({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
endpoint,
|
||||||
|
timeoutMs: timeoutSeconds * 1000
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bridgeStdioToHttp({ input, output, endpoint, timeoutMs }) {
|
||||||
|
let sessionId = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const message = await readMessage(input);
|
||||||
|
if (message === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = null;
|
||||||
|
let requestId = null;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(message);
|
||||||
|
requestId = getRequestId(parsed);
|
||||||
|
} catch (error) {
|
||||||
|
await writeJsonRpcError(output, null, -32700, 'Parse error');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await postJsonRpc(endpoint, message, {
|
||||||
|
timeoutMs,
|
||||||
|
sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextSessionId = response.headers['mcp-session-id'];
|
||||||
|
if (typeof nextSessionId === 'string' && nextSessionId) {
|
||||||
|
sessionId = nextSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300 && response.body.trim()) {
|
||||||
|
await writeMessage(output, response.body);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300 && isNotification(parsed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestId !== null) {
|
||||||
|
const messageText = response.body.trim()
|
||||||
|
? `Cocos MCP server returned HTTP ${response.statusCode}: ${response.body}`
|
||||||
|
: `Cocos MCP server returned HTTP ${response.statusCode}.`;
|
||||||
|
await writeJsonRpcError(output, requestId, -32000, messageText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Funplay Cocos MCP] ${error.message}`);
|
||||||
|
if (requestId !== null) {
|
||||||
|
await writeJsonRpcError(output, requestId, -32000, `Proxy transport error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postJsonRpc(endpoint, body, { timeoutMs, sessionId }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = endpoint.protocol === 'https:' ? https : http;
|
||||||
|
const request = client.request(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: ACCEPT_HEADER,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
...(sessionId ? { 'Mcp-Session-Id': sessionId } : {})
|
||||||
|
},
|
||||||
|
timeout: timeoutMs
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
const chunks = [];
|
||||||
|
response.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
response.on('end', () => {
|
||||||
|
resolve({
|
||||||
|
statusCode: response.statusCode || 0,
|
||||||
|
headers: response.headers,
|
||||||
|
body: Buffer.concat(chunks).toString('utf8')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
request.on('timeout', () => {
|
||||||
|
request.destroy(new Error(`HTTP request timed out after ${timeoutMs / 1000} seconds.`));
|
||||||
|
});
|
||||||
|
request.on('error', reject);
|
||||||
|
request.end(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEndpoint(value) {
|
||||||
|
try {
|
||||||
|
const endpoint = new URL(value);
|
||||||
|
if (endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return endpoint;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestId(value) {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Object.prototype.hasOwnProperty.call(value, 'id') ? value.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotification(value) {
|
||||||
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value) && value.method && !Object.prototype.hasOwnProperty.call(value, 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeJsonRpcError(output, id, code, message) {
|
||||||
|
await writeMessage(output, JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id,
|
||||||
|
error: {
|
||||||
|
code,
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readMessage(input) {
|
||||||
|
const headers = new Map();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const line = await readHeaderLine(input);
|
||||||
|
if (line === null) {
|
||||||
|
return headers.size === 0 ? null : Promise.reject(new Error('Unexpected EOF while reading MCP headers.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line === '') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = line.indexOf(':');
|
||||||
|
if (separator <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.set(line.slice(0, separator).trim().toLowerCase(), line.slice(separator + 1).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = Number.parseInt(headers.get('content-length') || '', 10);
|
||||||
|
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
||||||
|
throw new Error('Missing Content-Length header.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await readExact(input, contentLength);
|
||||||
|
return payload.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readHeaderLine(input) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
input.off('readable', onReadable);
|
||||||
|
input.off('end', onEnd);
|
||||||
|
input.off('error', onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReadable() {
|
||||||
|
let byte;
|
||||||
|
while ((byte = input.read(1)) !== null) {
|
||||||
|
if (byte[0] === 0x0a) {
|
||||||
|
cleanup();
|
||||||
|
if (chunks.length > 0 && chunks[chunks.length - 1][0] === 0x0d) {
|
||||||
|
chunks.pop();
|
||||||
|
}
|
||||||
|
resolve(Buffer.concat(chunks).toString('ascii'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chunks.push(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEnd() {
|
||||||
|
cleanup();
|
||||||
|
resolve(chunks.length === 0 ? null : Buffer.concat(chunks).toString('ascii'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
cleanup();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.on('readable', onReadable);
|
||||||
|
input.once('end', onEnd);
|
||||||
|
input.once('error', onError);
|
||||||
|
onReadable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readExact(input, length) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = [];
|
||||||
|
let remaining = length;
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
input.off('readable', onReadable);
|
||||||
|
input.off('end', onEnd);
|
||||||
|
input.off('error', onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReadable() {
|
||||||
|
while (remaining > 0) {
|
||||||
|
const chunk = input.read(remaining);
|
||||||
|
if (chunk === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chunks.push(chunk);
|
||||||
|
remaining -= chunk.length;
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
resolve(Buffer.concat(chunks, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEnd() {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error('Unexpected EOF while reading MCP payload.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
cleanup();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.on('readable', onReadable);
|
||||||
|
input.once('end', onEnd);
|
||||||
|
input.once('error', onError);
|
||||||
|
onReadable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeMessage(output, json) {
|
||||||
|
const payload = Buffer.from(json, 'utf8');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
output.write(`Content-Length: ${payload.length}\r\n\r\n`, 'ascii', (headerError) => {
|
||||||
|
if (headerError) {
|
||||||
|
reject(headerError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
output.write(payload, (payloadError) => {
|
||||||
|
if (payloadError) {
|
||||||
|
reject(payloadError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFlag(args, name) {
|
||||||
|
return args.some((arg) => arg.toLowerCase() === name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOption(args, name) {
|
||||||
|
for (let i = 0; i < args.length - 1; i += 1) {
|
||||||
|
if (args[i].toLowerCase() === name.toLowerCase()) {
|
||||||
|
return args[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPackageVersion() {
|
||||||
|
try {
|
||||||
|
const packagePath = path.resolve(__dirname, '..', 'package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||||
|
return packageJson.version || '0.0.0';
|
||||||
|
} catch (error) {
|
||||||
|
return '0.0.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.error('funplay-cocos-mcp');
|
||||||
|
console.error('Bridges stdio MCP traffic to a local Cocos Creator HTTP MCP server.');
|
||||||
|
console.error();
|
||||||
|
console.error('Options:');
|
||||||
|
console.error(' --url <http://127.0.0.1:8765/> Cocos MCP HTTP endpoint.');
|
||||||
|
console.error(' --timeout-seconds <120> HTTP timeout per request.');
|
||||||
|
console.error(' --version Print the proxy version.');
|
||||||
|
console.error(' --help Show this help.');
|
||||||
|
console.error();
|
||||||
|
console.error('Environment:');
|
||||||
|
console.error(' FUNPLAY_COCOS_MCP_URL');
|
||||||
|
console.error(' FUNPLAY_COCOS_MCP_TIMEOUT_SECONDS');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main().then((code) => {
|
||||||
|
process.exitCode = code;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(`[Funplay Cocos MCP] ${error.message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
+59
-3
@@ -4,8 +4,8 @@ const path = require('path');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const manifest = require('./package.json');
|
const manifest = require('./package.json');
|
||||||
const { configureTarget, getTargetStatuses } = require('./lib/client-config');
|
const { SERVER_NAME, buildTargets, configureTarget, getTargetStatuses } = require('./lib/client-config');
|
||||||
const { loadConfig, getProjectPath, getProjectName, getCocosVersion } = require('./lib/config');
|
const { loadConfig, getProjectPath, getProjectName, getProjectIdentity, getCocosVersion } = require('./lib/config');
|
||||||
const { McpServer } = require('./lib/server');
|
const { McpServer } = require('./lib/server');
|
||||||
const { createToolRegistry } = require('./lib/tool-registry');
|
const { createToolRegistry } = require('./lib/tool-registry');
|
||||||
const { ResourceProvider } = require('./lib/resources');
|
const { ResourceProvider } = require('./lib/resources');
|
||||||
@@ -13,6 +13,7 @@ const { PromptProvider } = require('./lib/prompts');
|
|||||||
const { InteractionLog } = require('./lib/interaction-log');
|
const { InteractionLog } = require('./lib/interaction-log');
|
||||||
const { RuntimeLog } = require('./lib/runtime-log');
|
const { RuntimeLog } = require('./lib/runtime-log');
|
||||||
const { checkForUpdate } = require('./lib/update-checker');
|
const { checkForUpdate } = require('./lib/update-checker');
|
||||||
|
const { normalizeSavedToolProfiles } = require('./lib/tool-profiles');
|
||||||
|
|
||||||
const EXTENSION_NAME = manifest.name || 'funplay-cocos-mcp';
|
const EXTENSION_NAME = manifest.name || 'funplay-cocos-mcp';
|
||||||
const LOG_PREFIX = '[Funplay Cocos MCP]';
|
const LOG_PREFIX = '[Funplay Cocos MCP]';
|
||||||
@@ -101,6 +102,7 @@ class ExtensionService {
|
|||||||
config: this.config,
|
config: this.config,
|
||||||
projectPath: getProjectPath(),
|
projectPath: getProjectPath(),
|
||||||
projectName: getProjectName(),
|
projectName: getProjectName(),
|
||||||
|
projectIdentity: getProjectIdentity(),
|
||||||
cocosVersion: getCocosVersion(),
|
cocosVersion: getCocosVersion(),
|
||||||
packagePath: path.dirname(__filename),
|
packagePath: path.dirname(__filename),
|
||||||
});
|
});
|
||||||
@@ -134,6 +136,8 @@ class ExtensionService {
|
|||||||
promptProvider: this.promptProvider,
|
promptProvider: this.promptProvider,
|
||||||
serverName: `Funplay Cocos MCP - ${getProjectName()}`,
|
serverName: `Funplay Cocos MCP - ${getProjectName()}`,
|
||||||
serverVersion: manifest.version || '0.0.0',
|
serverVersion: manifest.version || '0.0.0',
|
||||||
|
projectName: getProjectName(),
|
||||||
|
projectIdentity: getProjectIdentity(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.server.start();
|
await this.server.start();
|
||||||
@@ -178,8 +182,13 @@ class ExtensionService {
|
|||||||
const fallbackInfo = this.server && this.server.isRunning() && typeof this.server.getPortFallbackInfo === 'function'
|
const fallbackInfo = this.server && this.server.isRunning() && typeof this.server.getPortFallbackInfo === 'function'
|
||||||
? this.server.getPortFallbackInfo()
|
? this.server.getPortFallbackInfo()
|
||||||
: null;
|
: null;
|
||||||
|
const attachInfo = this.server && this.server.isRunning() && typeof this.server.getAttachInfo === 'function'
|
||||||
|
? this.server.getAttachInfo()
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
running: Boolean(this.server && this.server.isRunning()),
|
running: Boolean(this.server && this.server.isRunning()),
|
||||||
|
attachedToExisting: Boolean(attachInfo),
|
||||||
|
attachInfo,
|
||||||
host: this.config.host,
|
host: this.config.host,
|
||||||
port: effective.port,
|
port: effective.port,
|
||||||
requestedPort: this.config.port,
|
requestedPort: this.config.port,
|
||||||
@@ -191,10 +200,14 @@ class ExtensionService {
|
|||||||
enabledToolCategories: this.config.enabledToolCategories,
|
enabledToolCategories: this.config.enabledToolCategories,
|
||||||
disabledToolCategories: this.config.disabledToolCategories,
|
disabledToolCategories: this.config.disabledToolCategories,
|
||||||
enableSessions: this.config.enableSessions,
|
enableSessions: this.config.enableSessions,
|
||||||
|
executeJavascriptSafetyChecks: this.config.executeJavascriptSafetyChecks,
|
||||||
autostart: this.config.autostart,
|
autostart: this.config.autostart,
|
||||||
|
activeToolProfileName: this.config.activeToolProfileName,
|
||||||
|
savedToolProfiles: this.config.savedToolProfiles,
|
||||||
version: manifest.version || '0.0.0',
|
version: manifest.version || '0.0.0',
|
||||||
projectPath: getProjectPath(),
|
projectPath: getProjectPath(),
|
||||||
projectName: getProjectName(),
|
projectName: getProjectName(),
|
||||||
|
projectIdentity: getProjectIdentity(),
|
||||||
cocosVersion: getCocosVersion(),
|
cocosVersion: getCocosVersion(),
|
||||||
url: effective.url,
|
url: effective.url,
|
||||||
};
|
};
|
||||||
@@ -304,7 +317,21 @@ class ExtensionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getClientConfig() {
|
getClientConfig() {
|
||||||
const { url } = this.getEffectiveServerConnection();
|
const effective = this.getEffectiveServerConnection();
|
||||||
|
const { url } = effective;
|
||||||
|
const targetConfig = {
|
||||||
|
...this.config,
|
||||||
|
host: effective.host,
|
||||||
|
port: effective.port,
|
||||||
|
};
|
||||||
|
const targets = buildTargets(targetConfig).map((target) => ({
|
||||||
|
id: target.id,
|
||||||
|
name: target.name,
|
||||||
|
configPath: target.configPath,
|
||||||
|
isToml: Boolean(target.isToml),
|
||||||
|
preview: this.formatClientTargetPreview(target),
|
||||||
|
}));
|
||||||
|
const baseUrl = url.replace(/\/$/, '');
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
codex: `[mcp_servers.funplay_cocos]\nurl = "${url}"\n`,
|
codex: `[mcp_servers.funplay_cocos]\nurl = "${url}"\n`,
|
||||||
@@ -315,9 +342,28 @@ class ExtensionService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, null, 2),
|
}, null, 2),
|
||||||
|
targets,
|
||||||
|
curl: {
|
||||||
|
health: `curl ${baseUrl}/health`,
|
||||||
|
tools: `curl ${baseUrl}/tools`,
|
||||||
|
catalog: `curl ${baseUrl}/tools?catalog=1`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatClientTargetPreview(target) {
|
||||||
|
if (target.isToml) {
|
||||||
|
return `[mcp_servers.${SERVER_NAME}]\nurl = "${target.url}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootKey = target.rootKey || 'mcpServers';
|
||||||
|
return JSON.stringify({
|
||||||
|
[rootKey]: {
|
||||||
|
[SERVER_NAME]: target.entry,
|
||||||
|
},
|
||||||
|
}, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
configureClient(targetId) {
|
configureClient(targetId) {
|
||||||
this.ensureRuntime();
|
this.ensureRuntime();
|
||||||
this.log('info', `Configuring MCP client target: ${targetId}`);
|
this.log('info', `Configuring MCP client target: ${targetId}`);
|
||||||
@@ -383,6 +429,9 @@ class ExtensionService {
|
|||||||
enableSessions: partialConfig && typeof partialConfig.enableSessions === 'boolean'
|
enableSessions: partialConfig && typeof partialConfig.enableSessions === 'boolean'
|
||||||
? partialConfig.enableSessions
|
? partialConfig.enableSessions
|
||||||
: this.config.enableSessions,
|
: this.config.enableSessions,
|
||||||
|
executeJavascriptSafetyChecks: partialConfig && typeof partialConfig.executeJavascriptSafetyChecks === 'boolean'
|
||||||
|
? partialConfig.executeJavascriptSafetyChecks
|
||||||
|
: this.config.executeJavascriptSafetyChecks,
|
||||||
autostart: partialConfig && typeof partialConfig.autostart === 'boolean'
|
autostart: partialConfig && typeof partialConfig.autostart === 'boolean'
|
||||||
? partialConfig.autostart
|
? partialConfig.autostart
|
||||||
: this.config.autostart,
|
: this.config.autostart,
|
||||||
@@ -392,6 +441,12 @@ class ExtensionService {
|
|||||||
lastClientTargetId: partialConfig && partialConfig.lastClientTargetId
|
lastClientTargetId: partialConfig && partialConfig.lastClientTargetId
|
||||||
? String(partialConfig.lastClientTargetId)
|
? String(partialConfig.lastClientTargetId)
|
||||||
: this.config.lastClientTargetId,
|
: this.config.lastClientTargetId,
|
||||||
|
activeToolProfileName: partialConfig && typeof partialConfig.activeToolProfileName === 'string'
|
||||||
|
? String(partialConfig.activeToolProfileName)
|
||||||
|
: this.config.activeToolProfileName,
|
||||||
|
savedToolProfiles: partialConfig && Array.isArray(partialConfig.savedToolProfiles)
|
||||||
|
? normalizeSavedToolProfiles(partialConfig.savedToolProfiles)
|
||||||
|
: this.config.savedToolProfiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
const configPath = this.config.configPath;
|
const configPath = this.config.configPath;
|
||||||
@@ -402,6 +457,7 @@ class ExtensionService {
|
|||||||
nextConfig.port !== this.config.port ||
|
nextConfig.port !== this.config.port ||
|
||||||
nextConfig.toolProfile !== this.config.toolProfile ||
|
nextConfig.toolProfile !== this.config.toolProfile ||
|
||||||
nextConfig.enableSessions !== this.config.enableSessions ||
|
nextConfig.enableSessions !== this.config.enableSessions ||
|
||||||
|
nextConfig.executeJavascriptSafetyChecks !== this.config.executeJavascriptSafetyChecks ||
|
||||||
JSON.stringify(nextConfig.enabledTools) !== JSON.stringify(this.config.enabledTools) ||
|
JSON.stringify(nextConfig.enabledTools) !== JSON.stringify(this.config.enabledTools) ||
|
||||||
JSON.stringify(nextConfig.disabledTools) !== JSON.stringify(this.config.disabledTools) ||
|
JSON.stringify(nextConfig.disabledTools) !== JSON.stringify(this.config.disabledTools) ||
|
||||||
JSON.stringify(nextConfig.enabledToolCategories) !== JSON.stringify(this.config.enabledToolCategories) ||
|
JSON.stringify(nextConfig.enabledToolCategories) !== JSON.stringify(this.config.enabledToolCategories) ||
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
# Feature Absorption Plan: From cocos-mcp-server to funplay-cocos-mcp
|
||||||
|
|
||||||
|
This document identifies features in `cocos-mcp-server` that are missing from `funplay-cocos-mcp`, evaluates which ones are worth porting as dedicated tools, and provides concrete implementation guidance for each.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
Follow the existing funplay tool-design philosophy (`CONTRIBUTING.md`):
|
||||||
|
|
||||||
|
- **`execute_javascript` can already handle it → skip.** No dedicated tool needed.
|
||||||
|
- **High-frequency, complex parameters, or benefits from structured schema → dedicated tool.**
|
||||||
|
- **Multiple narrow tools → consolidate into 1-2 tools with an `action` parameter.**
|
||||||
|
- New tools default to `full` profile; only high-signal tools enter `core`.
|
||||||
|
|
||||||
|
## Priority Tiers
|
||||||
|
|
||||||
|
| Tier | Rationale | Tool count |
|
||||||
|
|---|---|---:|
|
||||||
|
| P1 — Fill real gaps | Capabilities funplay completely lacks and cannot trivially do via `execute_javascript` | 5 new tools |
|
||||||
|
| P2 — Enhance existing | Strengthen tools funplay already has, or add consolidated versions of cocos-mcp-server's fragmented tools | 3 enhancements |
|
||||||
|
| P3 — Optional | Useful but lower frequency; implement on demand | 6 items |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — Fill Real Gaps
|
||||||
|
|
||||||
|
### 1. `create_scene` — Scene creation
|
||||||
|
|
||||||
|
**Gap**: funplay has `open_scene` and `list_scenes` but cannot create a new scene.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/scene-tools.ts:205` — constructs a full `cc.Scene` JSON template (~400 lines) and writes it to disk, then imports via asset-db.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/scene-management.js` (new)
|
||||||
|
- **Dependencies**: `sceneBridge` (add `createScene` method to `scene.js`) + `Editor.Message.request('asset-db', 'create-asset', ...)`
|
||||||
|
- **Schema**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sceneName": { "type": "string", "description": "Name of the new scene" },
|
||||||
|
"savePath": { "type": "string", "description": "Asset path, e.g. db://assets/scenes/NewScene.scene" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Required**: `["sceneName", "savePath"]`
|
||||||
|
- **Profile**: `full`
|
||||||
|
- **Notes**: Do NOT port the 400-line JSON template. Instead, use `Editor.Message.request('scene', 'create-scene', ...)` if available in the target Cocos version, or call `director.runScene(new Scene())` in the scene script then `Editor.Message.request('scene', 'save-scene')`. The hand-written JSON approach in cocos-mcp-server is fragile across Cocos versions.
|
||||||
|
|
||||||
|
### 2. `copy_paste_node` — Node clipboard
|
||||||
|
|
||||||
|
**Gap**: funplay has `create_node`, `delete_node`, `set_node_transform` but no copy/paste/cut.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/scene-advanced-tools.ts:463` — `Editor.Message.request('scene', 'copy-node', uuids)`, `paste-node`, `cut-node`.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/scene-management.js` (same file as above)
|
||||||
|
- **Dependencies**: `Editor.Message.request('scene', ...)` — these are editor-side IPC, no scene script needed.
|
||||||
|
- **Schema** (consolidated with `action`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": { "type": "string", "enum": ["copy", "paste", "cut"] },
|
||||||
|
"uuids": { "type": "array", "items": { "type": "string" }, "description": "Node UUIDs to copy or cut" },
|
||||||
|
"target": { "type": "string", "description": "Target parent node UUID (paste only)" },
|
||||||
|
"keepWorldTransform": { "type": "boolean", "default": false }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Required**: `["action"]`
|
||||||
|
- **Profile**: `full`
|
||||||
|
- **IPC mapping**:
|
||||||
|
- `copy` → `Editor.Message.request('scene', 'copy-node', uuids)`
|
||||||
|
- `paste` → `Editor.Message.request('scene', 'paste-node', { target, uuids, keepWorldTransform })`
|
||||||
|
- `cut` → `Editor.Message.request('scene', 'cut-node', uuids)`
|
||||||
|
|
||||||
|
### 3. `batch_asset_ops` — Batch asset import/delete
|
||||||
|
|
||||||
|
**Gap**: funplay has `delete_asset` (single) but no batch import or batch delete.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/asset-advanced-tools.ts:61` — `batch_import_assets` (source dir → target dir + filter + recursive + overwrite), `batch_delete_assets` (URL array).
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/assets-advanced.js` (existing, append)
|
||||||
|
- **Dependencies**: `Editor.Message.request('asset-db', ...)`
|
||||||
|
- **Schema** (consolidated with `action`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": { "type": "string", "enum": ["import", "delete"] },
|
||||||
|
"sourceDirectory": { "type": "string", "description": "Local filesystem source (import only)" },
|
||||||
|
"targetDirectory": { "type": "string", "description": "Asset-db target URL, e.g. db://assets/textures (import only)" },
|
||||||
|
"urls": { "type": "array", "items": { "type": "string" }, "description": "Asset URLs to delete (delete only)" },
|
||||||
|
"fileFilter": { "type": "array", "items": { "type": "string" }, "description": "File extensions, e.g. [\".png\", \".jpg\"]" },
|
||||||
|
"recursive": { "type": "boolean", "default": false },
|
||||||
|
"overwrite": { "type": "boolean", "default": false }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Required**: `["action"]`
|
||||||
|
- **Profile**: `full`
|
||||||
|
- **IPC mapping**:
|
||||||
|
- `import` → `Editor.Message.request('asset-db', 'import', targetDirectory, sourceDirectory, { ... })`
|
||||||
|
- `delete` → `Editor.Message.request('asset-db', 'delete-assets', urls)`
|
||||||
|
|
||||||
|
### 4. `query_scene_state` — Scene state queries
|
||||||
|
|
||||||
|
**Gap**: funplay cannot check if the scene has unsaved changes, check scene readiness, or soft-reload.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/scene-advanced-tools.ts:283-305` — `query_scene_dirty`, `soft_reload_scene`, `query_scene_ready`.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/scene-management.js`
|
||||||
|
- **Dependencies**: `Editor.Message.request('scene', ...)`
|
||||||
|
- **Schema** (consolidated with `action`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": { "type": "string", "enum": ["is_dirty", "is_ready", "soft_reload"] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Required**: `["action"]`
|
||||||
|
- **Profile**: `core` — `is_dirty` is a critical signal for AI to decide whether to save before making changes.
|
||||||
|
- **IPC mapping**:
|
||||||
|
- `is_dirty` → `Editor.Message.request('scene', 'query-is-dirty')`
|
||||||
|
- `is_ready` → `Editor.Message.request('scene', 'query-ready')`
|
||||||
|
- `soft_reload` → `Editor.Message.request('scene', 'soft-reload')`
|
||||||
|
|
||||||
|
### 5. `find_unused_assets` — Find unused assets
|
||||||
|
|
||||||
|
**Gap**: funplay has `inspect_asset_dependencies` and `validate_asset_dependencies` for individual assets, but no project-wide unused-asset scan.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/asset-advanced-tools.ts:144` — scans a directory, cross-references the dependency graph.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/assets-advanced.js` (append)
|
||||||
|
- **Dependencies**: `Editor.Message.request('asset-db', 'query-assets', ...)` + dependency analysis
|
||||||
|
- **Schema**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"directory": { "type": "string", "default": "db://assets" },
|
||||||
|
"excludeDirectories": { "type": "array", "items": { "type": "string" }, "default": [] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Required**: `[]`
|
||||||
|
- **Profile**: `full`
|
||||||
|
- **Notes**: Implementation requires querying all assets, building a reverse dependency map, then filtering. Medium complexity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — Enhance Existing Capabilities
|
||||||
|
|
||||||
|
### 6. Enhance `inspect_asset_dependencies` — Add reverse direction
|
||||||
|
|
||||||
|
**Gap**: funplay only supports forward dependencies. cocos-mcp-server supports `dependents` (reverse) and `both`.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/asset-advanced-tools.ts:124` — `direction` parameter with enum `["dependents", "dependencies", "both"]`.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/assets-advanced.js` (modify existing tool)
|
||||||
|
- **Change**: Add `direction` parameter to the existing `inspect_asset_dependencies` tool schema.
|
||||||
|
- **IPC**: `Editor.Message.request('asset-db', 'query-dependency', ...)` + reverse traversal for `dependents`.
|
||||||
|
|
||||||
|
### 7. `manage_scene_view` — Scene view / Gizmo control (consolidate 20 → 3)
|
||||||
|
|
||||||
|
**Gap**: funplay has zero scene-view control tools. cocos-mcp-server has 20 separate tools.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/scene-view-tools.ts` — gizmo tool/pivot/coordinate, 2D/3D mode, grid, icon gizmo, focus camera, align camera/view, get/reset status.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/scene-view.js` (new)
|
||||||
|
- **Dependencies**: `Editor.Message.request('scene', ...)`
|
||||||
|
- **Consolidate into 3 tools**:
|
||||||
|
|
||||||
|
**`set_scene_view`**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": { "type": "string", "enum": ["gizmo_tool", "gizmo_pivot", "gizmo_coordinate", "view_mode", "grid", "icon_gizmo_3d", "icon_gizmo_size", "reset"] },
|
||||||
|
"value": { "description": "Value depends on action: gizmo_tool→\"position\"|\"rotation\"|\"scale\"|\"rect\", gizmo_pivot→\"pivot\"|\"center\", gizmo_coordinate→\"local\"|\"global\", view_mode→boolean(is2D), grid→boolean, icon_gizmo_3d→boolean, icon_gizmo_size→number" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`get_scene_view`**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": { "type": "string", "enum": ["gizmo_tool", "gizmo_pivot", "gizmo_coordinate", "view_mode", "grid", "icon_gizmo_3d", "icon_gizmo_size", "full_status"] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`focus_on_nodes`**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuids": { "type": "array", "items": { "type": "string" }, "description": "Node UUIDs to focus on (null/empty for all)" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Profile**: `full`
|
||||||
|
- **IPC mapping** (all `Editor.Message.request('scene', ...)`):
|
||||||
|
- `change-gizmo-tool`, `change-gizmo-pivot`, `change-gizmo-coordinate`
|
||||||
|
- `change-view-mode`, `set-grid-visible`, `set-icon-gizmo-3d`, `set-icon-gizmo-size`
|
||||||
|
- `focus-camera-on-nodes`, `align-camera-with-view`, `align-view-with-node`
|
||||||
|
- `query-gizmo-tool-name`, `query-gizmo-pivot`, `query-gizmo-coordinate`, etc.
|
||||||
|
|
||||||
|
### 8. `manage_reference_image` — Reference images (consolidate 12 → 2)
|
||||||
|
|
||||||
|
**Gap**: funplay has zero reference-image tools. cocos-mcp-server has 12.
|
||||||
|
|
||||||
|
**cocos-mcp-server source**: `source/tools/reference-image-tools.ts` — add/remove/switch/clear/refresh, set position/scale/opacity/data, query config/current, list.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- **File**: `lib/tools/reference-image.js` (new)
|
||||||
|
- **Dependencies**: `Editor.Message.request('reference-image', ...)`
|
||||||
|
- **Consolidate into 2 tools**:
|
||||||
|
|
||||||
|
**`manage_reference_image`**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": { "type": "string", "enum": ["add", "remove", "switch", "clear_all", "refresh", "list", "query_current", "query_config"] },
|
||||||
|
"paths": { "type": "array", "items": { "type": "string" }, "description": "Image paths (add/remove)" },
|
||||||
|
"path": { "type": "string", "description": "Single image path (switch)" },
|
||||||
|
"sceneUUID": { "type": "string", "description": "Optional scene UUID (switch)" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`set_reference_image_property`**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": { "type": "string", "enum": ["position", "scale", "opacity", "data"] },
|
||||||
|
"x": { "type": "number" }, "y": { "type": "number" },
|
||||||
|
"sx": { "type": "number" }, "sy": { "type": "number" },
|
||||||
|
"opacity": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||||
|
"key": { "type": "string", "enum": ["path", "x", "y", "sx", "sy", "opacity"] },
|
||||||
|
"value": { "description": "Value for the given key (data action)" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Profile**: `full`
|
||||||
|
- **IPC mapping**: `Editor.Message.request('reference-image', 'add-image'|'remove-image'|'switch-image'|'set-image-data'|'query-config'|'query-current-image'|'refresh'|'clear-all', ...)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — Optional Enhancements
|
||||||
|
|
||||||
|
Implement on demand. All are `full` profile.
|
||||||
|
|
||||||
|
| # | Tool name | Source | Consolidation | Key IPC |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 9 | `manage_undo` | `scene-advanced-tools.ts:241` | `begin`/`end`/`cancel` → 1 tool + action | `scene: begin-undo-recording` etc. |
|
||||||
|
| 10 | `manage_asset_crud` | `project-tools.ts:178` | `create`/`copy`/`move`/`save`/`reimport` → 1 tool + action | `asset-db: create-asset` etc. |
|
||||||
|
| 11 | `manage_preview_server` | `project-tools.ts:156` | `start`/`stop` → 1 tool + action | `preview-server: start`/`stop` |
|
||||||
|
| 12 | `get_project_settings` | `project-tools.ts:52` | Single tool, category param | `project: query-settings` |
|
||||||
|
| 13 | `validate_asset_references` | `asset-advanced-tools.ts:110` | Directory-wide scan | `asset-db: query-assets` + analysis |
|
||||||
|
| 14 | `build_project` | `project-tools.ts:24` | Platform enum param | `builder: build` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Not Recommended for Absorption
|
||||||
|
|
||||||
|
These cocos-mcp-server features are adequately covered by `execute_javascript` and do not warrant dedicated tools:
|
||||||
|
|
||||||
|
| Feature | Reason to skip |
|
||||||
|
|---|---|
|
||||||
|
| Broadcast message listening (`listen_broadcast`, `stop_listening`) | Low frequency; `execute_javascript` with `Editor.Message.broadcast` is sufficient |
|
||||||
|
| Preferences panel opening (`open_preferences_settings`) | One-liner: `Editor.Panel.open('preferences')` |
|
||||||
|
| Console log capture (`get_console_logs`, `clear_console`) | funplay's `get_recent_logs` + `clear_logs` already cover this |
|
||||||
|
| Texture compression (`compress_textures`) | Very low frequency; better as a project build setting |
|
||||||
|
| Asset manifest export (`export_asset_manifest`) | Low frequency; `execute_javascript` can iterate asset-db |
|
||||||
|
| Array element manipulation (`move_array_element`, `remove_array_element`) | `execute_javascript` in scene context is more flexible |
|
||||||
|
| Node property reset (`reset_node_property`) | `execute_javascript` one-liner via `Editor.Message.request('scene', 'reset-property', ...)` |
|
||||||
|
| Scene snapshot (`scene_snapshot`) | Advanced internal feature; AI rarely needs it directly |
|
||||||
|
| `validate_json_params` / `safe_string_value` / `format_mcp_request` | Workarounds for cocos-mcp-server's own JSON parsing bugs; funplay's server handles JSON correctly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Step 1: Create `lib/tools/scene-management.js`
|
||||||
|
|
||||||
|
New file with `createSceneManagementTools({ createSchema, sceneBridge })` exporting:
|
||||||
|
- `create_scene`
|
||||||
|
- `copy_paste_node`
|
||||||
|
- `query_scene_state`
|
||||||
|
|
||||||
|
### Step 2: Extend `lib/tools/assets-advanced.js`
|
||||||
|
|
||||||
|
Append to existing `createAssetsAdvancedTools`:
|
||||||
|
- `batch_asset_ops`
|
||||||
|
- `find_unused_assets`
|
||||||
|
- Add `direction` parameter to existing `inspect_asset_dependencies`
|
||||||
|
|
||||||
|
### Step 3: Create `lib/tools/scene-view.js`
|
||||||
|
|
||||||
|
New file with `createSceneViewTools({ createSchema })` exporting:
|
||||||
|
- `set_scene_view`
|
||||||
|
- `get_scene_view`
|
||||||
|
- `focus_on_nodes`
|
||||||
|
|
||||||
|
### Step 4: Create `lib/tools/reference-image.js`
|
||||||
|
|
||||||
|
New file with `createReferenceImageTools({ createSchema })` exporting:
|
||||||
|
- `manage_reference_image`
|
||||||
|
- `set_reference_image_property`
|
||||||
|
|
||||||
|
### Step 5: Register in `lib/tool-registry.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { createSceneManagementTools } = require('./tools/scene-management');
|
||||||
|
const { createSceneViewTools } = require('./tools/scene-view');
|
||||||
|
const { createReferenceImageTools } = require('./tools/reference-image');
|
||||||
|
|
||||||
|
// Inside createToolRegistry, in the tools array:
|
||||||
|
...createSceneManagementTools({ createSchema, sceneBridge }),
|
||||||
|
...createSceneViewTools({ createSchema }),
|
||||||
|
...createReferenceImageTools({ createSchema }),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Add scene-side methods to `scene.js`
|
||||||
|
|
||||||
|
Add `createScene` method to the scene script's `methods` export for scene creation support.
|
||||||
|
|
||||||
|
### Step 7: Run verification chain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check # syntax check all files
|
||||||
|
npm test # run unit tests
|
||||||
|
npm run docs:generate # regenerate docs/TOOLS.md
|
||||||
|
npm run docs:check # verify docs in sync
|
||||||
|
npm run release:check # release metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Update documentation
|
||||||
|
|
||||||
|
- `README.md` — add new tool categories to the Built-in Tools table
|
||||||
|
- `README_CN.md` — mirror changes
|
||||||
|
- `CHANGELOG.md` — add entry under next version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
| Metric | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| `full` profile tools | 101 | ~115 (net +14 after consolidation) |
|
||||||
|
| `core` profile tools | 37 | 38 (+`query_scene_state`) |
|
||||||
|
| Blank areas | Scene creation, view control, reference images, clipboard, batch assets | All filled |
|
||||||
|
| New files | — | `lib/tools/scene-management.js`, `lib/tools/scene-view.js`, `lib/tools/reference-image.js` |
|
||||||
|
| Modified files | — | `lib/tools/assets-advanced.js`, `lib/tool-registry.js`, `scene.js` |
|
||||||
@@ -0,0 +1,583 @@
|
|||||||
|
# Implementation Plan: High-Frequency Editor Operations
|
||||||
|
|
||||||
|
> **Goal**: Fill the 9 highest-impact tool gaps identified in the Cocos Creator business-development workflow analysis.
|
||||||
|
>
|
||||||
|
> **Current state**: 101 tools (core=37, full=101), version 0.4.0
|
||||||
|
> **Target state**: 110 tools (core=38, full=110)
|
||||||
|
|
||||||
|
## Tool Summary
|
||||||
|
|
||||||
|
| # | Tool | Profile | Side | Phase | New file? |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1 | `create_scene` | `full` | editor | 1 | `lib/tools/scene-management.js` |
|
||||||
|
| 2 | `query_scene_state` | `core` | editor | 1 | same |
|
||||||
|
| 3 | `copy_paste_node` | `full` | editor | 1 | same |
|
||||||
|
| 4 | `rename_node` | `full` | scene | 2 | scene.js + scene-management.js |
|
||||||
|
| 5 | `reparent_node` | `full` | scene | 2 | same |
|
||||||
|
| 6 | `create_prefab` | `full` | editor | 3 | prefabs.js + tool-registry.js |
|
||||||
|
| 7 | `create_script` | `full` | editor | 4 | `lib/tools/scripts.js` |
|
||||||
|
| 8 | `batch_asset_ops` | `full` | editor | 5 | assets-advanced.js (append) |
|
||||||
|
| 9 | `find_unused_assets` | `full` | editor | 5 | same |
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Action | Phase |
|
||||||
|
|---|---|---|
|
||||||
|
| `lib/tools/scene-management.js` | **New** — factory for tools 1-5 | 1, 2 |
|
||||||
|
| `scene.js` | **Modify** — add `renameNode`, `reparentNode` methods | 2 |
|
||||||
|
| `lib/prefabs.js` | **Modify** — add `createPrefab` helper | 3 |
|
||||||
|
| `lib/tool-registry.js` | **Modify** — import + spread new factories, add `create_prefab` inline | 1-5 |
|
||||||
|
| `lib/tools/scripts.js` | **New** — factory for tool 7 | 4 |
|
||||||
|
| `lib/tools/assets-advanced.js` | **Modify** — append tools 8-9 | 5 |
|
||||||
|
| `docs/TOOLS.md` | **Regenerate** via `npm run docs:generate` | final |
|
||||||
|
| `README.md` / `README_CN.md` | **Modify** — add new tool categories | final |
|
||||||
|
| `CHANGELOG.md` | **Modify** — add `## [0.5.0]` entry | final |
|
||||||
|
| `test/*.test.js` | **New** — unit tests for pure-logic helpers | each phase |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Scene Management Tools
|
||||||
|
|
||||||
|
**File**: `lib/tools/scene-management.js` (new)
|
||||||
|
|
||||||
|
**Dependencies**: `{ createSchema, getRuntimeContext }` — uses `Editor.Message.request` directly (editor-side), no scene bridge needed for tools 1-3.
|
||||||
|
|
||||||
|
**Imports**: Reuse `tryEditorRequests` from `./cocos-project.js`.
|
||||||
|
|
||||||
|
### Tool 1: `create_scene`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'create_scene',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a new scene asset in the Cocos project and open it.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
sceneName: { type: 'string', description: 'Name of the new scene (without extension).' },
|
||||||
|
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' },
|
||||||
|
open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' },
|
||||||
|
},
|
||||||
|
['sceneName', 'savePath']
|
||||||
|
),
|
||||||
|
handler: async (args) => { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation approach**:
|
||||||
|
|
||||||
|
Use `Editor.Message.request('asset-db', 'create-asset', savePath, '')` to create an empty `.scene` file. Cocos Creator's asset-db will generate a valid scene template automatically when the file extension is `.scene`. Then optionally `openAsset(savePath)`.
|
||||||
|
|
||||||
|
Fallback: if `create-asset` doesn't auto-generate scene content, write a minimal scene JSON template to disk and let asset-db import it.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Pseudocode
|
||||||
|
const result = await Editor.Message.request('asset-db', 'create-asset', args.savePath, '');
|
||||||
|
// result contains { uuid, url, ... }
|
||||||
|
if (args.open !== false) {
|
||||||
|
await openAsset(result.url || args.savePath);
|
||||||
|
}
|
||||||
|
return { created: true, sceneName: args.sceneName, url: result.url, uuid: result.uuid };
|
||||||
|
```
|
||||||
|
|
||||||
|
**IPC mapping**:
|
||||||
|
- `asset-db: create-asset` (primary)
|
||||||
|
- `asset-db: import-asset` (fallback if create-asset doesn't work for scenes)
|
||||||
|
|
||||||
|
**Error handling**: Throw if `savePath` doesn't end with `.scene`. Throw if asset already exists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tool 2: `query_scene_state`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'query_scene_state',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
action: { type: 'string', enum: ['is_dirty', 'is_ready', 'soft_reload'], description: 'State query or action.' },
|
||||||
|
},
|
||||||
|
['action']
|
||||||
|
),
|
||||||
|
handler: async (args) => { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why `core`**: `is_dirty` is a critical signal for AI to decide whether to save before making changes. This is the only new tool promoted to `core`.
|
||||||
|
|
||||||
|
**IPC mapping**:
|
||||||
|
- `is_dirty` → `Editor.Message.request('scene', 'query-is-dirty')` → returns boolean
|
||||||
|
- `is_ready` → `Editor.Message.request('scene', 'query-ready')` → returns boolean
|
||||||
|
- `soft_reload` → `Editor.Message.request('scene', 'soft-reload')` → reloads scene from disk
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```js
|
||||||
|
const IPC_MAP = {
|
||||||
|
is_dirty: { channel: 'scene', method: 'query-is-dirty' },
|
||||||
|
is_ready: { channel: 'scene', method: 'query-ready' },
|
||||||
|
soft_reload: { channel: 'scene', method: 'soft-reload' },
|
||||||
|
};
|
||||||
|
const candidate = IPC_MAP[args.action];
|
||||||
|
const result = await tryEditorRequests([candidate]);
|
||||||
|
return { action: args.action, result: result.result };
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tool 3: `copy_paste_node`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'copy_paste_node',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
action: { type: 'string', enum: ['copy', 'cut', 'paste'], description: 'Clipboard action.' },
|
||||||
|
uuids: { type: 'array', items: { type: 'string' }, description: 'Node UUIDs to copy or cut.' },
|
||||||
|
target: { type: 'string', description: 'Target parent node UUID for paste.' },
|
||||||
|
keepWorldTransform: { type: 'boolean', description: 'Keep world transform on paste. Default false.' },
|
||||||
|
},
|
||||||
|
['action']
|
||||||
|
),
|
||||||
|
handler: async (args) => { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IPC mapping**:
|
||||||
|
- `copy` → `Editor.Message.request('scene', 'copy-node', args.uuids)`
|
||||||
|
- `cut` → `Editor.Message.request('scene', 'cut-node', args.uuids)`
|
||||||
|
- `paste` → `Editor.Message.request('scene', 'paste-node', { target: args.target, keepWorldTransform: args.keepWorldTransform })`
|
||||||
|
|
||||||
|
**Validation**:
|
||||||
|
- `copy`/`cut` requires `uuids` non-empty
|
||||||
|
- `paste` requires `target`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Node Operations
|
||||||
|
|
||||||
|
**Files**: `scene.js` (add methods) + `lib/tools/scene-management.js` (add tool definitions)
|
||||||
|
|
||||||
|
### Tool 4: `rename_node` (scene-side)
|
||||||
|
|
||||||
|
**scene.js method** — add to `exports.methods`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async renameNode(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Target node was not found.');
|
||||||
|
}
|
||||||
|
const oldName = node.name;
|
||||||
|
const oldPath = getNodePath(node);
|
||||||
|
const newName = String(options.newName || '').trim();
|
||||||
|
if (!newName) {
|
||||||
|
throw new Error('newName is required.');
|
||||||
|
}
|
||||||
|
node.name = newName;
|
||||||
|
return {
|
||||||
|
renamed: true,
|
||||||
|
oldName,
|
||||||
|
newName,
|
||||||
|
oldPath,
|
||||||
|
newPath: getNodePath(node),
|
||||||
|
uuid: node.uuid,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool definition** (in `scene-management.js`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'rename_node',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Rename a scene node.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Node UUID.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
|
newName: { type: 'string', description: 'New name for the node.' },
|
||||||
|
},
|
||||||
|
['newName']
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('renameNode', args),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dependencies**: `{ createSchema, sceneBridge }` — add `sceneBridge` to the factory's dependency bag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tool 5: `reparent_node` (scene-side)
|
||||||
|
|
||||||
|
**scene.js method** — add to `exports.methods`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async reparentNode(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Target node was not found.');
|
||||||
|
}
|
||||||
|
const oldPath = getNodePath(node);
|
||||||
|
const oldParent = node.parent;
|
||||||
|
|
||||||
|
let newParent;
|
||||||
|
if (options.targetUuid) {
|
||||||
|
newParent = findNodeByUuid(options.targetUuid);
|
||||||
|
} else if (options.targetPath) {
|
||||||
|
newParent = findNodeByPath(options.targetPath);
|
||||||
|
} else if (options.targetName) {
|
||||||
|
newParent = findNodeByName(options.targetName);
|
||||||
|
}
|
||||||
|
if (!newParent) {
|
||||||
|
throw new Error('Target parent node was not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const siblingIndex = Number.isFinite(options.siblingIndex) ? options.siblingIndex : -1;
|
||||||
|
if (siblingIndex >= 0 && siblingIndex <= newParent.children.length) {
|
||||||
|
newParent.insertChild(node, siblingIndex);
|
||||||
|
} else {
|
||||||
|
node.parent = newParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reparented: true,
|
||||||
|
uuid: node.uuid,
|
||||||
|
oldPath,
|
||||||
|
newPath: getNodePath(node),
|
||||||
|
oldParent: oldParent ? oldParent.name : null,
|
||||||
|
newParent: newParent.name,
|
||||||
|
siblingIndex: node.getSiblingIndex(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool definition** (in `scene-management.js`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'reparent_node',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Move a node to a new parent in the scene hierarchy.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Node hierarchy path to move.' },
|
||||||
|
uuid: { type: 'string', description: 'Node UUID to move.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
|
targetPath: { type: 'string', description: 'New parent node path.' },
|
||||||
|
targetUuid: { type: 'string', description: 'New parent node UUID.' },
|
||||||
|
targetName: { type: 'string', description: 'Fallback exact new parent name.' },
|
||||||
|
siblingIndex: { type: 'number', description: 'Optional insert position among siblings. Default: append.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('reparentNode', args),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Prefab Creation
|
||||||
|
|
||||||
|
**Files**: `lib/prefabs.js` (add helper) + `lib/tool-registry.js` (add inline tool)
|
||||||
|
|
||||||
|
### Tool 6: `create_prefab`
|
||||||
|
|
||||||
|
**lib/prefabs.js** — add `createPrefab` helper:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function createPrefab(options = {}) {
|
||||||
|
const nodeUuid = String(options.nodeUuid || '').trim();
|
||||||
|
const savePath = String(options.savePath || '').trim();
|
||||||
|
if (!nodeUuid) throw new Error('nodeUuid is required.');
|
||||||
|
if (!savePath) throw new Error('savePath is required.');
|
||||||
|
|
||||||
|
const result = await Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath);
|
||||||
|
return {
|
||||||
|
created: true,
|
||||||
|
nodeUuid,
|
||||||
|
savePath,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool definition** (inline in `tool-registry.js`, near existing prefab tools ~line 760):
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'create_prefab',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a prefab asset from a scene node.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
nodeUuid: { type: 'string', description: 'UUID of the scene node to save as prefab.' },
|
||||||
|
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/prefabs/Enemy.prefab.' },
|
||||||
|
},
|
||||||
|
['nodeUuid', 'savePath']
|
||||||
|
),
|
||||||
|
handler: async (args) => createPrefab(args),
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**IPC mapping**: `Editor.Message.request('scene', 'create-prefab', nodeUuid, savePath)`
|
||||||
|
|
||||||
|
**Notes**: The `create-prefab` scene message is well-documented in Cocos Creator 3.8+. It takes a node UUID and a target `.prefab` path, creates the prefab asset, and links the scene node as a prefab instance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Script Creation
|
||||||
|
|
||||||
|
**File**: `lib/tools/scripts.js` (new)
|
||||||
|
|
||||||
|
**Dependencies**: `{ createSchema, getRuntimeContext }` — uses `fs` for file writing + `Editor.Message.request('asset-db', 'refresh-asset')` for refresh.
|
||||||
|
|
||||||
|
### Tool 7: `create_script`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'create_script',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a new TypeScript component script with a standard Cocos template.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
scriptName: { type: 'string', description: 'Class name in PascalCase, e.g. PlayerController.' },
|
||||||
|
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scripts/PlayerController.ts.' },
|
||||||
|
template: { type: 'string', enum: ['component', 'plain'], description: 'Script template. Default: component.' },
|
||||||
|
properties: { type: 'array', items: { type: 'object' }, description: 'Optional @property declarations.' },
|
||||||
|
},
|
||||||
|
['scriptName', 'savePath']
|
||||||
|
),
|
||||||
|
handler: async (args) => { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Template generation** (pure logic, unit-testable):
|
||||||
|
|
||||||
|
```js
|
||||||
|
function generateComponentTemplate(className, properties = []) {
|
||||||
|
const props = properties.map(p => {
|
||||||
|
const type = p.type || 'number';
|
||||||
|
const name = p.name || 'property';
|
||||||
|
return ` @property({ type: ${type} })\n ${name}: ${type} = ${p.default || '0'};`;
|
||||||
|
}).join('\n\n');
|
||||||
|
|
||||||
|
return `import { _decorator, Component, ${properties.map(p => p.type).filter(t => t && t !== 'number' && t !== 'string' && t !== 'boolean').join(', ')} } from 'cc';
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('${className}')
|
||||||
|
export class ${className} extends Component {
|
||||||
|
${props || ' // Add properties here'}
|
||||||
|
}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
1. Generate template string from `scriptName` + `template` + `properties`
|
||||||
|
2. Write to project filesystem via `resolveProjectPath` + `fs.writeFileSync`
|
||||||
|
3. Refresh asset-db: `Editor.Message.request('asset-db', 'refresh-asset', savePath)`
|
||||||
|
4. Return `{ created: true, scriptName, savePath, className }`
|
||||||
|
|
||||||
|
**Unit tests** (test/scripts.test.js):
|
||||||
|
- `generateComponentTemplate('PlayerController')` → contains `@ccclass`, `export class PlayerController extends Component`
|
||||||
|
- `generateComponentTemplate('Enemy', [{name:'speed',type:'number',default:'5'}])` → contains `@property({ type: number })` and `speed: number = 5`
|
||||||
|
- `generatePlainTemplate('Utils')` → no `@ccclass`, just `export class Utils`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Asset Batch Operations
|
||||||
|
|
||||||
|
**File**: `lib/tools/assets-advanced.js` (append to existing)
|
||||||
|
|
||||||
|
**Dependencies**: `{ createSchema, getRuntimeContext }` — uses `Editor.Message.request('asset-db', ...)` and `listAssets`.
|
||||||
|
|
||||||
|
### Tool 8: `batch_asset_ops`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'batch_asset_ops',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Batch import or delete assets in the Cocos asset database.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
action: { type: 'string', enum: ['import', 'delete'], description: 'Batch operation.' },
|
||||||
|
sourceDirectory: { type: 'string', description: 'Local filesystem source (import only).' },
|
||||||
|
targetDirectory: { type: 'string', description: 'Asset-db target URL, e.g. db://assets/textures (import only).' },
|
||||||
|
urls: { type: 'array', items: { type: 'string' }, description: 'Asset URLs to delete (delete only).' },
|
||||||
|
fileFilter: { type: 'array', items: { type: 'string' }, description: 'File extensions, e.g. [".png", ".jpg"].' },
|
||||||
|
recursive: { type: 'boolean', description: 'Import recursively. Default false.' },
|
||||||
|
overwrite: { type: 'boolean', description: 'Overwrite existing assets. Default false.' },
|
||||||
|
},
|
||||||
|
['action']
|
||||||
|
),
|
||||||
|
handler: async (args) => { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IPC mapping**:
|
||||||
|
- `import` → `Editor.Message.request('asset-db', 'import', targetDirectory, sourceDirectory)`
|
||||||
|
- `delete` → `Editor.Message.request('asset-db', 'delete-assets', urls)`
|
||||||
|
|
||||||
|
**Validation**:
|
||||||
|
- `import` requires `sourceDirectory` + `targetDirectory`
|
||||||
|
- `delete` requires `urls` non-empty
|
||||||
|
|
||||||
|
### Tool 9: `find_unused_assets`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'find_unused_assets',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Find assets not referenced by any scene, prefab, or script in the project.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
directory: { type: 'string', description: 'Asset-db directory to scan. Default: db://assets.' },
|
||||||
|
excludeDirectories: { type: 'array', items: { type: 'string' }, description: 'Directories to exclude.' },
|
||||||
|
assetTypes: { type: 'array', items: { type: 'string' }, description: 'Filter by asset type, e.g. ["cc.Texture2D", "cc.AudioClip"].' },
|
||||||
|
limit: { type: 'number', description: 'Max results. Default 200.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation** (pure-logic core, unit-testable):
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function findUnusedAssets(projectPath, options = {}) {
|
||||||
|
// 1. Query all assets in the directory
|
||||||
|
const allAssets = await listAssets({
|
||||||
|
pattern: options.directory || 'db://assets/**',
|
||||||
|
ccType: options.assetTypes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Build a set of all referenced UUIDs by scanning:
|
||||||
|
// - All .scene files
|
||||||
|
// - All .prefab files
|
||||||
|
// - All .ts/.js files (for dynamic asset references)
|
||||||
|
// - All .anim files
|
||||||
|
// - All .meta files (for sub-asset references)
|
||||||
|
const referencedUuids = new Set();
|
||||||
|
// ... scan files, collect UUIDs using collectUuidReferences from assets-advanced.js
|
||||||
|
|
||||||
|
// 3. Filter: assets whose UUID is NOT in referencedUuids
|
||||||
|
const unused = allAssets.filter(asset => !referencedUuids.has(asset.uuid));
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalScanned: allAssets.length,
|
||||||
|
unusedCount: unused.length,
|
||||||
|
unused: unused.slice(0, options.limit || 200),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unit tests** (test/find-unused-assets.test.js):
|
||||||
|
- Mock `listAssets` + file scanning → verify correct filtering
|
||||||
|
- Test exclude directories
|
||||||
|
- Test asset type filtering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
### `lib/tool-registry.js` changes
|
||||||
|
|
||||||
|
**1. Add imports** (top of file):
|
||||||
|
```js
|
||||||
|
const { createSceneManagementTools } = require('./tools/scene-management');
|
||||||
|
const { createScriptTools } = require('./tools/scripts');
|
||||||
|
const { createPrefab } = require('./prefabs'); // add to existing destructure
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Spread factories** into the `tools` array:
|
||||||
|
```js
|
||||||
|
// After existing cocos-project tools (~line 717)
|
||||||
|
...createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }),
|
||||||
|
|
||||||
|
// After existing prefab tools (~line 780)
|
||||||
|
{
|
||||||
|
name: 'create_prefab',
|
||||||
|
// ... inline definition
|
||||||
|
handler: async (args) => createPrefab(args),
|
||||||
|
},
|
||||||
|
|
||||||
|
// After existing file tools (~line 1315)
|
||||||
|
...createScriptTools({ createSchema, getRuntimeContext }),
|
||||||
|
|
||||||
|
// After existing assets-advanced tools (~line 1023)
|
||||||
|
// batch_asset_ops and find_unused_assets are appended inside
|
||||||
|
// createAssetsAdvancedTools via the existing factory
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Pass `sceneBridge` to `createSceneManagementTools`** — currently only `createSceneEventTools` receives `sceneBridge`. Add it to the scene-management factory call.
|
||||||
|
|
||||||
|
### `lib/tools/assets-advanced.js` changes
|
||||||
|
|
||||||
|
Append `batch_asset_ops` and `find_unused_assets` tool definitions to the `createAssetsAdvancedTools` return array. Add `getRuntimeContext` to the factory's dependency bag (already received).
|
||||||
|
|
||||||
|
### `lib/tools/scene-management.js` factory signature
|
||||||
|
|
||||||
|
```js
|
||||||
|
function createSceneManagementTools({ createSchema, sceneBridge, getRuntimeContext }) {
|
||||||
|
return [
|
||||||
|
// Tool 1: create_scene (editor-side, uses getRuntimeContext for projectPath)
|
||||||
|
// Tool 2: query_scene_state (editor-side, uses tryEditorRequests)
|
||||||
|
// Tool 3: copy_paste_node (editor-side, uses tryEditorRequests)
|
||||||
|
// Tool 4: rename_node (scene-side, uses sceneBridge.call)
|
||||||
|
// Tool 5: reparent_node (scene-side, uses sceneBridge.call)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
module.exports = { createSceneManagementTools };
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
Execute phases sequentially. Each phase ends with `npm run check && npm test` before proceeding.
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1 (tools 1-3) ──▶ npm run check && npm test
|
||||||
|
│
|
||||||
|
Phase 2 (tools 4-5) ──▶ npm run check && npm test
|
||||||
|
│
|
||||||
|
Phase 3 (tool 6) ──▶ npm run check && npm test
|
||||||
|
│
|
||||||
|
Phase 4 (tool 7) ──▶ npm run check && npm test
|
||||||
|
│
|
||||||
|
Phase 5 (tools 8-9) ──▶ npm run check && npm test
|
||||||
|
│
|
||||||
|
Final: docs:generate ──▶ docs:check ──▶ release:check ──▶ pack:dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist (per phase)
|
||||||
|
|
||||||
|
- [ ] `npm run check` — syntax check all files
|
||||||
|
- [ ] `npm test` — unit tests pass
|
||||||
|
- [ ] `npm run docs:generate` — regenerate TOOLS.md (final phase only)
|
||||||
|
- [ ] `npm run docs:check` — docs in sync (final phase only)
|
||||||
|
- [ ] `npm run release:check` — release metadata valid (final phase only)
|
||||||
|
- [ ] `npm run pack:dry-run` — package builds (final phase only)
|
||||||
|
|
||||||
|
## Version Bump
|
||||||
|
|
||||||
|
- `package.json` version: `0.4.0` → `0.5.0`
|
||||||
|
- `server.json` version(s): update to match
|
||||||
|
- `CHANGELOG.md`: add `## [0.5.0] - 2026-06-30` entry
|
||||||
|
- Git tag: `v0.5.0`
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
| Metric | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| `full` profile tools | 101 | 110 (+9) |
|
||||||
|
| `core` profile tools | 37 | 38 (+1: `query_scene_state`) |
|
||||||
|
| New files | — | `lib/tools/scene-management.js`, `lib/tools/scripts.js` |
|
||||||
|
| Modified files | — | `scene.js`, `lib/prefabs.js`, `lib/tool-registry.js`, `lib/tools/assets-advanced.js` |
|
||||||
|
| New scene.js methods | — | `renameNode`, `reparentNode` |
|
||||||
|
| New unit test files | — | `test/scene-management.test.js`, `test/scripts.test.js`, `test/find-unused-assets.test.js` |
|
||||||
|
| Gaps filled | Scene creation, node clipboard, node rename/reparent, prefab creation, script creation, batch assets, unused asset scan | All high-frequency gaps covered |
|
||||||
File diff suppressed because it is too large
Load Diff
+244
@@ -0,0 +1,244 @@
|
|||||||
|
# Tool Reference
|
||||||
|
|
||||||
|
<!-- This file is generated by `npm run docs:generate`. Do not edit by hand. -->
|
||||||
|
|
||||||
|
Generated from `lib/tool-registry.js`. The default `core` profile exposes 38 tools; the `full` profile exposes 110 tools.
|
||||||
|
|
||||||
|
## Profile Summary
|
||||||
|
|
||||||
|
| Profile | Tool Count | Purpose |
|
||||||
|
|---|---:|---|
|
||||||
|
| `core` | 38 | Focused default surface for common editor automation. |
|
||||||
|
| `full` | 110 | All built-in tools, including destructive and low-level helpers. |
|
||||||
|
|
||||||
|
## Core Tools
|
||||||
|
|
||||||
|
`capture_editor_screenshot`, `capture_preview_screenshot`, `capture_scene_screenshot`, `check_for_updates`, `clear_logs`, `execute_editor_script`, `execute_javascript`, `execute_scene_script`, `get_build_status`, `get_editor_state`, `get_hierarchy`, `get_performance_snapshot`, `get_project_info`, `get_recent_logs`, `get_runtime_state`, `get_scene_info`, `get_script_diagnostic_context`, `get_selection`, `get_tool_catalog`, `inspect_asset`, `inspect_asset_dependencies`, `inspect_prefab`, `inspect_prefab_instance`, `list_assets`, `list_editor_windows`, `list_project_instructions`, `list_scenes`, `open_asset`, `open_scene`, `query_scene_state`, `read_project_instruction`, `run_script_diagnostics`, `search_project_logs`, `select_asset`, `set_selection`, `validate_asset_dependencies`, `validate_prefab_references`, `validate_scene`
|
||||||
|
|
||||||
|
## Tools By Category
|
||||||
|
|
||||||
|
### Animation
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `add_animation_clip` | `full` | stateful | Add an AnimationClip asset to a node Animation component. |
|
||||||
|
| `list_animations` | `full` | read-only | [core] List Animation components in the active scene or under one node. |
|
||||||
|
| `play_animation` | `full` | stateful | [core] Play an Animation component clip on a node. |
|
||||||
|
| `stop_animation` | `full` | stateful | [core] Stop an Animation component clip on a node. |
|
||||||
|
|
||||||
|
### Assets
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `batch_asset_ops` | `full` | mutating | [core] Batch import or delete assets in the Cocos asset database. |
|
||||||
|
| `create_scene` | `full` | stateful | [core] Create a new scene asset in the Cocos project and optionally open it. |
|
||||||
|
| `delete_asset` | `full` | mutating | Delete an asset from asset-db by uuid, db url, or path. |
|
||||||
|
| `find_unused_assets` | `full` | read-only | [core] Find assets not referenced by any scene, prefab, or animation in the project. |
|
||||||
|
| `inspect_asset` | `core`, `full` | read-only | [specialist] Inspect asset-db info, metadata, and serialized asset data by uuid or path. Prefer this when you need a precise structured asset read. |
|
||||||
|
| `inspect_asset_dependencies` | `core`, `full` | read-only | [specialist] Inspect UUID-style dependencies referenced by a serialized Cocos asset. |
|
||||||
|
| `list_assets` | `core`, `full` | read-only | [specialist] Query project assets from asset-db by pattern or asset type. Prefer this when you need exact asset discovery; otherwise use execute_javascript for broader automation. |
|
||||||
|
| `list_scenes` | `core`, `full` | read-only | [specialist] List scene assets in the project. Prefer this when you need exact scene discovery before opening one; otherwise stay in execute_javascript for broader workflows. |
|
||||||
|
| `open_asset` | `core`, `full` | stateful | [specialist] Open an asset inside Cocos Creator by uuid, db url, or path. Use this only when opening the asset itself is the explicit next step. |
|
||||||
|
| `open_scene` | `core`, `full` | stateful | [specialist] Open a scene asset in Cocos Creator by uuid, db url, or path. Use this when scene switching is the explicit goal; otherwise keep execute_javascript as the main planning tool. |
|
||||||
|
| `run_scene_asset` | `full` | mutating | Load a scene asset by uuid directly into the current runtime scene context. |
|
||||||
|
| `select_asset` | `core`, `full` | stateful | [specialist] Select an asset in the Cocos editor. Use this when editor selection state matters; otherwise keep execute_javascript as the primary workflow. |
|
||||||
|
|
||||||
|
### Broadcast
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `broadcast_editor_message` | `full` | stateful | [core] Send or broadcast a Cocos editor message for advanced editor automation. |
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_build_status` | `core`, `full` | read-only | [specialist] Query Cocos build/preview status using known builder message variants. |
|
||||||
|
| `open_build_panel` | `full` | stateful | [core] Open the Cocos build panel, defaulting to the builder panel id. |
|
||||||
|
| `run_project_preview` | `full` | stateful | [core] Start Cocos preview/run using known preview and builder message variants. |
|
||||||
|
| `save_current_scene` | `full` | stateful | [core] Save the currently open Cocos scene using available editor scene messages. |
|
||||||
|
|
||||||
|
### Camera
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `create_camera` | `full` | stateful | Create a Camera node in the active scene. |
|
||||||
|
| `list_cameras` | `full` | read-only | [core] List Camera components in the active scene. |
|
||||||
|
| `set_camera_properties` | `full` | mutating | Set selected Camera component properties. |
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `add_component` | `full` | stateful | Add a component to a node by component class name. |
|
||||||
|
| `inspect_component` | `full` | read-only | [core] Inspect a component attached to a node. |
|
||||||
|
| `invoke_component_method` | `full` | mutating | [core] Invoke a method on a component for runtime validation and test hooks. |
|
||||||
|
| `list_components` | `full` | read-only | [core] List components attached to a scene node. |
|
||||||
|
| `remove_component` | `full` | mutating | Remove a component from a node by name or index. |
|
||||||
|
| `reset_component_property` | `full` | mutating | Reset or clear a component property by dot path. |
|
||||||
|
| `set_component_property` | `full` | mutating | Set a component property by dot path using a JSON value. |
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_script_diagnostic_context` | `core`, `full` | read-only | [specialist] Run TypeScript diagnostics and attach source snippets for each error. This is a preferred specialist tool for compile-error triage before repair. |
|
||||||
|
| `run_script_diagnostics` | `core`, `full` | stateful | [specialist] Run a TypeScript no-emit check for the current Cocos project and return parsed diagnostics. This is a preferred specialist tool for script errors when diagnostics are needed. |
|
||||||
|
| `validate_asset_dependencies` | `core`, `full` | read-only | [specialist] Validate UUID-style dependencies for one asset or a project asset query. |
|
||||||
|
| `validate_prefab_references` | `core`, `full` | read-only | [specialist] Validate prefab asset references by checking serialized UUID references against asset-db. |
|
||||||
|
| `validate_scene` | `core`, `full` | read-only | [specialist] Run a compact validation pass over the active scene, runtime state, TypeScript diagnostics, and recent project log errors. |
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `bind_button_click_event` | `full` | stateful | [core] Bind a Cocos Button click event to a target node component method. |
|
||||||
|
| `emit_node_event` | `full` | mutating | [core] Emit a custom event on a target scene node with an optional JSON payload. |
|
||||||
|
| `list_button_click_events` | `full` | read-only | [core] List click event bindings on a Cocos Button component. |
|
||||||
|
| `simulate_button_click` | `full` | mutating | [core] Simulate a Cocos Button click by emitting click events on the target button node. |
|
||||||
|
|
||||||
|
### Execution
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `execute_editor_script` | `core`, `full` | mutating | [compat] Execute JavaScript in the editor/browser context. Prefer execute_javascript with context="editor" as the main unified tool; use this when you specifically want the editor-only compatibility entrypoint. |
|
||||||
|
| `execute_javascript` | `core`, `full` | mutating | [primary] Execute JavaScript in either the scene or editor context. Use context="scene" for live scene/runtime inspection and mutation, or context="editor" for Editor APIs, asset-db workflows, MCP orchestration, local filesystem access, and higher-level automation. Prefer this as the main flexible tool when many narrow tools would be noisy. |
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `exists` | `full` | read-only | [core] Check whether a project file or directory exists. |
|
||||||
|
| `get_file_snippet` | `full` | read-only | [core] Read a focused snippet around a file line number. |
|
||||||
|
| `list_directory` | `full` | read-only | [core] List files and directories inside a project directory. |
|
||||||
|
| `read_file` | `full` | read-only | [core] Read a file from the Cocos project. |
|
||||||
|
| `refresh_assets` | `full` | stateful | [core] Best-effort asset database refresh for a file or the assets root. |
|
||||||
|
| `replace_in_file` | `full` | mutating | [core] Replace text in a file, useful for script auto-fix loops. |
|
||||||
|
| `search_files` | `full` | read-only | [core] Search project files by simple wildcard pattern. |
|
||||||
|
| `write_file` | `full` | mutating | [core] Write or overwrite a file in the Cocos project. |
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `simulate_key_combo` | `full` | mutating | [core] Send a low-level Electron modified key press such as Ctrl+S or Cmd+P. |
|
||||||
|
| `simulate_key_press` | `full` | mutating | [core] Send a low-level Electron key press to the editor, preview, or simulator window. |
|
||||||
|
| `simulate_mouse_click` | `full` | mutating | [core] Send a low-level Electron mouse click to the editor, preview, or simulator window. |
|
||||||
|
| `simulate_mouse_drag` | `full` | mutating | [core] Send a low-level Electron mouse drag to the editor, preview, or simulator window. |
|
||||||
|
| `simulate_preview_input` | `full` | mutating | [core] Convenience wrapper for low-level preview/simulator input. Uses mouse click by default or key press when keyCode is provided. |
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `create_cocos_mcp_project_skill` | `full` | stateful | [core] Create a recommended local Codex project skill for Funplay Cocos MCP workflows. |
|
||||||
|
| `create_project_skill` | `full` | stateful | [core] Create a local Codex project skill under .codex/skills/{skillName}/SKILL.md. |
|
||||||
|
| `list_project_instructions` | `core`, `full` | read-only | [specialist] List project AI instruction files and local Codex project skills. |
|
||||||
|
| `read_project_instruction` | `core`, `full` | read-only | [specialist] Read a project AI instruction file such as AGENTS.md, CLAUDE.md, or a .codex skill SKILL.md. |
|
||||||
|
| `write_project_instruction` | `full` | mutating | [core] Create or update a project AI instruction file inside the Cocos project. |
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `clear_logs` | `core`, `full` | mutating | [specialist] Clear in-memory MCP logs and, only with explicit confirmation, truncate common project log files. |
|
||||||
|
| `get_recent_logs` | `core`, `full` | read-only | [specialist] Return recent MCP runtime logs, recent tool interactions, and tails of common project log files. |
|
||||||
|
| `search_project_logs` | `core`, `full` | read-only | [specialist] Search common Cocos project log files for a string or regular expression. |
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `create_script` | `full` | stateful | [core] Create a new TypeScript component script with a standard Cocos template. |
|
||||||
|
| `get_performance_snapshot` | `core`, `full` | read-only | [specialist] Return scene scale and runtime performance-oriented counters such as node/component counts, UI counts, depth, memory, and warnings. |
|
||||||
|
| `list_editor_windows` | `core`, `full` | read-only | [specialist] List available Electron windows so screenshots or input-targeting can choose the correct window. Use this when window targeting is the explicit problem. |
|
||||||
|
|
||||||
|
### Prefabs
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `apply_prefab_instance` | `full` | stateful | [core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message. |
|
||||||
|
| `create_prefab` | `full` | stateful | [core] Create a prefab asset from a scene node. |
|
||||||
|
| `create_prefab_instance` | `full` | stateful | [core] Create a linked prefab instance in the editor hierarchy using Cocos scene create-node when available. |
|
||||||
|
| `duplicate_prefab` | `full` | stateful | [core] Create a new prefab asset by duplicating an existing prefab file without copying its .meta UUID. |
|
||||||
|
| `edit_prefab_json` | `full` | stateful | [core] Edit a prefab JSON file by JSON path assignment or literal search/replace, then validate references. |
|
||||||
|
| `inspect_prefab` | `core`, `full` | read-only | [specialist] Inspect a prefab asset, its metadata, serialized file path, and UUID-like asset references. |
|
||||||
|
| `inspect_prefab_instance` | `core`, `full` | read-only | [specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available. |
|
||||||
|
| `instantiate_prefab` | `full` | stateful | Instantiate a prefab into the active scene by prefab uuid. |
|
||||||
|
| `list_prefabs` | `full` | read-only | [core] List prefab assets in the project. |
|
||||||
|
| `revert_prefab_instance` | `full` | stateful | [core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages. |
|
||||||
|
|
||||||
|
### Preferences
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_editor_preference` | `full` | read-only | [core] Read a Cocos editor preference through Editor.Profile when available. |
|
||||||
|
| `set_editor_preference` | `full` | mutating | [core] Write a Cocos editor preference through Editor.Profile when available. |
|
||||||
|
|
||||||
|
### Project
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_editor_state` | `core`, `full` | read-only | [specialist] Return a structured editor-state snapshot including project info, runtime server status, current selection, and visible Electron windows. Prefer this when you want one compact editor summary. |
|
||||||
|
| `get_project_info` | `core`, `full` | read-only | [specialist] Return the active Cocos project path, version, and MCP server configuration. Prefer this for a fast structured project summary; use execute_javascript when you need to inspect and act in one step. |
|
||||||
|
| `get_tool_catalog` | `core`, `full` | read-only | [specialist] Return every built-in MCP tool with profile, category, and current exposure state. Use this before changing custom tool exposure. |
|
||||||
|
|
||||||
|
### Runtime
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_runtime_state` | `core`, `full` | read-only | [specialist] Return structured Cocos runtime state including pause state, frame count, and scheduler time scale. Prefer this when you want a compact validation snapshot. |
|
||||||
|
| `pause_runtime` | `full` | stateful | [core] Pause Cocos director game logic execution. |
|
||||||
|
| `resume_runtime` | `full` | stateful | [core] Resume Cocos director game logic execution. |
|
||||||
|
| `set_time_scale` | `full` | mutating | [core] Set Cocos scheduler time scale for runtime validation. |
|
||||||
|
|
||||||
|
### Scene
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `copy_paste_node` | `full` | stateful | [core] Copy, cut, or paste scene nodes via the Cocos editor clipboard. |
|
||||||
|
| `create_node` | `full` | stateful | Create a new node under the active scene or a specified parent path. |
|
||||||
|
| `delete_node` | `full` | mutating | Delete a node by path, uuid, or name. |
|
||||||
|
| `execute_scene_script` | `core`, `full` | mutating | [compat] Execute JavaScript in the active Cocos scene context. Prefer execute_javascript with context="scene" as the main unified tool; use this when you specifically want the scene-only compatibility entrypoint. |
|
||||||
|
| `find_nodes` | `full` | read-only | [core] Find scene nodes by exact name, partial path, or component type. |
|
||||||
|
| `get_hierarchy` | `core`, `full` | read-only | [specialist] Return a structured hierarchy tree from the active scene or a specific node path. Prefer execute_javascript for broader reasoning or repair; use this when you want a predictable hierarchy snapshot. |
|
||||||
|
| `get_scene_info` | `core`, `full` | read-only | [specialist] Return a structured summary of the active Cocos scene. Prefer execute_javascript for multi-step inspection or mutation; use this when you specifically want a compact scene snapshot. |
|
||||||
|
| `inspect_node` | `full` | read-only | [core] Inspect a specific node by path, uuid, or name. |
|
||||||
|
| `query_scene_state` | `core`, `full` | stateful | [specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload. |
|
||||||
|
| `rename_node` | `full` | stateful | [core] Rename a scene node. |
|
||||||
|
| `reparent_node` | `full` | stateful | [core] Move a node to a new parent in the scene hierarchy. |
|
||||||
|
| `set_node_transform` | `full` | mutating | Update node position, rotation, scale, or active state. |
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `capture_desktop_screenshot` | `full` | read-only | [core] Capture a screenshot from the local desktop and return it as an MCP image payload. |
|
||||||
|
| `capture_editor_screenshot` | `core`, `full` | read-only | [specialist] Capture the focused Cocos Creator editor window and return it as an MCP image payload. Prefer screenshot tools only when visual verification is explicitly needed. |
|
||||||
|
| `capture_game_screenshot` | `full` | read-only | [core] Capture the Game/Preview panel region from the editor window with panel-level cropping when available. |
|
||||||
|
| `capture_preview_screenshot` | `core`, `full` | read-only | [specialist] Capture the preview or simulator window as an MCP image payload. Prefer this only when you need visual proof of game or preview output. |
|
||||||
|
| `capture_scene_screenshot` | `core`, `full` | read-only | [specialist] Capture the Scene panel region from the editor window with panel-level cropping when available. Prefer this only for visual validation of scene-side results. |
|
||||||
|
|
||||||
|
### Selection
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_editor_selection` | `full` | read-only | [compat] Return the current node and asset selection in the Cocos editor. Prefer get_selection as the primary structured selection read tool. |
|
||||||
|
| `get_selection` | `core`, `full` | read-only | [specialist] Return the current editor selection in a compact structured form. Prefer this when selection state matters for the next action. |
|
||||||
|
| `set_selection` | `core`, `full` | mutating | [specialist] Set or clear the current editor selection for an asset or node. Use this when downstream editor workflows depend on selection state. |
|
||||||
|
|
||||||
|
### Ui
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `create_button` | `full` | stateful | Create a UI Button node with child Label. |
|
||||||
|
| `create_canvas` | `full` | stateful | Create a Cocos Canvas node with UITransform. |
|
||||||
|
| `create_label` | `full` | stateful | Create a UI Label node under a parent. |
|
||||||
|
| `create_sprite` | `full` | stateful | Create a UI Sprite node, optionally assigning a SpriteFrame asset uuid. |
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
| Tool | Profiles | Access | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `check_for_updates` | `core`, `full` | read-only | [specialist] Check the latest Funplay Cocos MCP GitHub release and compare it with the installed extension version. |
|
||||||
|
|
||||||
@@ -0,0 +1,722 @@
|
|||||||
|
# Cocos Creator 3.8.8 编辑器 Message 列表
|
||||||
|
|
||||||
|
> 数据来源: Cocos Creator 3.8.8 编辑器 app.asar 内置扩展包
|
||||||
|
> 提取时间: 2026-06-30
|
||||||
|
> i18n 已解析为中文文本
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [animation-graph](#animation-graph) (10 messages)
|
||||||
|
2. [animator](#animator) (38 messages)
|
||||||
|
3. [asset-db](#asset-db) (66 messages)
|
||||||
|
4. [builder](#builder) (59 messages)
|
||||||
|
5. [engine](#engine) (12 messages)
|
||||||
|
6. [information](#information) (4 messages)
|
||||||
|
7. [menu](#menu) (3 messages)
|
||||||
|
8. [messages](#messages) (11 messages)
|
||||||
|
9. [metrics](#metrics) (11 messages)
|
||||||
|
10. [placeholder](#placeholder) (1 messages)
|
||||||
|
11. [preferences](#preferences) (10 messages)
|
||||||
|
12. [preview](#preview) (22 messages)
|
||||||
|
13. [program](#program) (5 messages)
|
||||||
|
14. [programming](#programming) (12 messages)
|
||||||
|
15. [project](#project) (19 messages)
|
||||||
|
16. [scene](#scene) (218 messages)
|
||||||
|
17. [server](#server) (7 messages)
|
||||||
|
18. [shortcuts](#shortcuts) (8 messages)
|
||||||
|
19. [tester](#tester) (4 messages)
|
||||||
|
20. [utils](#utils) (2 messages)
|
||||||
|
21. [window](#window) (4 messages)
|
||||||
|
22. [cocos-service](#cocos-service) (16 messages)
|
||||||
|
23. [im-plugin](#im-plugin) (11 messages)
|
||||||
|
|
||||||
|
**总计: 553 条 message**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## animation-graph
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open | open | No | | | |
|
||||||
|
| dialog-warn | dialogWarn | No | | | |
|
||||||
|
| apply | default.apply | No | | | |
|
||||||
|
| unselect | default.unselect | No | | | |
|
||||||
|
| delete | default.delete | No | | | |
|
||||||
|
| copy | default.copy | No | | | |
|
||||||
|
| duplicate | default.duplicate | No | | | |
|
||||||
|
| paste | default.paste | No | | | |
|
||||||
|
| scene:ready | profileBindWatchWhenSceneReady, default.refresh | No | | | |
|
||||||
|
| animation-graph:changed | default.refresh | No | | | |
|
||||||
|
|
||||||
|
## animator
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open | open | No | | | |
|
||||||
|
| scene:ready | default.scene:ready | No | | | |
|
||||||
|
| scene:close | default.scene:close | No | | | |
|
||||||
|
| scene:change-node | default.scene:change-node | No | | | |
|
||||||
|
| scene:animation-start | default.scene:animation-start | No | | | |
|
||||||
|
| scene:animation-end | default.scene:animation-end | No | | | |
|
||||||
|
| scene:animation-change | default.scene:animation-change | No | | | |
|
||||||
|
| scene:animation-state-change | default.scene:animation-state-change | No | | | |
|
||||||
|
| scene:change-mode | default.scene:change-mode | No | | | |
|
||||||
|
| scene:animation-clip-change | default.scene:animation-clip-change | No | | | |
|
||||||
|
| selection:activated | default.selection:activated | No | | | |
|
||||||
|
| asset-db:asset-change | default.asset-db:asset-change | No | | | |
|
||||||
|
| asset-db:asset-delete | default.asset-db:asset-delete | No | | | |
|
||||||
|
| inspector-drop-animation | dropClipToNode | No | | | |
|
||||||
|
| change-debug-mode | default.change-debug-mode | No | | | |
|
||||||
|
| copy | default.copy | No | | | |
|
||||||
|
| paste | default.paste | No | | | |
|
||||||
|
| select-all | default.selectAll | No | | | |
|
||||||
|
| delete | default.deleteSelected | No | | | |
|
||||||
|
| create | default.createKey | No | | | |
|
||||||
|
| focus | default.showAllKeys | No | | | |
|
||||||
|
| show-selected-keys | default.showSelectedKeys | No | | | |
|
||||||
|
| next-step | default.nextStep | No | | | |
|
||||||
|
| prev-step | default.prevStep | No | | | |
|
||||||
|
| jump-to-next-key | default.jumpToNextKey | No | | | |
|
||||||
|
| jump-to-prev-key | default.jumpToPrevKey | No | | | |
|
||||||
|
| jump-to-first-frame | default.jumpFirstFrame | No | | | |
|
||||||
|
| jump-to-last-frame | default.jumpLastFrame | No | | | |
|
||||||
|
| play-or-pause | default.playOrPause | No | | | |
|
||||||
|
| stop | default.stop | No | | | |
|
||||||
|
| clear-selected | default.clearSelect | No | | | |
|
||||||
|
| switch-animation-mode | default.changeRecordState | No | | | |
|
||||||
|
| open-docs | openDocs | No | | | |
|
||||||
|
| query-last-clip-cache | queryLatestClipCache | No | | | |
|
||||||
|
| save-clip-cache | saveClipCacheToFile | No | | | |
|
||||||
|
| update-cache-config | updateCacheConfig | No | | | |
|
||||||
|
| enable-embedded-player | default.enableEmbeddedPlayer | No | | | |
|
||||||
|
| enable-auxiliary-curve | default.enableAuxiliaryCurve | No | | | |
|
||||||
|
|
||||||
|
## asset-db
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| asset-db:ready | ready | Yes | 资源数据库准备就绪时的广播 | | Editor.Message.broadcast('asset-db:ready'); |
|
||||||
|
| asset-db:close | close | Yes | 资源数据库关闭时的广播 | | Editor.Message.broadcast('asset-db:close'); |
|
||||||
|
| asset-db:asset-add | | Yes | 当资源数据库准备就绪后,再新增资源时的广播 | | Editor.Message.broadcast('asset-db:asset-add'); |
|
||||||
|
| asset-db:asset-change | | Yes | 当一个资源被修改时的广播 | | Editor.Message.broadcast('asset-db:asset-change'); |
|
||||||
|
| asset-db:asset-delete | | Yes | 当一个资源被删除时的广播 | | Editor.Message.broadcast('asset-db:asset-delete'); |
|
||||||
|
| project:change-high-quality | projectChangeHighQuality | No | | | |
|
||||||
|
| query-ready | queryReady | Yes | 检查资源数据库是否启动完毕 | @returns {boolean} 是否准备就绪 | await Editor.Message.request('asset-db', 'query-ready'); |
|
||||||
|
| create-asset | createAsset | Yes | 新建一个资源 | - url {string} 资源的 URL,例如 db://assets/abc.json<br>- content {string \| null} 写入文件的 string,为 null 则新建文件夹<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'create-asset', url, content); |
|
||||||
|
| new-asset | newAsset | No | 新建一个资源 | | |
|
||||||
|
| import-asset | importAsset | Yes | 将一个文件或文件夹导入到资源数据库内 | - source {string} 本地的文件绝对地址<br>- target {string} 导入到数据库的 url 地址<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'import-asset', path, url, { overwrite: true, rename: true }); |
|
||||||
|
| copy-asset | copyAsset | Yes | 复制某个资源 | - source {string} 源资源的 URL 路径,例如 db://assets/abc.json<br>- target {string} 复制到的目标位置 URL<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'copy-asset', sourceUrl, targetUrl, { overwrite: true, rename: true }); |
|
||||||
|
| move-asset | moveAsset | Yes | 将一个资源移动到某个地方 | - source {string} 需要移动的源资源 URL 路径,例如 db://assets/abc.json<br>- target {string} 移动到的目标位置 URL<br>- option {AssetOperationOption}<br> - option.overwrite {boolean} 是否强制覆盖,默认 false<br> - option.rename {boolean} 冲突是否自动更名,默认 false<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'move-asset', sourceUrl, targetUrl); |
|
||||||
|
| rename-asset | renameAsset | No | 重命名指定资源 | | |
|
||||||
|
| delete-asset | deleteAsset | Yes | 删除一个资源 | - url {string} 资源的 URL<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'delete-asset', pathOrUrlOrUUID); |
|
||||||
|
| open-asset | openAsset | Yes | 尝试使用记录的打开程序打开一个资源 | - urlOrUUID {string} 尝试打开的资源 URL 或者 UUID | await Editor.Message.request('asset-db', 'open-asset', urlOrUUID); |
|
||||||
|
| save-asset | saveAsset | Yes | 保存资源 | - urlOrUUID {string} 资源的 URL 或者 UUID<br>- content {string \| Buffer} 资源的内容字符串,如果是 typeArray,请使用 Buffer.from 转换。<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'save-asset', urlOrUUID, content); |
|
||||||
|
| save-asset-meta | saveAssetMeta | Yes | 保存资源的 meta 信息 | - urlOrUUID {string} 资源的 URL 或者 UUID<br>- content {string} 资源 meta 序列化后的内容字符串<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'save-asset-meta', urlOrUUID, content); |
|
||||||
|
| reimport-asset | reimportAsset | Yes | 重新导入资源 | - urlOrUUID {string} 资源的 URL 或者 UUID | await Editor.Message.request('asset-db', 'reimport-asset', urlOrUUID); |
|
||||||
|
| refresh-asset | refreshAsset | Yes | 刷新一个资源所在的 url 位置,删除资源会被销毁,新增资源会导入 | - urlOrUUID {string} 资源的 URL 或者 UUID | await Editor.Message.request('asset-db', 'refresh-asset', urlOrUUID); |
|
||||||
|
| refresh-all-effect | refreshAllEffect | No | | | |
|
||||||
|
| query-path | queryPath | Yes | 查询一个资源的路径 | - urlOrUUID {string} 资源的 URL 或者 UUID<br><br>@returns {string} 返回一个资源的磁盘绝对路径 | await Editor.Message.request('asset-db', 'query-path', urlOrUUID); |
|
||||||
|
| query-url | queryUrl | Yes | 查询一个资源的 URL | - uuid {string} 资源的 UUID<br><br>@returns {string} 返回一个资源的 url | await Editor.Message.request('asset-db', 'query-url', uuidOrPath); |
|
||||||
|
| query-uuid | queryUUID | Yes | 查询一个资源的 UUID | - url {string} 资源的 URL<br><br>@returns {string} 返回一个资源的 uuid | await Editor.Message.request('asset-db', 'query-uuid', urlOrUUID); |
|
||||||
|
| query-assets | queryAssets | Yes | 根据条件查询资源数组 | - pattern? {string} 路径匹配模式,glob 格式 (db://**)<br>- ccType? {string} 资源类型,例如 cc.Texture2D<br>- importer? {string} 资源导入器类型,例如 texture<br><br>@returns {AssetInfo[]} 返回资源数组 | await Editor.Message.request('asset-db', 'query-assets', { ccType: 'cc.Script' }); |
|
||||||
|
| query-asset-info | queryAssetInfo | Yes | 查询一个资源的基本信息 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br><br>@returns {AssetInfo} 返回一个资源信息 | await Editor.Message.request('asset-db', 'query-asset-info', urlOrUUIDOrPath); |
|
||||||
|
| query-missing-asset-info | queryMissingAssetInfo | No | | | |
|
||||||
|
| query-asset-meta | queryAssetMeta | Yes | 查询一个资源的 META 信息 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br><br>@returns {AssetMeta} 返回一个资源 meta 信息 | await Editor.Message.request('asset-db', 'query-asset-meta', urlOrUUID); |
|
||||||
|
| query-asset-dependinces | queryAssetDependencies | No | | | |
|
||||||
|
| query-asset-used | queryAssetUsed | No | | | |
|
||||||
|
| query-asset-dependencies | queryAssetDependencies | Yes | 查询一个资源依赖的资源或脚本 uuid 数组 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br>- type? {QueryAssetType} 查询的资源类型,默认 asset, 可选值:asset, script, all<br><br>@returns {string[]} 返回一个资源依赖的资源或脚本 uuid 数组 | await Editor.Message.request('asset-db', 'query-asset-dependencies', urlOrUUID, type); |
|
||||||
|
| query-asset-users | queryAssetUsers | Yes | 查询一个资源被哪些资源或脚本直接使用到 | - urlOrUUID {string} 资源的 url 地址或者 uuid<br>- type? {QueryAssetType} 查询的资源类型,默认 asset, 可选值:asset, script, all<br><br>@returns {string[]} 返回一个资源被哪些资源或脚本直接使用到 | await Editor.Message.request('asset-db', 'query-asset-users', urlOrUUID, type); |
|
||||||
|
| query-asset-data | queryAssetData | No | | | |
|
||||||
|
| generate-available-url | generateAvailableUrl | Yes | 根据传入的 url 生成一个可用的新 url | - url {string} 资源的 url<br><br>@returns {string} 返回一个新的或者和传入参数一样的 url | await Editor.Message.request('asset-db', 'generate-available-url', url); |
|
||||||
|
| query-asset-mtime | queryAssetMtime | No | | | |
|
||||||
|
| refresh | refresh | No | | | |
|
||||||
|
| is-busy | isBusy | No | | | |
|
||||||
|
| pause | pause | No | | | |
|
||||||
|
| resume | resume | No | | | |
|
||||||
|
| open-devtools | open-devtools | No | | | |
|
||||||
|
| query-db-info | query-db-info | No | | | |
|
||||||
|
| query-db-infos | query-db-infos | No | | | |
|
||||||
|
| query-db-list | query-db-list | No | | | |
|
||||||
|
| create-asset-dialog | create-asset-dialog | No | | | |
|
||||||
|
| create-asset-template | create-asset-template | No | | | |
|
||||||
|
| init-asset | init-asset | No | | | |
|
||||||
|
| query-all-importer | query-all-importer | No | | | |
|
||||||
|
| query-all-asset-types | query-all-asset-types | No | | | |
|
||||||
|
| execute-script | executeScript | No | | | |
|
||||||
|
| execute-custom-operation | executeCustomOperation | No | | | |
|
||||||
|
| start-db | startDatabase | No | | | |
|
||||||
|
| stop-db | stopDatabase | No | | | |
|
||||||
|
| notice-reload-editor | noticeReloadEditor | No | | | |
|
||||||
|
| notice-config-changed | noticeConfigChanged | No | | | |
|
||||||
|
| update-config | updateConfig | No | | | |
|
||||||
|
| refresh-all-database | refreshAllDatabase | No | | | |
|
||||||
|
| refresh-default-user-data-config | refreshDefaultUserDataConfig | No | | | |
|
||||||
|
| update-default-user-data | updateDefaultUserData | No | | | |
|
||||||
|
| query-create-list | queryCreateList | No | | | |
|
||||||
|
| query-icon-config-map | queryIconConfigMap | No | | | |
|
||||||
|
| query-asset-config-map | queryAssetConfigMap | No | | | |
|
||||||
|
| query-create-menu-list | queryCreateMenuList | No | | | |
|
||||||
|
| query-asset-userData-config | queryAssetUserDataConfig | No | | | |
|
||||||
|
| query-asset-thumbnail | queryAssetThumbnail | No | | | |
|
||||||
|
| batch-message-handler | batchMessageHandler | No | | | |
|
||||||
|
| show-asset-template-dir | showAssetTemplateDir | No | | | |
|
||||||
|
| query-global-internal-library | query-global-internal-library | No | | | |
|
||||||
|
|
||||||
|
## builder
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open | open | Yes | 打开构建面板 | @returns {null} | await Editor.Message.request('builder', 'open'); |
|
||||||
|
| open-bundle | openBundle | No | | | |
|
||||||
|
| change-build-bundle | build-bundle.changeBuildBundle | No | | | |
|
||||||
|
| open-page | default.open-page | No | | | |
|
||||||
|
| query-worker-ready | query-worker-ready | Yes | 查询构建进程是否启动 | @returns {boolean} - 构建进程是否准备好 | await Editor.Message.request('builder', 'query-worker-ready'); |
|
||||||
|
| execute-build-stage | execute-build-stage | No | | | |
|
||||||
|
| open-platform-debug-tools | open-platform-debug-tools | No | | | |
|
||||||
|
| create-build-plugin-template | createBuildPluginTemplate | No | | | |
|
||||||
|
| create-build-template | create-build-template | No | | | |
|
||||||
|
| create-application-template | createApplicationTemplate | No | | | |
|
||||||
|
| open-devtools | openWorkerDevTool | No | | | |
|
||||||
|
| generate-preview-setting | generate-preview-setting | No | | | |
|
||||||
|
| query-tasks-info | query-tasks-info | No | | | |
|
||||||
|
| add-task | add-task | No | | | |
|
||||||
|
| select-all-task | default.select-all-task | No | | | |
|
||||||
|
| clear-selected-task | default.clear-selected-task | No | | | |
|
||||||
|
| add-bundle-task | add-bundle-task | No | | | |
|
||||||
|
| recompile-task | recompile-task | No | | | |
|
||||||
|
| remove-task | remove-task | No | | | |
|
||||||
|
| break-task | break-task | No | | | |
|
||||||
|
| query-task | query-task | No | | | |
|
||||||
|
| update-task | update-task | No | | | |
|
||||||
|
| save-task | save-task | No | | | |
|
||||||
|
| preview-pac | preview-pac | No | | | |
|
||||||
|
| query-atlas-files | query-atlas-files | No | | | |
|
||||||
|
| command-build | command-build | No | | | |
|
||||||
|
| asset-db:ready | asset-db:ready, default.asset-db:ready | No | | | |
|
||||||
|
| asset-db:close | asset-db:close | No | | | |
|
||||||
|
| asset-db:asset-delete | default.asset-db:asset-delete, build-bundle.asset-db:asset-delete | No | | | |
|
||||||
|
| asset-db:asset-add | default.asset-db:asset-add, build-bundle.asset-db:asset-add | No | | | |
|
||||||
|
| asset-db:asset-change | default.asset-db:asset-change, build-bundle.asset-db:asset-change | No | | | |
|
||||||
|
| request-to-build-worker | request-to-build-worker | No | | | |
|
||||||
|
| execute-hook-task | execute-hook-task | No | | | |
|
||||||
|
| preferences-changed | preferences-changed | No | | | |
|
||||||
|
| query-compress-config | query-compress-config | No | | | |
|
||||||
|
| query-platform-config | query-platform-config | No | | | |
|
||||||
|
| query-bundle-config | query-bundle-config | No | | | |
|
||||||
|
| migrate-options | migrateOptions | No | | | |
|
||||||
|
| builder:task-changed | default.builder:task-changed, build-bundle.builder:task-changed | No | | | |
|
||||||
|
| builder:task-add | build-bundle.builder:task-add | No | | | |
|
||||||
|
| builder:task-delete | default.builder:task-delete, build-bundle.builder:task-delete | No | | | |
|
||||||
|
| builder:bundle-task-changed | build-bundle.bundle-task:changed | No | | | |
|
||||||
|
| change-debug-mode | default.change-debug-mode | No | | | |
|
||||||
|
| build-worker:ready | default.onBuildWorkerReady, build-bundle.onBuildWorkerReady | Yes | 构建进程启动 | | Editor.Message.broadcast('build-worker:ready'); |
|
||||||
|
| build-worker:closed | default.onBuildWorkerClosed, build-bundle.onBuildWorkerClosed | Yes | 构建进程关闭 | | Editor.Message.broadcast('build-worker:closed'); |
|
||||||
|
| build-by-shortcut | default.buildByShortcut | No | | | |
|
||||||
|
| console:update-log-level | console:update-log-level | No | | | |
|
||||||
|
| register-package | registerPackage | No | | | |
|
||||||
|
| unregister-package | unRegisterPackage | No | | | |
|
||||||
|
| programming:pack-build-end | programming:pack-build-end | No | | | |
|
||||||
|
| open-docs | openDocs | No | | | |
|
||||||
|
| check-and-complete-options | check-and-complete-options | No | | | |
|
||||||
|
| open-panel-devtools | openPanelDevTools | No | | | |
|
||||||
|
| open-worker-devtools | openWorkerDevTool | No | | | |
|
||||||
|
| clear-all-cache | clearAllCache | No | | | |
|
||||||
|
| clear-assets-cache | clearProjectAssetsCache | No | | | |
|
||||||
|
| clear-engine-cache | clearEngineCache | No | | | |
|
||||||
|
| copy-build-notice | copy-build-notice | No | | | |
|
||||||
|
| export-bundle-config | build-bundle.exportBundleBuildConfig | No | | | |
|
||||||
|
|
||||||
|
## engine
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| rebuild | rebuild | No | | | |
|
||||||
|
| relaunch | relaunch | No | | | |
|
||||||
|
| import-engine-error | importEngineError | No | | | |
|
||||||
|
| pipeline-config-change | onPipelineConfigChange | No | | | |
|
||||||
|
| query-info | query-info | No | | | |
|
||||||
|
| query-engine-info | query-engine-info | No | | | |
|
||||||
|
| query-modules-config | query-modules-config | No | | | |
|
||||||
|
| change-custom-engine-config | changeCustomEngineConfig | No | | | |
|
||||||
|
| engine:engine-modules-global-config-changed | onEngineModulesChanged | No | | | |
|
||||||
|
| engine-custom-macro-changed | onCustomMacroChanged | No | | | |
|
||||||
|
| query-engine-modules-profile | queryEngineModulesProfile | No | | | |
|
||||||
|
| filter-engine-modules | filterEngineModules | No | | | |
|
||||||
|
|
||||||
|
## information
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| query-information | queryInformation | No | | | |
|
||||||
|
| open-information-dialog | openInformationDialog | No | | | |
|
||||||
|
| has-dialog | hasDialog | No | | | |
|
||||||
|
| close-dialog | closeDialog | No | | | |
|
||||||
|
|
||||||
|
## menu
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| engine:engine-modules-global-config-changed | engineModuleChanged | No | | | |
|
||||||
|
| edit-mode:enter | modeChanged | No | | | |
|
||||||
|
| shortcuts:change | onShortcutChange | No | | | |
|
||||||
|
|
||||||
|
## messages
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| start-record | addListener | No | | | |
|
||||||
|
| stop-record | removeListener | No | | | |
|
||||||
|
| start-auto-save | startAutoSave | No | | | |
|
||||||
|
| stop-auto-save | stopAutoSave | No | | | |
|
||||||
|
| open | open | No | | | |
|
||||||
|
| open-debug | openDebug | No | | | |
|
||||||
|
| broadcast | debug.broadcast | No | | | |
|
||||||
|
| request | debug.request | No | | | |
|
||||||
|
| send | debug.send | No | | | |
|
||||||
|
| reply | debug.reply | No | | | |
|
||||||
|
| query-message-state | queryMessageState | No | | | |
|
||||||
|
|
||||||
|
## metrics
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| metrics:init | init | No | | | |
|
||||||
|
| metrics:trackEvent | trackEvent | No | | | |
|
||||||
|
| metrics:trackException | trackException | No | | | |
|
||||||
|
| metrics:trackProcessMemory | trackProcessMemory | No | | | |
|
||||||
|
| metrics:trackTimeStart | trackTimeStart | No | | | |
|
||||||
|
| metrics:trackTimeEnd | trackTimeEnd | No | | | |
|
||||||
|
| metrics:_trackEventWithTimer | _trackEventWithTimer | No | | | |
|
||||||
|
| metrics:_sendEventGroup | _sendEventGroup | No | | | |
|
||||||
|
| metrics:_trackCrashEvent | _trackCrashEvent | No | | | |
|
||||||
|
| query-google-v4-data | query-google-v4-data | No | | | |
|
||||||
|
| query-google-metrics-v4-html | query-google-metrics-v4-html | No | | | |
|
||||||
|
|
||||||
|
## placeholder
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| install-extension | installExtension | No | | | |
|
||||||
|
|
||||||
|
## preferences
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open-settings | openSettings | Yes | 打开偏好设置面板 | - tab {string} 需要打开的选项卡(功能插件的名称)<br>- ...args: {any[]} 打开选项卡带的其他参数<br><br>@returns {null} | await Editor.Message.request('preferences', 'open-settings'); |
|
||||||
|
| change-settings-tab | settings.changeTab | No | | | |
|
||||||
|
| refresh-settings-tab | settings.refreshTab | No | | | |
|
||||||
|
| query-settings-tab | settings.queryTab | No | | | |
|
||||||
|
| query-preferences-configs | queryPreferencesConfigs | No | | | |
|
||||||
|
| query-configs-from-path | queryConfigsFromPath | No | | | |
|
||||||
|
| query-config | queryConfig | Yes | 查询偏好配置 | - name {string} 插件或分类名<br>- path? {string} 配置路径<br>- type? {'default' \| 'global' \| 'local'} 配置类型<br><br>@returns {any} 返回配置数据 | await Editor.Message.request('preferences', 'query-config', 'preview', 'general', 'global'); |
|
||||||
|
| set-config | setConfig | Yes | 设置偏好配置 | - name {string} 插件名<br>- path {string} 配置路径<br>- value {any} 配置数据<br>- type? {'default' \| 'global' \| 'local'} 配置类型<br><br>@returns {boolean} 是否设置成功 | await Editor.Message.request('preferences', 'set-config', 'preview', 'general.auto_refresh', false, 'global'); |
|
||||||
|
| import-config | importConfig | No | | | |
|
||||||
|
| export-config | exportConfig | No | | | |
|
||||||
|
|
||||||
|
## preview
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open | open | No | | | |
|
||||||
|
| ready | ready | No | | | |
|
||||||
|
| generate-settings | generateSettings | No | | | |
|
||||||
|
| query-preview-url | queryPreviewUrl | No | | | |
|
||||||
|
| query-connect-num | queryConnectNum | No | | | |
|
||||||
|
| scene:save | currentSceneSave | No | | | |
|
||||||
|
| programming:compiled | programming:compiled | No | | | |
|
||||||
|
| programming:compile-start | programming:compile-start | No | | | |
|
||||||
|
| open-terminal | open-terminal | No | | | |
|
||||||
|
| restart-simulator | restart-simulator | No | | | |
|
||||||
|
| reload-terminal | reload-terminal | No | | | |
|
||||||
|
| get-preview-ip | get-preview-ip | No | | | |
|
||||||
|
| set-preview-ip | set-preview-ip | No | | | |
|
||||||
|
| create-template | create-template | No | | | |
|
||||||
|
| change-platform | change-platform | No | | | |
|
||||||
|
| programming:pack-build-end | on-pack-build-end | No | | | |
|
||||||
|
| build-simulator-engine-ts | buildSimulatorEngineTS | No | | | |
|
||||||
|
| preview-scene-in-browser | previewSceneInBrowser | No | | | |
|
||||||
|
| write-setting-file | writeSettingFile | No | | | |
|
||||||
|
| asset-db:asset-change | asset-db:asset-change | No | | | |
|
||||||
|
| build-worker:ready | build-worker:ready | No | | | |
|
||||||
|
| build-worker:closed | build-worker:closed | No | | | |
|
||||||
|
|
||||||
|
## program
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| query-program-info | queryProgramInfo | No | | | |
|
||||||
|
| query-program-config | queryProgramConfig | No | | | |
|
||||||
|
| open-program | openProgram | No | | | |
|
||||||
|
| execute-program | executeProgram | No | | | |
|
||||||
|
| open-url | openUrl | No | | | |
|
||||||
|
|
||||||
|
## programming
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| packer-driver/start | packer-driver/start | No | | | |
|
||||||
|
| packer-driver/get-loader-context | packer-driver/get-loader-context | No | | | |
|
||||||
|
| packer-driver/ready | packer-driver/ready | No | | | |
|
||||||
|
| packer-driver/update-auto-update-import-config | packer-driver/update-auto-update-import-config | No | | | |
|
||||||
|
| packer-driver/query-script-deps | packer-driver/query-script-deps | No | | | |
|
||||||
|
| packer-driver/query-script-users | packer-driver/query-script-users | No | | | |
|
||||||
|
| packer-driver/query-cc-editor-module-map | packer-driver/query-cc-editor-module-map | No | | | |
|
||||||
|
| query-shared-settings | query-shared-settings | No | | | |
|
||||||
|
| clear-code-cache | clear-code-cache | No | | | |
|
||||||
|
| open-dev-tools | open-dev-tools | No | | | |
|
||||||
|
| engine:engine-custom-macro-changed | custom-macro-changed | No | | | |
|
||||||
|
| query-sorted-plugins | querySortedPlugins | No | | | |
|
||||||
|
|
||||||
|
## project
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| asset-db:ready | settings.assetDbReady | No | | | |
|
||||||
|
| open-settings | openSettings | Yes | 打开项目设置面板 | - name {string} 要打开的选项卡所属的插件注册名称<br>- tab {string} 在注册功能时使用的键<br>- ...args: {any[]} 打开选项卡时附带的其他参数<br><br>@returns {null} | await Editor.Message.request('project', 'open-settings'); |
|
||||||
|
| open-joint | openJoint | No | | | |
|
||||||
|
| change-settings-tab | settings.changeTab | No | | | |
|
||||||
|
| refresh-settings-tab | settings.refreshTab | No | | | |
|
||||||
|
| query-settings-tab | settings.queryTab | No | | | |
|
||||||
|
| change-script-config | changeScriptConfig | No | | | |
|
||||||
|
| change-design-resolution | changeDesignResolution | No | | | |
|
||||||
|
| query-design-resolution | queryDesignResolution | No | | | |
|
||||||
|
| change-custom-layer | changeCustomLayer | No | | | |
|
||||||
|
| change-sorting-layer | changeSortingLayer | No | | | |
|
||||||
|
| change-hight-quality | changeHighQuality | No | | | |
|
||||||
|
| calc-joint-layout | calcJointLayouts | No | | | |
|
||||||
|
| query-project-configs | queryProjectConfigs | No | | | |
|
||||||
|
| query-configs-from-path | queryConfigsFromPath | No | | | |
|
||||||
|
| query-config | queryConfig | Yes | 查询项目配置 | - name {string} 插件名<br>- path? {string} 配置路径<br>- type? {'default' \| 'project'} 配置类型<br><br>@returns {any} 返回配置数据 | await Editor.Message.request('project', 'query-config', 'engine', 'modules'); |
|
||||||
|
| set-config | setConfig | Yes | 设置项目配置 | - name {string} 插件名<br>- path {string} 配置路径<br>- value {any} 配置数据<br><br>@returns {boolean} 设置成功与否 | await Editor.Message.request('project', 'set-config', 'project', 'general.downloadMaxConcurrency', 10); |
|
||||||
|
| import-config | importConfig | No | | | |
|
||||||
|
| export-config | exportConfig | No | | | |
|
||||||
|
|
||||||
|
## scene
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| query-scene-bounds | default.query-scene-bounds | No | | | |
|
||||||
|
| change-native-config | default.change-native-config | No | | | |
|
||||||
|
| debug-view | default.change-debug-view-option | No | | | |
|
||||||
|
| quit-editor | default.quit-editor | No | | | |
|
||||||
|
| scene:preview-stop | preview.onEditorPreviewStop | No | | | |
|
||||||
|
| open | open | No | | | |
|
||||||
|
| full-screen | default.fullScreen | No | | | |
|
||||||
|
| i18n:change | default.i18n:change | No | | | |
|
||||||
|
| scene:ready | onSceneReady, default.scene:ready | Yes | 场景打开通知 | - uuid {string} uuid of scene | Editor.Message.broadcast('scene:ready', assetUuid); |
|
||||||
|
| scene:close | default.scene:close, onSceneClosed | Yes | 场景关闭通知 | | Editor.Message.broadcast('scene:close'); |
|
||||||
|
| asset-db:ready | default.asset-db:ready | No | | | |
|
||||||
|
| asset-db:close | default.asset-db:close | No | | | |
|
||||||
|
| selection:hover | default.selection:hover | No | | | |
|
||||||
|
| selection:select | default.selection:select | No | | | |
|
||||||
|
| selection:unselect | default.selection:unselect | No | | | |
|
||||||
|
| asset-db:asset-add | default.asset-db:asset-add | No | | | |
|
||||||
|
| asset-db:asset-change | default.asset-db:asset-change | No | | | |
|
||||||
|
| asset-db:asset-delete | default.asset-db:asset-delete | No | | | |
|
||||||
|
| programming:pack-build-end | default.programming:pack-build-end | No | | | |
|
||||||
|
| project:change-design-resolution | default.project:change-design-resolution | No | | | |
|
||||||
|
| project:change-custom-layer | default.project:change-custom-layer | No | | | |
|
||||||
|
| project:change-sorting-layer | default.project:change-sorting-layer | No | | | |
|
||||||
|
| project:update-physics-group | default.project:update-physics-group | No | | | |
|
||||||
|
| project:change-high-quality | default.project:change-high-quality | No | | | |
|
||||||
|
| engine:engine-modules-global-config-changed | default.engine:engine-modules-global-config-changed | No | | | |
|
||||||
|
| open-devtools | default.open-devtools | No | | | |
|
||||||
|
| open-preview-devtools | default.open-preview-devtools | No | | | |
|
||||||
|
| graphical-tools | default.graphicalTools | No | | | |
|
||||||
|
| open-scene | default.open-scene | Yes | 打开场景 | - uuid {string} 场景资源的 UUID | await Editor.Message.request('scene', 'open-scene', sceneUuid); |
|
||||||
|
| load-empty-scene | default.load-empty-scene | No | | | |
|
||||||
|
| save-scene | default.save-scene | Yes | 保存场景 | | await Editor.Message.request('scene', 'save-scene'); |
|
||||||
|
| save-as-scene | default.save-as-scene | Yes | 场景另存为 | | await Editor.Message.request('scene', 'save-as-scene'); |
|
||||||
|
| close-scene | default.close-scene | Yes | 关闭场景 | | await Editor.Message.request('scene', 'close-scene'); |
|
||||||
|
| set-property | default.set-property | Yes | 设置某个元素内的属性 | - options {SetPropertyOptions}<br> - uuid {string} 修改属性的对象的 uuid<br> - path {string} 属性挂载对象的搜索路径<br> - dump {IProperty} 属性 dump 出来的数据 | await Editor.Message.request('scene', 'set-property', {<br> uuid: nodeUuid,<br> path: '__comps__.1.defaultClip',<br> dump: {<br> type: 'cc.AnimationClip',<br> value: {<br> uuid: animClipUuid,<br> },<br> },<br>}); |
|
||||||
|
| reset-property | default.reset-property | Yes | 重置元素属性到默认值 | - options {SetPropertyOptions}<br> - uuid {string} 修改属性的对象的 uuid<br> - path {string} 属性挂载对象的搜索路径 | await Editor.Message.request('scene', 'reset-property', {<br> uuid: nodeUuid,<br> path: 'position',<br>}); |
|
||||||
|
| preview-set-property | default.preview-set-property | No | | | |
|
||||||
|
| cancel-preview-set-property | default.cancel-preview-set-property | No | | | |
|
||||||
|
| update-property-from-null | default.update-property-from-null | No | | | |
|
||||||
|
| set-node-and-children-layer | default.set-node-and-children-layer | No | | | |
|
||||||
|
| move-array-element | default.move-array-element | Yes | 移动数组内某个元素的位置 | - options {MoveArrayOptions}<br> - uuid {string} 节点的 uuid<br> - path {string} 数组的搜索路径<br> - target {number} 目标 item 原来的索引<br> - offset {number} 偏移量<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'move-array-element', {<br> uuid: nodeUuid,<br> path: '__comps__',<br> target: 1,<br> offset: -1,<br>}); |
|
||||||
|
| remove-array-element | default.remove-array-element | Yes | 删除数组内某个元素的位置 | - options {MoveArrayOptions}<br> - uuid {string} 节点的 uuid<br> - path {string} 数组的搜索路径<br> - index {number} 目标 item 的索引<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'remove-array-element', {<br> uuid: nodeUuid,<br> path: '__comps__',<br> index: 0,<br>}); |
|
||||||
|
| select-all-nodes | default.select-all-nodes | No | | | |
|
||||||
|
| copy-node | default.copy-node | Yes | 拷贝节点,给下一步粘贴(创建)节点准备数据 | - uuids {string \| string[]} 节点的 uuid<br> <br> @returns {string \| string[]} 返回节点的 uuid | await Editor.Message.request('scene', 'copy-node', uuids); |
|
||||||
|
| duplicate-node | default.duplicate-node | Yes | 复制节点 | - uuids {string \| string[]} 节点的 uuid<br> <br> @returns {string \| string[]} 返回新节点的 uuid | await Editor.Message.request('scene', 'duplicate-node', uuids); |
|
||||||
|
| paste-node | default.paste-node | Yes | 粘贴节点 | - options {PasteNodeOptions}<br> - target {string} 目标节点 uuid<br> - uuids {string \| string[]} 被复制的节点 uuid<br> - keepWorldTransform {boolean} 是否保持新节点的世界坐标不变<br> <br> @returns {string \| string[]} 返回新节点的 uuid | await Editor.Message.request('scene', 'paste-node', {<br> target: nodeUuid,<br> uuids: nodeUuids,<br>}); |
|
||||||
|
| cut-node | default.cut-node | Yes | 剪切节点 | - uuids {string \| string[]} 节点的 uuid<br> <br> @returns {string \| string[]} 返回节点的 uuid | await Editor.Message.request('scene', 'cut-node', uuids); |
|
||||||
|
| set-parent | default.set-parent | Yes | 设置节点父级 | - options {CutNodeOptions}<br> - parent {string} 父节点 uuid<br> - uuids {string\|string[]} 需要设置的子节点 uuid<br> - keepWorldTransform {boolean} 是否保持新节点的世界坐标不变<br> <br> @returns {string \| string[]} 返回节点的 uuid | await Editor.Message.request('scene','set-parent', {<br> parent: nodeUuid,<br> uuids: nodeUuids,<br>}); |
|
||||||
|
| create-node | default.create-node | Yes | 创建节点 | - options {CreateNodeOptions}<br> - parent {string} 父节点 uuid<br> - components? {string[]} 组件名字<br> <br> - name? {string} 节点名字<br> - dump? {INode \| IScene} node 初始化应用的 dump 数据<br> - keepWorldTransform? {boolean} 是否保持新节点的世界坐标不变<br> - type? {string} 资源类型<br> - canvasRequired? {boolean} 是否需要有 cc.Canvas<br> - unlinkPrefab? {boolean} 是否要解绑为普通节点<br> - assetUuid? {string} asset uuid,从资源实例化节点<br> <br> 注意: 使用 assetUuid 从预制体创建节点时,无论 unlinkPrefab 传 false 还是不传,都不会自动建立 Prefab 关联。需额外调用 link-prefab 消息建立关联。<br> <br> @returns {string \| string[]} 返回新节点的 uuid | await Editor.Message.request('scene', 'create-node', {<br> name: 'New Node'<br> parent: nodeUuid,<br>}); |
|
||||||
|
| reset-node | default.reset-node | Yes | 重置节点的位置, 角度和缩放 | - uuid {string} 节点的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'reset-node', {<br> uuid: nodeUuid,<br>}); |
|
||||||
|
| reset-component | default.reset-component | Yes | 重置组件 | - options {ResetComponentOptions}<br> - uuid {string} 组件的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'reset-component', {<br> uuid: componentUuid,<br>}); |
|
||||||
|
| restore-prefab | default.restore-prefab | Yes | 使用预制体资源还原对应预制件节点(内置撤销记录) | - uuid {string} 节点的 uuid<br> - assetUuid {string} 资源的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'restore-prefab', nodeUuid, assetUuid); |
|
||||||
|
| remove-node | default.remove-node | Yes | 删除节点 | - options {RemoveNodeOptions}<br> - uuid: {string \| string[]} 节点的 uuid | await Editor.Message.request('scene', 'remove-node', { <br> uuid: nodeUuid<br>}); |
|
||||||
|
| create-component | default.create-component | Yes | 创建组件 | - options {CreateComponentOptions}<br> - uuid {string} 节点的 uuid<br> - component {string} 组件 classId (cid)(推荐方式) 或者 className 类名 | Editor.Message.request('scene', 'create-component', { <br> uuid: nodeUuid,<br> component: 'cc.Sprite'<br>}); |
|
||||||
|
| remove-component | default.remove-component | Yes | 删除组件 | - options {CreateComponentOptions}<br> - uuid {string} 节点的 uuid<br> - component {string} 组件 classId (cid)(推荐方式) 或者 className 类名 | await Editor.Message.request('scene', 'remove-component', { <br> uuid: componentUuid,<br>}); |
|
||||||
|
| execute-component-method | default.execute-component-method | Yes | 执行组件上的方法 | - options {ExecuteComponentMethodOptions}<br> - uuid {string} 组件的 uuid<br> - name {string} 方法名<br> - args {any[]} 参数 | await Editor.Message.request('scene', 'execute-component-method', {<br> uuid: componentUuid,<br> name: 'getNoisePreview',<br> args: [100, 100],<br>}); |
|
||||||
|
| execute-scene-script | default.execute-scene-script | Yes | 执行某个插件注册的方法 | - options {ExecuteSceneScriptMethodsOptions}<br> - name {string} 注册进来的插件名字<br> - method {string} 执行的方法名字<br> - args {any[]} 参数数组 | await Editor.Message.request('scene', 'execute-scene-script', {<br> name: 'animation-graph',<br> method: 'query',<br> args: [],<br>}); |
|
||||||
|
| snapshot | default.snapshot | Yes | 快照当前场景状态 | | await Editor.Message.request('scene', 'snapshot'); |
|
||||||
|
| snapshot-abort | default.snapshot-abort | Yes | 中止快照 | | await Editor.Message.request('scene', 'snapshot-abort'); |
|
||||||
|
| begin-recording | default.begin-recording | Yes | 开始记录节点 Undo 数据 | | const undoID = await Editor.Message.request('scene', 'begin-recording', nodeUuid); |
|
||||||
|
| end-recording | default.end-recording | Yes | 结束记录节点 Undo 数据 | | await Editor.Message.request('scene', 'end-recording', undoID); |
|
||||||
|
| cancel-recording | default.cancel-recording | Yes | 取消记录节点 Undo 数据 | | await Editor.Message.request('scene', 'cancel-recording', undoID); |
|
||||||
|
| undo | default.undo | No | | | |
|
||||||
|
| redo | default.redo | No | | | |
|
||||||
|
| soft-reload | default.soft-reload | Yes | 软刷新场景 | | await Editor.Message.request('scene', 'soft-reload'); |
|
||||||
|
| preview-material | default.preview-material | No | | | |
|
||||||
|
| light-probe-update-tetrahedron | default.light-probe-update-tetrahedron | No | | | |
|
||||||
|
| change-gizmo-tool | default.change-gizmo-tool | Yes | 更改 Gizmo 工具 | - name {string} 工具名字 'position' \| 'rotation' \| 'scale'\| 'rect' | await Editor.Message.request('scene', 'change-gizmo-tool', 'position'); |
|
||||||
|
| query-gizmo-tool-name | default.query-gizmo-tool-name | Yes | 获取当前 Gizmo 工具的名字 | @returns {string} 'position' \| 'rotation' \| 'scale' \| 'rect' | await Editor.Message.request('scene', 'query-gizmo-tool-name'); |
|
||||||
|
| change-gizmo-pivot | default.change-gizmo-pivot | Yes | 更改变换基准点 | - name {string} 变换基准点 'pivot' \| 'center' | await Editor.Message.request('scene', 'query-gizmo-pivot'); |
|
||||||
|
| query-gizmo-pivot | default.query-gizmo-pivot | Yes | 获取当前 Gizmo 基准点名字 | @returns {string} 'pivot' \| 'center' | await Editor.Message.request('scene', 'query-gizmo-pivot'); |
|
||||||
|
| query-gizmo-view-mode | default.query-gizmo-view-mode | Yes | 查询视图模式(查看/选择) | @return {string} 'view' \| 'select' | await Editor.Message.request('scene', 'query-gizmo-view-mode'); |
|
||||||
|
| change-gizmo-coordinate | default.change-gizmo-coordinate | Yes | 更改坐标系 | - type {string} 坐标系 'local' \| 'global' | await Editor.Message.request('scene', 'change-gizmo-coordinate', 'global'); |
|
||||||
|
| query-gizmo-coordinate | default.query-gizmo-coordinate | Yes | 获取当前坐标系名字 | @returns {string} 'local' \| 'global' | await Editor.Message.request('scene', 'query-gizmo-coordinate'); |
|
||||||
|
| change-is2D | default.change-is2D | Yes | 更改2D/3D视图模式 | - is2D {boolean} 2D/3D视图 | await Editor.Message.request('scene', 'change-is2D', true); |
|
||||||
|
| query-is2D | default.query-is2D | Yes | 获取当前视图模式 | @returns {boolean} true:2D, false:3D | await Editor.Message.request('scene', 'query-is2D'); |
|
||||||
|
| set-grid-visible | default.set-grid-visible | Yes | 显示/隐藏网格 | - visible {boolean} 显示/隐藏网格 | await Editor.Message.request('scene', 'set-grid-visible', false); |
|
||||||
|
| query-is-grid-visible | default.query-is-grid-visible | Yes | 查询网格显示状态 | @returns {boolean} true: visible, false: invisible | await Editor.Message.request('scene', 'query-is-grid-visible'); |
|
||||||
|
| set-icon-gizmo-3d | default.set-icon-gizmo-3d | Yes | 设置 IconGizmo 为 3D 或 2D 模式 | - is3D {boolean} 3D/2D IconGizmo | await Editor.Message.request('scene', 'set-icon-gizmo-3d', false); |
|
||||||
|
| query-is-icon-gizmo-3d | default.query-is-icon-gizmo-3d | Yes | 查询 IconGizmo 模式 | @returns {boolean} true: 3D, false: 2D | await Editor.Message.request('scene', 'query-is-icon-gizmo-3d'); |
|
||||||
|
| set-icon-gizmo-size | default.set-icon-gizmo-size | Yes | 设置 IconGizmo 的大小 | - size {number} IconGizmo 的大小 | await Editor.Message.request('scene', 'set-icon-gizmo-size', 60); |
|
||||||
|
| query-icon-gizmo-size | default.query-icon-gizmo-size | Yes | 查询 IconGizmo 的大小 | @returns {number} IconGizmo 的大小 | await Editor.Message.request('scene', 'query-icon-gizmo-size'); |
|
||||||
|
| query-transform-snap-configs | default.query-transform-snap-configs | No | | | |
|
||||||
|
| set-transform-snap-configs | default.set-transform-snap-configs | No | | | |
|
||||||
|
| query-rect-snapping-configs | default.query-rect-snapping-configs | No | | | |
|
||||||
|
| set-rect-snapping-configs | default.set-rect-snapping-configs | No | | | |
|
||||||
|
| focus-camera | default.focus-camera | Yes | 聚焦场景相机到节点上 | - uuids {string[] \| null} 节点 uuid | await Editor.Message.request('scene', 'focus-camera', nodeUuids); |
|
||||||
|
| align-with-view | default.align-with-view | Yes | 将场景相机位置与角度应用到选中节点上 | @returns {null} | await Editor.Message.request('scene', 'align-with-view'); |
|
||||||
|
| align-view-with-node | default.align-view-with-node | Yes | 将选中节点位置与角度应用到当前视角 | @returns {null} | await Editor.Message.request('scene', 'align-with-view-node'); |
|
||||||
|
| zoom-scene-view-down | default.zoom-scene-view-down | No | | | |
|
||||||
|
| zoom-scene-view-up | default.zoom-scene-view-up | No | | | |
|
||||||
|
| toggle-active-selected-node | default.toggle-active-selected-node | No | | | |
|
||||||
|
| toggle-active-unselected-node | default.toggle-active-unselected-node | No | | | |
|
||||||
|
| toggle-active-all-nodes | default.toggle-active-all-nodes | No | | | |
|
||||||
|
| apply-material | default.apply-material | No | | | |
|
||||||
|
| query-physics-material | default.query-physics-material | No | | | |
|
||||||
|
| change-physics-material | default.change-physics-material | No | | | |
|
||||||
|
| apply-physics-material | default.apply-physics-material | No | | | |
|
||||||
|
| query-animation-graph-variant | default.query-animation-graph-variant | No | | | |
|
||||||
|
| change-animation-graph-variant | default.change-animation-graph-variant | No | | | |
|
||||||
|
| apply-animation-graph-variant | default.apply-animation-graph-variant | No | | | |
|
||||||
|
| query-animation-mask | default.query-animation-mask | No | | | |
|
||||||
|
| change-animation-mask | default.change-animation-mask | No | | | |
|
||||||
|
| apply-animation-mask | default.apply-animation-mask | No | | | |
|
||||||
|
| apply-render-texture | default.apply-render-texture | No | | | |
|
||||||
|
| copy-camera-data-to-nodes | default.copy-camera-data-to-nodes | No | | | |
|
||||||
|
| record-animation | default.record-animation | No | | | |
|
||||||
|
| change-animation-root | default.change-animation-root | No | | | |
|
||||||
|
| set-edit-time | default.set-edit-time | No | | | |
|
||||||
|
| change-clip-state | default.change-clip-state | No | | | |
|
||||||
|
| change-edit-clip | default.change-edit-clip | No | | | |
|
||||||
|
| save-clip | default.save-clip | No | | | |
|
||||||
|
| animation-operation | default.animation-operation | No | | | |
|
||||||
|
| create-prefab | default.create-prefab | No | 创建预制体资源(内置撤销记录) | | |
|
||||||
|
| getdata-prefab | default.getdata-prefab | No | | | |
|
||||||
|
| link-prefab | default.link-prefab | No | 将节点关联到预制体资源,建立 Prefab 关联关系 | - uuid {string} 节点的 uuid<br> - assetUuid {string} 预制体资源的 uuid<br> <br> @returns {boolean} 操作是否成功 | await Editor.Message.request('scene', 'link-prefab', nodeUuid, assetUuid); |
|
||||||
|
| unlink-prefab | default.unlink-prefab | No | | | |
|
||||||
|
| apply-prefab | default.apply-prefab | No | 应用预制体节点修改到对应资源(内置撤销记录) | | |
|
||||||
|
| apply-removed-component | default.apply-removed-component | No | 应用预制体删除组件的修改到对应资源(内置撤销记录) | | |
|
||||||
|
| revert-removed-component | default.revert-removed-component | No | 还原预制体节点被移除的组件(内置撤销记录) | | |
|
||||||
|
| query-is-ready | default.query-is-ready | Yes | 查询当前场景是否准备就绪 | | await Editor.Message.request('scene', 'query-is-ready'); |
|
||||||
|
| query-node | default.query-node | Yes | 查询一个节点的数据 | - uuid {string} 节点的 uuid<br> <br> @returns {Object} 节点的 dump 数据 | await Editor.Message.request('scene', 'query-node', nodeUuid); |
|
||||||
|
| query-component | default.query-component | Yes | 查询一个组件的数据 | - uuid {string} 组件的 uuid<br> <br> @returns {Object} 组件的 dump 数据 | await Editor.Message.request('scene', 'query-component', nodeUuid); |
|
||||||
|
| query-node-tree | default.query-node-tree | Yes | 查询节点树的信息 | - uuid? {string} 根节点 uuid,不传入则以场景节点为根节点<br> <br> @returns {Object}<br> - name {string} 节点名字或者 'scene'<br> - active {boolean} 节点激活状态 <br> - type {string} cc.Scene or cc.Node<br> - uuid {string} 节点的 uuid<br> - children {[]} 子节点数组<br> - prefab {number} prefab状态, 1 表示是 prefab, 2 表示是 prefab 但丢失资源<br> - isScene {boolean} 是否是场景节点<br> - components {[Object]} 组件数组<br> - type {string} 组件类型<br> - value {string} 组件的 uuid <br> - extends {[string]} 组件的继承链数组 | await Editor.Message.request('scene', 'query-node-tree', nodeUuid); |
|
||||||
|
| query-nodes-by-asset-uuid | default.query-nodes-by-asset-uuid | Yes | 查询使用了资源 UUID 的节点 | - 查询使用了资源 UUID 的节点<br> <br> @returns {string[]} 节点的 uuid | await Editor.Message.request('scene', 'query-nodes-by-asset-uuid', assetUuid); |
|
||||||
|
| query-nodes-miss-assets | default.query-nodes-miss-assets | No | | | |
|
||||||
|
| query-component-function-of-node | default.query-component-function-of-node | No | | | |
|
||||||
|
| query-all-effects | default.query-all-effects | No | | | |
|
||||||
|
| query-material | default.query-material | No | | | |
|
||||||
|
| query-effect | default.query-effect | No | | | |
|
||||||
|
| query-serialized-material | default.query-serialized-material | No | | | |
|
||||||
|
| query-render-pipeline | default.query-render-pipeline | No | | | |
|
||||||
|
| change-render-pipeline | default.change-render-pipeline | No | | | |
|
||||||
|
| apply-render-pipeline | default.apply-render-pipeline | No | | | |
|
||||||
|
| query-creatable-asset-types | default.query-creatable-asset-types | No | | | |
|
||||||
|
| query-scene-json | default.query-scene-json | No | | | |
|
||||||
|
| query-current-scene | default.query-current-scene | No | | | |
|
||||||
|
| query-dirty | default.query-dirty | Yes | 查询当前场景是否有修改 | | await Editor.Message.request('scene', 'query-dirty'); |
|
||||||
|
| query-classes | default.query-classes | Yes | 查询所有在引擎中注册的类 | @returns {[Object]}<br> - extends? {string} 过滤出基于此类名扩展而来的类 | await Editor.Message.request('scene', 'query-classes'); |
|
||||||
|
| query-components | default.query-components | Yes | 查询当前场景的所有组件 | @returns {[Object]}<br> - name {string} 组件名字<br> - path {string} 菜单路径 | await Editor.Message.request('scene', 'query-components'); |
|
||||||
|
| query-component-has-script | default.query-component-has-script | Yes | 查询引擎组件列表是否含有指定类名的脚本 | - name 脚本的类名 Class<br> <br> @returns {boolean} 存在 true, 不存在 false | await Editor.Message.request('scene', 'query-component-has-script', 'cc.Sprite'); |
|
||||||
|
| query-script-name | default.query-script-name | No | | | |
|
||||||
|
| query-script-cid | default.query-script-cid | No | | | |
|
||||||
|
| query-layer-builtin | default.query-layer-builtin | No | | | |
|
||||||
|
| query-sorting-layer-builtin | default.query-sorting-layer-builtin | No | | | |
|
||||||
|
| query-scene-mode | default.query-scene-mode | No | | | |
|
||||||
|
| query-animation-state | default.query-animation-state | No | | | |
|
||||||
|
| query-current-animation-info | default.query-current-animation-info | No | | | |
|
||||||
|
| query-animation-root | default.query-animation-root | No | | | |
|
||||||
|
| query-animation-root-info | default.query-animation-root-info | No | | | |
|
||||||
|
| query-animation-edit-info | default.query-animation-edit-info | No | | | |
|
||||||
|
| query-animation-clip | default.query-animation-clip | No | | | |
|
||||||
|
| query-animation-properties | default.query-animation-properties | No | | | |
|
||||||
|
| query-animation-clips-info | default.query-animation-clips-info | No | | | |
|
||||||
|
| query-animation-clips-time | default.query-animation-clips-time | No | | | |
|
||||||
|
| query-property-value-at-frame | default.query-property-value-at-frame | No | | | |
|
||||||
|
| query-aux-curve-value-at-frame | default.query-aux-curve-value-at-frame | No | | | |
|
||||||
|
| query-auxiliary-curves | default.query-auxiliary-curves | No | | | |
|
||||||
|
| generate-available-name | default.generate-available-name | No | | | |
|
||||||
|
| editor-preview-set-play | default.editor-preview-set-play | No | | | |
|
||||||
|
| editor-refresh-gizmo-config | default.editor-refresh-gizmo-config | No | | | |
|
||||||
|
| editor-preview-call-method | default.editor-preview-call-method | No | | | |
|
||||||
|
| editor-preview-change-style-web | default.editor-preview-change-style | No | | | |
|
||||||
|
| editor-preview-change-style-native | preview.editorPreviewChangeStyle | No | | | |
|
||||||
|
| editor-preview-change-config | default.editor-preview-change-config, preview.editorPreviewChangeConfig | No | | | |
|
||||||
|
| scene:show-loading | default.scene:show-loading | No | | | |
|
||||||
|
| scene:hide-loading | default.scene:hide-loading | No | | | |
|
||||||
|
| execute-model-preview-animation-operation | default.execute-model-preview-animation-operation | No | | | |
|
||||||
|
| regenerate-polygon-2d-points | default.regenerate-polygon-2d-points | No | | | |
|
||||||
|
| export-particle-plist | default.export-particle-plist | No | | | |
|
||||||
|
| change-node-lock | default.change-node-lock | No | | | |
|
||||||
|
| unit-test | default.unit-test | No | | | |
|
||||||
|
| change-target-resolution | default.change-target-resolution | No | | | |
|
||||||
|
| query-latest-cache | queryLatestCache | No | | | |
|
||||||
|
| clear-scene-cache | clearSceneCache | No | | | |
|
||||||
|
| save-scene-cache-to-file | saveSceneCacheToFile | No | | | |
|
||||||
|
| update-cache-config | updateCacheConfig | No | | | |
|
||||||
|
| query-enum-list-with-path | default.query-enum-list-with-path | No | | | |
|
||||||
|
| query-wrap-mode-enum-list | default.query-wrap-mode-enum-list | No | | | |
|
||||||
|
| set-scene-light-on | default.set-scene-light-on | No | | | |
|
||||||
|
| query-scene-light-on | default.query-scene-light-on | No | | | |
|
||||||
|
| native-ipc | default.native-ipc | No | | | |
|
||||||
|
| native-scene | default.native-scene | No | | | |
|
||||||
|
| panel-browser | panelToBrowser | No | | | |
|
||||||
|
| browser-panel | default.on-browser-panel | No | | | |
|
||||||
|
| is-native | default.query-is-native | No | | | |
|
||||||
|
| query-preview-data | default.query-preview-data | No | | | |
|
||||||
|
| call-preview-function | default.call-preview-function | No | | | |
|
||||||
|
| query-thumbnail | default.query-thumbnail | No | | | |
|
||||||
|
| query-preview-game-view-data | preview.getGameViewData | No | | | |
|
||||||
|
| set-preview-window-visible | preview.setPreviewWindowVisible, setPreviewWindowVisible | No | | | |
|
||||||
|
| create-preview-window | preview.createPreviewWindow | No | | | |
|
||||||
|
| resize-native-window | resizeNativeWindow | No | | | |
|
||||||
|
| create-native-window | createNativeWindow | No | | | |
|
||||||
|
| preview-panel-ready | onPreviewPanelReady | No | | | |
|
||||||
|
| preview-panel-stop | onPreviewPanelStop | No | | | |
|
||||||
|
| toggle-light-probe-edit-mode | default.toggle-light-probe-edit-mode | No | | | |
|
||||||
|
| query-light-probe-edit-mode | default.query-light-probe-edit-mode | No | | | |
|
||||||
|
| toggle-light-probe-bounding-box-edit-mode | default.toggle-light-probe-bounding-box-edit-mode | No | | | |
|
||||||
|
| query-light-probe-bounding-box-edit-mode | default.query-light-probe-bounding-box-edit-mode | No | | | |
|
||||||
|
| scene:light-probe-edit-mode-changed | | Yes | 光照探针编辑模式切换通知 | - mode {boolean} 切换后的探针编辑模式 | Editor.Message.broadcast('scene:light-probe-edit-mode-changed', true); |
|
||||||
|
| scene:light-probe-bounding-box-edit-mode-changed | | Yes | 光照探针组件包围盒编辑模式切换通知 | - mode {boolean} 切换后的探针组件包围盒编辑模式 | Editor.Message.broadcast('scene:light-probe-bounding-box-edit-mode-changed', true); |
|
||||||
|
| lod-apply-current-camera-size | default.lod-apply-current-camera-size | No | | | |
|
||||||
|
| lod-insert | default.lod-insert | No | | | |
|
||||||
|
| lod-erase | default.lod-erase | No | | | |
|
||||||
|
| shortcuts:change | default.shortcuts:change | No | | | |
|
||||||
|
| window:zoom-level-change | default.window:zoom-level-change | No | | | |
|
||||||
|
| window:focus-zoom-level-change | default.window:focus-zoom-level-change | No | | | |
|
||||||
|
| multi-open-scene | default.multi-open-scene | No | | | |
|
||||||
|
| multi-close-scene | default.multi-close-scene | No | | | |
|
||||||
|
| multi-scene-dirty | default.multi-scene-dirty | No | | | |
|
||||||
|
| multi-scene-query | default.multi-scene-query | No | | | |
|
||||||
|
| multi-scene-focus | default.multi-scene-focus | No | | | |
|
||||||
|
| multi-scene-focus-query | default.multi-scene-focus-query | No | | | |
|
||||||
|
| multi-save-all-scene | default.multi-save-all-scene | No | | | |
|
||||||
|
| multi-is-multi-edit-mode | default.multi-is-multi-edit-mode | No | | | |
|
||||||
|
| multi-close-to-the-right | default.multi-close-to-the-right | No | | | |
|
||||||
|
| multi-move-tabs-to | default.multi-move-tabs-to | No | | | |
|
||||||
|
| multi-close-others | default.multi-close-others | No | | | |
|
||||||
|
|
||||||
|
## server
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| query-ip-list | queryIPList | Yes | 查询 IP 列表 | @returns {string[]} - IP 列表 | await Editor.Message.request('server', 'query-ip-list'); |
|
||||||
|
| query-https-enabled | queryHTTPSEnabled | No | | | |
|
||||||
|
| query-sort-ip-list | querySortIpList | Yes | 获取排序后的 ip 列表 | @returns {string[]} - 排序后的 IP 列表 | await Editor.Message.request('server', 'query-sort-ip-list'); |
|
||||||
|
| query-port | queryPort | Yes | 查询编辑器服务器当前启动的端口号 | @returns {number} - 端口号 | await Editor.Message.request('server', 'query-port'); |
|
||||||
|
| scan-lan | scanLAN | No | | | |
|
||||||
|
| change-preview-port | change-preview-port | No | | | |
|
||||||
|
| change-https-options | change-https-options | No | | | |
|
||||||
|
|
||||||
|
## shortcuts
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open | open | No | | | |
|
||||||
|
| query-shortcut-map | queryShortcutMap | No | | | |
|
||||||
|
| change-shortcut | changeShortcut | No | | | |
|
||||||
|
| change-tab | default.changeTab | No | | | |
|
||||||
|
| reset-shortcut | resetShortcut | No | | | |
|
||||||
|
| query-packages-shortcut-list | queryPackagesShortcutList | No | | | |
|
||||||
|
| remove-custom-shortcut | removeCustomShortcut | No | | | |
|
||||||
|
| shortcuts:change | default.onShortcutChanged | No | | | |
|
||||||
|
|
||||||
|
## tester
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open | open | No | | | |
|
||||||
|
| forwarding-to-window | forwarding-to-window | No | | | |
|
||||||
|
| * | panel.* | No | | | |
|
||||||
|
| auto-test | auto-test | No | | | |
|
||||||
|
|
||||||
|
## utils
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| export-dts | exportDTS | No | | | |
|
||||||
|
| tester-tag | testerTag | No | | | |
|
||||||
|
|
||||||
|
## window
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| focus-window-zoom-in | onFocusWindowZoomIn | No | | | |
|
||||||
|
| focus-window-zoom-out | onFocusWindowZoomOut | No | | | |
|
||||||
|
| focus-window-zoom-to-initial | onFocusWindowZoomToInitial | No | | | |
|
||||||
|
| window-zoom-level-change | onWindowZoomLevelChange | No | | | |
|
||||||
|
|
||||||
|
## cocos-service
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| open-service-panel | openServicePanel | No | | | |
|
||||||
|
| enable-service | enableService | No | | | |
|
||||||
|
| open-popup | openPopup | No | | | |
|
||||||
|
| close-popup | closePopup | No | | | |
|
||||||
|
| print-log | printLog | No | | | |
|
||||||
|
| api-request | apiRequest | No | | | |
|
||||||
|
| popup-show | popupShow | No | | | |
|
||||||
|
| plugin-msg | pluginMsg | No | | | |
|
||||||
|
| plugin-msg-service | default.pluginMsg | No | | | |
|
||||||
|
| plugin-msg-popup | simple.pluginMsg, dockable.pluginMsg | No | | | |
|
||||||
|
| service-ready | serviceReady | No | | | |
|
||||||
|
| i18n:change | i18nChange | No | | | |
|
||||||
|
| service-status-changed | onServiceStatusChanged | No | | | |
|
||||||
|
| drop-scene | dropScene | No | | | |
|
||||||
|
| drop-hierarchy | dropHierarchy | No | | | |
|
||||||
|
| drop-assets | dropAssets | No | | | |
|
||||||
|
|
||||||
|
## im-plugin
|
||||||
|
|
||||||
|
| Message | Methods | Public | Description | Doc | Example |
|
||||||
|
|---------|---------|--------|-------------|-----|---------|
|
||||||
|
| show-panel | showPanel | No | | | |
|
||||||
|
| silent-check-and-update | silentCheckAndUpdate | No | | | |
|
||||||
|
| im-plugin:check | default.check | No | | | |
|
||||||
|
| silent-check | silentCheck | No | | | |
|
||||||
|
| close-panel | closePanel | No | | | |
|
||||||
|
| whether-close | whetherClose | No | | | |
|
||||||
|
| get-sign-in-url | getSignInUrl | No | | | |
|
||||||
|
| get-redirect-url | getRedirectUrl | No | | | |
|
||||||
|
| update-toolbar | updateToolbarMain | No | | | |
|
||||||
|
| save-toolbar-info | saveToolbarInfo | No | | | |
|
||||||
|
| get-saved-toolbar-info | getSavedToolbarInfo | No | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Message 调用方式
|
||||||
|
|
||||||
|
在扩展中可以通过以下方式发送 message:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 发送消息 (等待返回结果)
|
||||||
|
const result = await Editor.Message.request(packageName, messageName, ...args);
|
||||||
|
|
||||||
|
// 发送消息 (不等待返回结果)
|
||||||
|
Editor.Message.send(packageName, messageName, ...args);
|
||||||
|
|
||||||
|
// 广播消息
|
||||||
|
Editor.Message.broadcast(messageName, ...args);
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `packageName` 为上述各表格的扩展包名称 (如 `asset-db`, `scene`, `builder` 等)。
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { normalizeSavedToolProfiles } = require('./tool-profiles');
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
@@ -12,9 +14,12 @@ const DEFAULTS = {
|
|||||||
enabledToolCategories: [],
|
enabledToolCategories: [],
|
||||||
disabledToolCategories: [],
|
disabledToolCategories: [],
|
||||||
enableSessions: false,
|
enableSessions: false,
|
||||||
|
executeJavascriptSafetyChecks: true,
|
||||||
autostart: true,
|
autostart: true,
|
||||||
maxInteractionLogEntries: 50,
|
maxInteractionLogEntries: 50,
|
||||||
lastClientTargetId: 'claude_code',
|
lastClientTargetId: 'claude_code',
|
||||||
|
activeToolProfileName: '',
|
||||||
|
savedToolProfiles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProjectPath() {
|
function getProjectPath() {
|
||||||
@@ -28,6 +33,19 @@ function getProjectName() {
|
|||||||
return path.basename(getProjectPath());
|
return path.basename(getProjectPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProjectIdentityPath(projectPath) {
|
||||||
|
const normalized = path.resolve(String(projectPath || process.cwd())).replace(/\\/g, '/');
|
||||||
|
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectIdentity(projectPath = getProjectPath()) {
|
||||||
|
return crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(`funplay-cocos-mcp:${normalizeProjectIdentityPath(projectPath)}`)
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, 24);
|
||||||
|
}
|
||||||
|
|
||||||
function getCocosVersion() {
|
function getCocosVersion() {
|
||||||
if (global.Editor && Editor.App) {
|
if (global.Editor && Editor.App) {
|
||||||
if (typeof Editor.App.version === 'string' && Editor.App.version) {
|
if (typeof Editor.App.version === 'string' && Editor.App.version) {
|
||||||
@@ -106,11 +124,16 @@ function loadConfig() {
|
|||||||
enabledToolCategories: normalizeStringList(fileConfig.enabledToolCategories).map((item) => item.toLowerCase()),
|
enabledToolCategories: normalizeStringList(fileConfig.enabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
disabledToolCategories: normalizeStringList(fileConfig.disabledToolCategories).map((item) => item.toLowerCase()),
|
disabledToolCategories: normalizeStringList(fileConfig.disabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
enableSessions: typeof fileConfig.enableSessions === 'boolean' ? fileConfig.enableSessions : DEFAULTS.enableSessions,
|
enableSessions: typeof fileConfig.enableSessions === 'boolean' ? fileConfig.enableSessions : DEFAULTS.enableSessions,
|
||||||
|
executeJavascriptSafetyChecks: typeof fileConfig.executeJavascriptSafetyChecks === 'boolean'
|
||||||
|
? fileConfig.executeJavascriptSafetyChecks
|
||||||
|
: DEFAULTS.executeJavascriptSafetyChecks,
|
||||||
autostart: typeof fileConfig.autostart === 'boolean' ? fileConfig.autostart : DEFAULTS.autostart,
|
autostart: typeof fileConfig.autostart === 'boolean' ? fileConfig.autostart : DEFAULTS.autostart,
|
||||||
maxInteractionLogEntries: Number.isInteger(fileConfig.maxInteractionLogEntries)
|
maxInteractionLogEntries: Number.isInteger(fileConfig.maxInteractionLogEntries)
|
||||||
? Math.max(10, Math.min(500, fileConfig.maxInteractionLogEntries))
|
? Math.max(10, Math.min(500, fileConfig.maxInteractionLogEntries))
|
||||||
: DEFAULTS.maxInteractionLogEntries,
|
: DEFAULTS.maxInteractionLogEntries,
|
||||||
lastClientTargetId: normalizeClientTargetId(fileConfig.lastClientTargetId),
|
lastClientTargetId: normalizeClientTargetId(fileConfig.lastClientTargetId),
|
||||||
|
activeToolProfileName: typeof fileConfig.activeToolProfileName === 'string' ? fileConfig.activeToolProfileName : '',
|
||||||
|
savedToolProfiles: normalizeSavedToolProfiles(fileConfig.savedToolProfiles),
|
||||||
configPath,
|
configPath,
|
||||||
configError: fileConfig.__error || '',
|
configError: fileConfig.__error || '',
|
||||||
};
|
};
|
||||||
@@ -120,6 +143,7 @@ module.exports = {
|
|||||||
DEFAULTS,
|
DEFAULTS,
|
||||||
getProjectPath,
|
getProjectPath,
|
||||||
getProjectName,
|
getProjectName,
|
||||||
|
getProjectIdentity,
|
||||||
getCocosVersion,
|
getCocosVersion,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
normalizeProfile,
|
normalizeProfile,
|
||||||
|
|||||||
+5
-1
@@ -68,7 +68,11 @@ function findTsConfig(projectPath, explicitPath) {
|
|||||||
|
|
||||||
function runExec(file, args, cwd) {
|
function runExec(file, args, cwd) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
execFile(file, args, { cwd, maxBuffer: 8 * 1024 * 1024 }, (error, stdout, stderr) => {
|
const options = { cwd, maxBuffer: 8 * 1024 * 1024 };
|
||||||
|
if (process.platform === 'win32' && /\.(cmd|bat)$/.test(file)) {
|
||||||
|
options.shell = true;
|
||||||
|
}
|
||||||
|
execFile(file, args, options, (error, stdout, stderr) => {
|
||||||
resolve({
|
resolve({
|
||||||
code: error && typeof error.code === 'number' ? error.code : 0,
|
code: error && typeof error.code === 'number' ? error.code : 0,
|
||||||
stdout: stdout || '',
|
stdout: stdout || '',
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { isPathInside } = require('./path-safety');
|
||||||
|
|
||||||
|
const DELETE_METHOD_PATTERN = /\bfs(?:\s*\.\s*promises)?\s*\.\s*(rm|rmdir|unlink|truncate|rmSync|rmdirSync|unlinkSync|truncateSync)\s*\(/;
|
||||||
|
const WRITE_STREAM_PATTERN = /\bfs\s*\.\s*(createWriteStream|openSync)\s*\(/;
|
||||||
|
const SHELL_PATTERN = /require\s*\(\s*['"]child_process['"]\s*\)|\bchild_process\s*\.|\b(exec|execFile|spawn|fork|execSync|execFileSync|spawnSync)\s*\(/;
|
||||||
|
const WRITE_METHOD_PATTERN = /\bfs(?:\s*\.\s*promises)?\s*\.\s*(writeFile|appendFile|copyFile|cp|rename|mkdir|writeFileSync|appendFileSync|copyFileSync|cpSync|renameSync|mkdirSync)\s*\(/;
|
||||||
|
const HOME_PATH_PATTERN = /(?:^~(?:\/|\\|$)|\$HOME|%USERPROFILE%|%HOMEPATH%)/i;
|
||||||
|
const TRAVERSAL_PATTERN = /(^|[\\/])\.\.([\\/]|$)/;
|
||||||
|
|
||||||
|
function extractStringLiterals(code) {
|
||||||
|
const literals = [];
|
||||||
|
const pattern = /(['"`])((?:\\[\s\S]|(?!\1)[\s\S])*?)\1/g;
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(String(code || '')))) {
|
||||||
|
literals.push(match[2]);
|
||||||
|
}
|
||||||
|
return literals;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteLiteral(value) {
|
||||||
|
return path.isAbsolute(value)
|
||||||
|
|| path.win32.isAbsolute(value)
|
||||||
|
|| /^\\\\/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteLiteralInsideProject(projectPath, value) {
|
||||||
|
if (!projectPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.win32.isAbsolute(value)) {
|
||||||
|
const root = projectPath.replace(/\//g, '\\');
|
||||||
|
const relative = path.win32.relative(root, value);
|
||||||
|
return relative === '' || (relative && !relative.startsWith('..') && !path.win32.isAbsolute(relative));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.isAbsolute(value)) {
|
||||||
|
return isPathInside(projectPath, path.resolve(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectJavascriptSafety(code, options = {}) {
|
||||||
|
const source = String(code || '');
|
||||||
|
const projectPath = options.projectPath ? path.resolve(String(options.projectPath)) : '';
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
if (DELETE_METHOD_PATTERN.test(source)) {
|
||||||
|
violations.push('direct fs delete/truncate calls are blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WRITE_STREAM_PATTERN.test(source)) {
|
||||||
|
violations.push('raw writable file streams are blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SHELL_PATTERN.test(source)) {
|
||||||
|
violations.push('child_process execution is blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFileMutation = DELETE_METHOD_PATTERN.test(source)
|
||||||
|
|| WRITE_METHOD_PATTERN.test(source)
|
||||||
|
|| WRITE_STREAM_PATTERN.test(source);
|
||||||
|
if (hasFileMutation && /\bos\s*\.\s*homedir\s*\(/.test(source)) {
|
||||||
|
violations.push('file mutations derived from os.homedir() are blocked by default');
|
||||||
|
}
|
||||||
|
if (hasFileMutation && /\bprocess\s*\.\s*env\s*\.\s*(HOME|USERPROFILE|HOMEPATH|APPDATA|LOCALAPPDATA|TMP|TEMP)\b/.test(source)) {
|
||||||
|
violations.push('file mutations derived from user/system environment paths are blocked by default');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const literal of extractStringLiterals(source)) {
|
||||||
|
if (HOME_PATH_PATTERN.test(literal)) {
|
||||||
|
violations.push(`user-home path literal is blocked: ${literal}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TRAVERSAL_PATTERN.test(literal)) {
|
||||||
|
violations.push(`path traversal literal is blocked: ${literal}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAbsoluteLiteral(literal)) {
|
||||||
|
if (!isAbsoluteLiteralInsideProject(projectPath, literal)) {
|
||||||
|
violations.push(`absolute path outside the Cocos project is blocked: ${literal}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: violations.length === 0,
|
||||||
|
violations: Array.from(new Set(violations)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertJavascriptSafety(code, options = {}) {
|
||||||
|
const result = inspectJavascriptSafety(code, options);
|
||||||
|
if (result.ok) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'JavaScript safety checks blocked this code: ' +
|
||||||
|
`${result.violations.join('; ')}. ` +
|
||||||
|
'Use project-relative helper/file tools, or pass safety_checks=false only after reviewing the risk.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
assertJavascriptSafety,
|
||||||
|
inspectJavascriptSafety,
|
||||||
|
};
|
||||||
+27
-1
@@ -169,7 +169,9 @@ async function duplicatePrefab(projectPath, options = {}) {
|
|||||||
throw new Error(`Prefab source file was not found: ${source}`);
|
throw new Error(`Prefab source file was not found: ${source}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetPath = resolveProjectPath(projectPath, target.endsWith('.prefab') ? target : `${target}.prefab`);
|
const rawTarget = target.endsWith('.prefab') ? target : `${target}.prefab`;
|
||||||
|
const targetUnderAssets = rawTarget.replace(/^assets[\\/]/, '');
|
||||||
|
const targetPath = resolveProjectPath(projectPath, path.join('assets', targetUnderAssets));
|
||||||
const assetsRoot = path.join(projectPath, 'assets');
|
const assetsRoot = path.join(projectPath, 'assets');
|
||||||
const relativeToAssets = path.relative(assetsRoot, targetPath);
|
const relativeToAssets = path.relative(assetsRoot, targetPath);
|
||||||
if (relativeToAssets.startsWith('..') || path.isAbsolute(relativeToAssets)) {
|
if (relativeToAssets.startsWith('..') || path.isAbsolute(relativeToAssets)) {
|
||||||
@@ -259,8 +261,32 @@ async function revertPrefabInstance(nodeUuid) {
|
|||||||
throw lastError || new Error('No prefab revert editor message was available.');
|
throw lastError || new Error('No prefab revert editor message was available.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createPrefab(options = {}) {
|
||||||
|
const nodeUuid = String(options.nodeUuid || '').trim();
|
||||||
|
const savePath = String(options.savePath || '').trim();
|
||||||
|
if (!nodeUuid) {
|
||||||
|
throw new Error('nodeUuid is required.');
|
||||||
|
}
|
||||||
|
if (!savePath) {
|
||||||
|
throw new Error('savePath is required.');
|
||||||
|
}
|
||||||
|
if (!savePath.endsWith('.prefab')) {
|
||||||
|
throw new Error('savePath must end with .prefab');
|
||||||
|
}
|
||||||
|
// Note: 'create-prefab' is not a public message in Cocos Creator 3.8.8
|
||||||
|
// (Public: No in editor-messages-3.8.8.md). It works but may change in future versions.
|
||||||
|
const result = await requestEditorMessage('scene', 'create-prefab', nodeUuid, savePath);
|
||||||
|
return {
|
||||||
|
created: true,
|
||||||
|
nodeUuid,
|
||||||
|
savePath,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
applyPrefabInstance,
|
applyPrefabInstance,
|
||||||
|
createPrefab,
|
||||||
duplicatePrefab,
|
duplicatePrefab,
|
||||||
editPrefabJson,
|
editPrefabJson,
|
||||||
inspectPrefab,
|
inspectPrefab,
|
||||||
|
|||||||
@@ -146,8 +146,26 @@ function createProjectSkill(projectPath, options = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCocosMcpProjectSkill(projectPath, options = {}) {
|
||||||
|
return createProjectSkill(projectPath, {
|
||||||
|
skillName: options.skillName || 'funplay-cocos-mcp-workflow',
|
||||||
|
title: options.title || 'Funplay Cocos MCP Workflow',
|
||||||
|
description: options.description || 'Use this skill when editing, validating, or debugging this Cocos Creator project through Funplay Cocos MCP.',
|
||||||
|
overwrite: options.overwrite !== false,
|
||||||
|
instructions: String(options.instructions || '').trim() || [
|
||||||
|
'- Start by reading `cocos://project/context` or calling `get_editor_state` to confirm the active project, scene, server URL, and tool profile.',
|
||||||
|
'- Prefer `execute_javascript` for high-level scene/editor orchestration, but keep safety checks enabled unless the code was reviewed.',
|
||||||
|
'- Use focused tools when they are better primitives: `list_assets`, `inspect_asset_dependencies`, `validate_asset_dependencies`, `run_script_diagnostics`, `get_script_diagnostic_context`, and screenshot tools.',
|
||||||
|
'- For UI work, inspect the active Canvas/hierarchy first, mutate the smallest necessary node/component set, then verify with `validate_scene` and a screenshot.',
|
||||||
|
'- For prefab or asset edits, inspect dependencies/references before mutation and refresh assets afterward.',
|
||||||
|
'- When changing tool exposure, save a named tool profile so the same client setup can be restored later.',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
KNOWN_INSTRUCTION_PATHS,
|
KNOWN_INSTRUCTION_PATHS,
|
||||||
|
createCocosMcpProjectSkill,
|
||||||
createProjectSkill,
|
createProjectSkill,
|
||||||
listProjectInstructions,
|
listProjectInstructions,
|
||||||
readProjectInstruction,
|
readProjectInstruction,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class ResourceProvider {
|
|||||||
createResource('cocos://logs/editor', `${projectName} Editor Logs`, 'Recent MCP runtime logs and tool interaction history.'),
|
createResource('cocos://logs/editor', `${projectName} Editor Logs`, 'Recent MCP runtime logs and tool interaction history.'),
|
||||||
createResource('cocos://logs/project', `${projectName} Project Logs`, 'Recent tails from common project log files.'),
|
createResource('cocos://logs/project', `${projectName} Project Logs`, 'Recent tails from common project log files.'),
|
||||||
createResource('cocos://mcp/interactions', `${projectName} MCP Interactions`, 'Recent MCP tool interaction summaries.'),
|
createResource('cocos://mcp/interactions', `${projectName} MCP Interactions`, 'Recent MCP tool interaction summaries.'),
|
||||||
|
createResource('cocos://mcp/execute-context', `${projectName} Execute Context Guide`, 'Available variables and code patterns for execute_javascript scene and editor contexts.'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +132,8 @@ class ResourceProvider {
|
|||||||
return this.getProjectLogsText(projectPath);
|
return this.getProjectLogsText(projectPath);
|
||||||
case 'cocos://mcp/interactions':
|
case 'cocos://mcp/interactions':
|
||||||
return this.interactionLog.summary();
|
return this.interactionLog.summary();
|
||||||
|
case 'cocos://mcp/execute-context':
|
||||||
|
return this.getExecuteContextGuide();
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -286,6 +289,100 @@ class ResourceProvider {
|
|||||||
].join('\n'))
|
].join('\n'))
|
||||||
.join('\n\n---\n\n');
|
.join('\n\n---\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExecuteContextGuide() {
|
||||||
|
return [
|
||||||
|
'Execute JavaScript Context Guide',
|
||||||
|
'',
|
||||||
|
'This document describes the variables and code patterns available in',
|
||||||
|
'execute_javascript for both scene and editor contexts.',
|
||||||
|
'',
|
||||||
|
'=== Scene Context (context="scene") ===',
|
||||||
|
'',
|
||||||
|
'Injected Variables:',
|
||||||
|
' cc - Cocos engine module. Use cc.Sprite, cc.Node, cc.Vec3, etc.',
|
||||||
|
' scene - Active scene root node (cc.Node). Same as director.getScene().',
|
||||||
|
' director - cc.director.',
|
||||||
|
' require - Node.js require (with Cocos module paths).',
|
||||||
|
' Editor - Editor global API (if running in editor).',
|
||||||
|
' args - User-passed args object.',
|
||||||
|
'',
|
||||||
|
'Return Patterns:',
|
||||||
|
' 1. Direct return: return { myResult: 42 };',
|
||||||
|
' 2. run function: async function run(env) { return result; }',
|
||||||
|
' 3. module.exports: module.exports = async (env) => result;',
|
||||||
|
'',
|
||||||
|
'Key Rules:',
|
||||||
|
' - Use cc.Sprite, cc.Node, cc.UITransform — NOT bare Sprite/Node.',
|
||||||
|
' - Do NOT redeclare scene, director, cc — they are already in scope.',
|
||||||
|
' - Find nodes by traversing scene.children recursively (see pattern below).',
|
||||||
|
' - Load assets with callback-style assetManager.loadAny (see pattern below).',
|
||||||
|
'',
|
||||||
|
'Pattern: Find a node by UUID',
|
||||||
|
' let target = null;',
|
||||||
|
' function search(n) {',
|
||||||
|
' if (n.uuid === "NODE_UUID") { target = n; return; }',
|
||||||
|
' for (const c of n.children) { search(c); if (target) return; }',
|
||||||
|
' }',
|
||||||
|
' search(scene);',
|
||||||
|
'',
|
||||||
|
'Pattern: Load and set an asset reference (SpriteFrame, Texture, etc.)',
|
||||||
|
' const { Sprite, assetManager, SpriteFrame } = cc;',
|
||||||
|
' const sf = await new Promise((resolve, reject) => {',
|
||||||
|
' assetManager.loadAny(',
|
||||||
|
' { uuid: "ASSET_UUID", type: SpriteFrame },',
|
||||||
|
' (err, asset) => { if (err) reject(err); else resolve(asset); }',
|
||||||
|
' );',
|
||||||
|
' });',
|
||||||
|
' sprite.spriteFrame = sf;',
|
||||||
|
'',
|
||||||
|
'Pattern: Create a node with components',
|
||||||
|
' const { Node, UITransform, Sprite } = cc;',
|
||||||
|
' const node = new Node("MyNode");',
|
||||||
|
' node.layer = parent.layer;',
|
||||||
|
' node.addComponent(UITransform);',
|
||||||
|
' node.addComponent(Sprite);',
|
||||||
|
' parent.addChild(node);',
|
||||||
|
'',
|
||||||
|
'=== Editor Context (context="editor") ===',
|
||||||
|
'',
|
||||||
|
'Injected Variables:',
|
||||||
|
' require - Node.js require.',
|
||||||
|
' Editor - Editor global API.',
|
||||||
|
' args - User-passed args object.',
|
||||||
|
' context - Runtime context (projectPath, projectName, cocosVersion, etc.).',
|
||||||
|
' helpers - { getStatus, listTools, readResource, callTool,',
|
||||||
|
' listClientTargets, getClientConfig, configureClient }.',
|
||||||
|
' fs - Node.js fs module.',
|
||||||
|
' path - Node.js path module.',
|
||||||
|
' os - Node.js os module.',
|
||||||
|
'',
|
||||||
|
'Pattern: Call asset-db from editor context',
|
||||||
|
' const info = await Editor.Message.request("asset-db", "query-asset-info", "ASSET_UUID");',
|
||||||
|
'',
|
||||||
|
'Pattern: Call a scene method from editor context',
|
||||||
|
' const result = await Editor.Message.request(',
|
||||||
|
' "scene", "execute-scene-script", { name: "funplay-cocos-mcp", method: "inspectNode", args: { uuid: "NODE_UUID" } }',
|
||||||
|
' );',
|
||||||
|
'',
|
||||||
|
'=== Common Pitfalls ===',
|
||||||
|
'',
|
||||||
|
'1. set_component_property cannot set asset references (SpriteFrame, Texture,',
|
||||||
|
' Material, Font, AudioClip). Use execute_javascript with loadAny instead.',
|
||||||
|
'',
|
||||||
|
'2. create_node only accepts parentPath (e.g. "Canvas/Player"), not parentUuid.',
|
||||||
|
' Use get_hierarchy first to find the full path.',
|
||||||
|
'',
|
||||||
|
'3. assetManager.loadAny(uuid) without callback returns null in scene context.',
|
||||||
|
' Always use callback style: loadAny({uuid, type}, (err, asset) => {}).',
|
||||||
|
'',
|
||||||
|
'4. Custom script components (e.g. TestComponent) require the script to be',
|
||||||
|
' compiled before add_component or findComponent can find them.',
|
||||||
|
'',
|
||||||
|
'5. Node hierarchy paths are slash-separated from scene root, e.g.',
|
||||||
|
' "should_hide_in_hierarchy/TestPrefab/Sprite". Use get_hierarchy to verify.',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
+158
-4
@@ -84,7 +84,11 @@ class McpServer {
|
|||||||
this.runtimeLog = options.runtimeLog;
|
this.runtimeLog = options.runtimeLog;
|
||||||
this.serverName = options.serverName;
|
this.serverName = options.serverName;
|
||||||
this.serverVersion = options.serverVersion;
|
this.serverVersion = options.serverVersion;
|
||||||
|
this.projectName = options.projectName || '';
|
||||||
|
this.projectIdentity = options.projectIdentity || '';
|
||||||
this.server = null;
|
this.server = null;
|
||||||
|
this.attached = false;
|
||||||
|
this.attachedInfo = null;
|
||||||
this.actualPort = null;
|
this.actualPort = null;
|
||||||
this.portFallbackInfo = null;
|
this.portFallbackInfo = null;
|
||||||
this.negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
|
this.negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
|
||||||
@@ -93,10 +97,13 @@ class McpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isRunning() {
|
isRunning() {
|
||||||
return Boolean(this.server && this.server.listening);
|
return Boolean(this.attached || (this.server && this.server.listening));
|
||||||
}
|
}
|
||||||
|
|
||||||
getPort() {
|
getPort() {
|
||||||
|
if (this.attached && this.actualPort) {
|
||||||
|
return this.actualPort;
|
||||||
|
}
|
||||||
if (this.server && typeof this.server.address === 'function') {
|
if (this.server && typeof this.server.address === 'function') {
|
||||||
const address = this.server.address();
|
const address = this.server.address();
|
||||||
if (address && typeof address.port === 'number') {
|
if (address && typeof address.port === 'number') {
|
||||||
@@ -114,6 +121,10 @@ class McpServer {
|
|||||||
return this.portFallbackInfo;
|
return this.portFallbackInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAttachInfo() {
|
||||||
|
return this.attachedInfo;
|
||||||
|
}
|
||||||
|
|
||||||
log(level, message) {
|
log(level, message) {
|
||||||
if (this.runtimeLog && typeof this.runtimeLog.add === 'function') {
|
if (this.runtimeLog && typeof this.runtimeLog.add === 'function') {
|
||||||
this.runtimeLog.add(level, message);
|
this.runtimeLog.add(level, message);
|
||||||
@@ -137,12 +148,42 @@ class McpServer {
|
|||||||
|
|
||||||
this.actualPort = null;
|
this.actualPort = null;
|
||||||
this.portFallbackInfo = null;
|
this.portFallbackInfo = null;
|
||||||
|
this.attached = false;
|
||||||
|
this.attachedInfo = null;
|
||||||
|
|
||||||
const requestHandler = async (request, response) => {
|
const requestHandler = async (request, response) => {
|
||||||
try {
|
try {
|
||||||
if (request.method === 'GET' && request.url === '/health') {
|
const requestUrl = new URL(request.url || '/', 'http://localhost');
|
||||||
|
if (request.method === 'GET' && requestUrl.pathname === '/health') {
|
||||||
this.log('info', 'GET /health');
|
this.log('info', 'GET /health');
|
||||||
return json(response, 200, { ok: true, name: this.serverName, version: this.serverVersion }, this.negotiatedProtocolVersion);
|
return json(response, 200, {
|
||||||
|
ok: true,
|
||||||
|
name: this.serverName,
|
||||||
|
version: this.serverVersion,
|
||||||
|
projectName: this.projectName,
|
||||||
|
projectIdentity: this.projectIdentity,
|
||||||
|
}, this.negotiatedProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === 'GET' && requestUrl.pathname === '/tools') {
|
||||||
|
this.log('info', 'GET /tools');
|
||||||
|
const includeCatalog = requestUrl.searchParams.get('catalog') === '1';
|
||||||
|
const tools = includeCatalog && typeof this.toolRegistry.listToolCatalog === 'function'
|
||||||
|
? this.toolRegistry.listToolCatalog()
|
||||||
|
: this.toolRegistry.listTools();
|
||||||
|
const baseUrl = `http://${request.headers.host || `${this.config.host}:${this.getPort()}`}`;
|
||||||
|
return json(response, 200, {
|
||||||
|
ok: true,
|
||||||
|
name: this.serverName,
|
||||||
|
version: this.serverVersion,
|
||||||
|
count: tools.length,
|
||||||
|
tools,
|
||||||
|
examples: {
|
||||||
|
health: `curl ${baseUrl}/health`,
|
||||||
|
tools: `curl ${baseUrl}/tools`,
|
||||||
|
catalog: `curl ${baseUrl}/tools?catalog=1`,
|
||||||
|
},
|
||||||
|
}, this.negotiatedProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isAllowedOrigin(request)) {
|
if (!this.isAllowedOrigin(request)) {
|
||||||
@@ -266,7 +307,12 @@ class McpServer {
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
candidate.removeAllListeners();
|
||||||
if (error && error.code === 'EADDRINUSE' && port < 65535 && attempt < MAX_PORT_FALLBACK_ATTEMPTS) {
|
if (error && error.code === 'EADDRINUSE' && port < 65535 && attempt < MAX_PORT_FALLBACK_ATTEMPTS) {
|
||||||
|
if (await this.tryAttachToExisting(port)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nextPort = port + 1;
|
const nextPort = port + 1;
|
||||||
this.log(
|
this.log(
|
||||||
'warn',
|
'warn',
|
||||||
@@ -277,7 +323,6 @@ class McpServer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate.removeAllListeners();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,6 +334,15 @@ class McpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
|
if (this.attached) {
|
||||||
|
this.log('info', `Detached from existing MCP listener on ${this.config.host}:${this.actualPort}.`);
|
||||||
|
this.attached = false;
|
||||||
|
this.attachedInfo = null;
|
||||||
|
this.actualPort = null;
|
||||||
|
this.portFallbackInfo = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.server) {
|
if (!this.server) {
|
||||||
this.log('info', 'Stop skipped: server object is empty.');
|
this.log('info', 'Stop skipped: server object is empty.');
|
||||||
return;
|
return;
|
||||||
@@ -299,6 +353,7 @@ class McpServer {
|
|||||||
this.server = null;
|
this.server = null;
|
||||||
this.actualPort = null;
|
this.actualPort = null;
|
||||||
this.portFallbackInfo = null;
|
this.portFallbackInfo = null;
|
||||||
|
this.attachedInfo = null;
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
active.close((error) => {
|
active.close((error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -312,6 +367,100 @@ class McpServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tryAttachToExisting(port) {
|
||||||
|
if (!this.projectIdentity || this.config.attachToExisting === false || port === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const probe = await this.probeExistingServer(port);
|
||||||
|
if (!probe || !probe.result) {
|
||||||
|
this.log('warn', `Port ${port} is occupied, but no compatible Funplay MCP initialize response was received.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = probe.result || {};
|
||||||
|
const serverInfo = result.serverInfo || {};
|
||||||
|
const funplay = result.funplay || {};
|
||||||
|
const remoteProjectIdentity = funplay.projectIdentity || serverInfo.projectIdentity || '';
|
||||||
|
const remoteName = serverInfo.name || '';
|
||||||
|
|
||||||
|
if (remoteName === this.serverName && remoteProjectIdentity === this.projectIdentity) {
|
||||||
|
this.attached = true;
|
||||||
|
this.attachedInfo = {
|
||||||
|
host: this.config.host,
|
||||||
|
port,
|
||||||
|
serverName: remoteName,
|
||||||
|
projectName: funplay.projectName || this.projectName,
|
||||||
|
projectIdentity: remoteProjectIdentity,
|
||||||
|
version: serverInfo.version || '',
|
||||||
|
};
|
||||||
|
this.actualPort = port;
|
||||||
|
this.portFallbackInfo = null;
|
||||||
|
this.log('info', `Attached to existing MCP listener for this project at http://${this.config.host}:${port}/.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
'warn',
|
||||||
|
`Port ${port} belongs to another listener; expected name=${this.serverName}, project=${this.projectIdentity}, ` +
|
||||||
|
`got name=${remoteName || 'unknown'}, project=${remoteProjectIdentity || 'unknown'}.`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
probeExistingServer(port) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 'funplay-probe',
|
||||||
|
method: 'initialize',
|
||||||
|
params: {
|
||||||
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||||
|
clientInfo: {
|
||||||
|
name: 'funplay-cocos-mcp-probe',
|
||||||
|
version: this.serverVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const request = http.request(
|
||||||
|
{
|
||||||
|
host: this.config.host,
|
||||||
|
port,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/',
|
||||||
|
timeout: 600,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, text/event-stream',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
const chunks = [];
|
||||||
|
response.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
response.on('end', () => {
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
request.on('timeout', () => {
|
||||||
|
request.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
request.on('error', () => resolve(null));
|
||||||
|
request.end(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
listen(server, port, host) {
|
listen(server, port, host) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const onError = (error) => {
|
const onError = (error) => {
|
||||||
@@ -537,6 +686,11 @@ class McpServer {
|
|||||||
name: this.serverName,
|
name: this.serverName,
|
||||||
version: this.serverVersion,
|
version: this.serverVersion,
|
||||||
},
|
},
|
||||||
|
funplay: {
|
||||||
|
server: 'funplay-cocos-mcp',
|
||||||
|
projectName: this.projectName,
|
||||||
|
projectIdentity: this.projectIdentity,
|
||||||
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
resources: {},
|
resources: {},
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PROFILE_FIELDS = [
|
||||||
|
'toolProfile',
|
||||||
|
'enabledToolCategories',
|
||||||
|
'disabledToolCategories',
|
||||||
|
'enabledTools',
|
||||||
|
'disabledTools',
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeProfileName(value) {
|
||||||
|
const normalized = String(value || '').trim();
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error('profile name is required.');
|
||||||
|
}
|
||||||
|
return normalized.slice(0, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStringList(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.split(/[\n,]/).map((item) => item.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProfileMode(value) {
|
||||||
|
const normalized = String(value || 'core').trim().toLowerCase();
|
||||||
|
return normalized === 'full' || normalized === 'custom' ? normalized : 'core';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToolProfile(value) {
|
||||||
|
const profile = value || {};
|
||||||
|
return {
|
||||||
|
name: normalizeProfileName(profile.name),
|
||||||
|
toolProfile: normalizeProfileMode(profile.toolProfile),
|
||||||
|
enabledToolCategories: normalizeStringList(profile.enabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
disabledToolCategories: normalizeStringList(profile.disabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
enabledTools: normalizeStringList(profile.enabledTools),
|
||||||
|
disabledTools: normalizeStringList(profile.disabledTools),
|
||||||
|
updatedAt: profile.updatedAt ? String(profile.updatedAt) : new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSavedToolProfiles(value) {
|
||||||
|
const profiles = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const item of Array.isArray(value) ? value : []) {
|
||||||
|
try {
|
||||||
|
const profile = normalizeToolProfile(item);
|
||||||
|
const key = profile.name.toLowerCase();
|
||||||
|
if (seen.has(key)) {
|
||||||
|
const index = profiles.findIndex((existing) => existing.name.toLowerCase() === key);
|
||||||
|
profiles[index] = profile;
|
||||||
|
} else {
|
||||||
|
seen.add(key);
|
||||||
|
profiles.push(profile);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore malformed saved profile entries rather than breaking extension startup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profiles.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToolProfileSnapshot(config = {}, name) {
|
||||||
|
return normalizeToolProfile({
|
||||||
|
name,
|
||||||
|
toolProfile: config.toolProfile,
|
||||||
|
enabledToolCategories: config.enabledToolCategories,
|
||||||
|
disabledToolCategories: config.disabledToolCategories,
|
||||||
|
enabledTools: config.enabledTools,
|
||||||
|
disabledTools: config.disabledTools,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertToolProfile(savedProfiles, profile) {
|
||||||
|
const normalized = normalizeToolProfile(profile);
|
||||||
|
const profiles = normalizeSavedToolProfiles(savedProfiles);
|
||||||
|
const key = normalized.name.toLowerCase();
|
||||||
|
const index = profiles.findIndex((item) => item.name.toLowerCase() === key);
|
||||||
|
if (index >= 0) {
|
||||||
|
profiles[index] = normalized;
|
||||||
|
} else {
|
||||||
|
profiles.push(normalized);
|
||||||
|
}
|
||||||
|
return normalizeSavedToolProfiles(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteToolProfile(savedProfiles, name) {
|
||||||
|
const key = normalizeProfileName(name).toLowerCase();
|
||||||
|
return normalizeSavedToolProfiles(savedProfiles)
|
||||||
|
.filter((profile) => profile.name.toLowerCase() !== key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findToolProfile(savedProfiles, name) {
|
||||||
|
const key = normalizeProfileName(name).toLowerCase();
|
||||||
|
return normalizeSavedToolProfiles(savedProfiles)
|
||||||
|
.find((profile) => profile.name.toLowerCase() === key) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyToolProfile(config = {}, profile) {
|
||||||
|
const normalized = normalizeToolProfile(profile);
|
||||||
|
const next = { ...config };
|
||||||
|
for (const field of PROFILE_FIELDS) {
|
||||||
|
next[field] = Array.isArray(normalized[field])
|
||||||
|
? normalized[field].slice()
|
||||||
|
: normalized[field];
|
||||||
|
}
|
||||||
|
next.activeToolProfileName = normalized.name;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportToolProfiles(savedProfiles) {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
profiles: normalizeSavedToolProfiles(savedProfiles),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProfileImportPayload(payload) {
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
return JSON.parse(payload);
|
||||||
|
}
|
||||||
|
return payload || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function importToolProfiles(savedProfiles, payload, options = {}) {
|
||||||
|
const parsed = parseProfileImportPayload(payload);
|
||||||
|
const incoming = Array.isArray(parsed)
|
||||||
|
? parsed
|
||||||
|
: Array.isArray(parsed.profiles)
|
||||||
|
? parsed.profiles
|
||||||
|
: [];
|
||||||
|
if (!incoming.length) {
|
||||||
|
throw new Error('No tool profiles found in import payload.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = options.replace ? [] : normalizeSavedToolProfiles(savedProfiles);
|
||||||
|
return incoming.reduce((profiles, profile) => upsertToolProfile(profiles, profile), base);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
applyToolProfile,
|
||||||
|
createToolProfileSnapshot,
|
||||||
|
deleteToolProfile,
|
||||||
|
exportToolProfiles,
|
||||||
|
findToolProfile,
|
||||||
|
importToolProfiles,
|
||||||
|
normalizeSavedToolProfiles,
|
||||||
|
normalizeToolProfile,
|
||||||
|
upsertToolProfile,
|
||||||
|
};
|
||||||
+115
-309
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
|
||||||
const {
|
const {
|
||||||
clearSelection,
|
clearSelection,
|
||||||
deleteAsset,
|
deleteAsset,
|
||||||
@@ -24,6 +23,7 @@ const {
|
|||||||
} = require('./logs');
|
} = require('./logs');
|
||||||
const { resolveProjectPath } = require('./path-safety');
|
const { resolveProjectPath } = require('./path-safety');
|
||||||
const {
|
const {
|
||||||
|
createCocosMcpProjectSkill,
|
||||||
createProjectSkill,
|
createProjectSkill,
|
||||||
listProjectInstructions,
|
listProjectInstructions,
|
||||||
readProjectInstruction,
|
readProjectInstruction,
|
||||||
@@ -31,18 +31,31 @@ const {
|
|||||||
} = require('./project-instructions');
|
} = require('./project-instructions');
|
||||||
const {
|
const {
|
||||||
applyPrefabInstance,
|
applyPrefabInstance,
|
||||||
|
createPrefab,
|
||||||
duplicatePrefab,
|
duplicatePrefab,
|
||||||
editPrefabJson,
|
editPrefabJson,
|
||||||
inspectPrefab,
|
inspectPrefab,
|
||||||
revertPrefabInstance,
|
revertPrefabInstance,
|
||||||
validatePrefabReferences,
|
validatePrefabReferences,
|
||||||
} = require('./prefabs');
|
} = require('./prefabs');
|
||||||
|
const { createAssetsAdvancedTools } = require('./tools/assets-advanced');
|
||||||
|
const { createCocosProjectTools } = require('./tools/cocos-project');
|
||||||
|
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
|
||||||
|
const { createSceneEventTools } = require('./tools/scene-events');
|
||||||
|
const { createSceneManagementTools } = require('./tools/scene-management');
|
||||||
|
const { createScriptTools } = require('./tools/scripts');
|
||||||
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
|
||||||
const { checkForUpdate } = require('./update-checker');
|
const { checkForUpdate } = require('./update-checker');
|
||||||
|
const { assertJavascriptSafety } = require('./javascript-safety');
|
||||||
const { safeStringify } = require('./utils');
|
const { safeStringify } = require('./utils');
|
||||||
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
|
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
|
||||||
|
|
||||||
const TOOL_CATEGORY_RULES = [
|
const TOOL_CATEGORY_RULES = [
|
||||||
|
['project', /^(get_project_info|get_editor_state|get_tool_catalog)$/],
|
||||||
|
['build', /^(get_build_status|open_build_panel|run_project_preview|save_current_scene)$/],
|
||||||
|
['preferences', /preference/],
|
||||||
|
['broadcast', /broadcast/],
|
||||||
|
['events', /event|bind_button_click|button_click/],
|
||||||
['updates', /update/],
|
['updates', /update/],
|
||||||
['logs', /log/],
|
['logs', /log/],
|
||||||
['diagnostics', /diagnostic|validate/],
|
['diagnostics', /diagnostic|validate/],
|
||||||
@@ -60,7 +73,6 @@ const TOOL_CATEGORY_RULES = [
|
|||||||
['runtime', /runtime|time_scale|node_event|invoke_component/],
|
['runtime', /runtime|time_scale|node_event|invoke_component/],
|
||||||
['scene', /scene|hierarchy|node/],
|
['scene', /scene|hierarchy|node/],
|
||||||
['execution', /execute_/],
|
['execution', /execute_/],
|
||||||
['project', /project|editor_state|tool_catalog/],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function createSchema(properties, required) {
|
function createSchema(properties, required) {
|
||||||
@@ -306,89 +318,28 @@ function toOutput(value) {
|
|||||||
return safeStringify(value);
|
return safeStringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesPattern(fileName, pattern) {
|
function useJavascriptSafetyChecks(args, runtimeContext) {
|
||||||
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
if (args && typeof args.safety_checks === 'boolean') {
|
||||||
return new RegExp(`^${escaped}$`, 'i').test(fileName);
|
return args.safety_checks;
|
||||||
|
}
|
||||||
|
if (args && typeof args.safetyChecks === 'boolean') {
|
||||||
|
return args.safetyChecks;
|
||||||
|
}
|
||||||
|
const config = runtimeContext && runtimeContext.config;
|
||||||
|
if (config && typeof config.executeJavascriptSafetyChecks === 'boolean') {
|
||||||
|
return config.executeJavascriptSafetyChecks;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchFiles(rootDir, pattern, limit) {
|
function assertToolJavascriptSafety(args, runtimeContext) {
|
||||||
const results = [];
|
if (!useJavascriptSafetyChecks(args, runtimeContext)) {
|
||||||
if (!fs.existsSync(rootDir)) {
|
return;
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = [rootDir];
|
assertJavascriptSafety(args && args.code, {
|
||||||
while (stack.length && results.length < limit) {
|
projectPath: runtimeContext && runtimeContext.projectPath,
|
||||||
const current = stack.pop();
|
});
|
||||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'temp' || entry.name === 'library') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = path.join(current, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
stack.push(fullPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchesPattern(entry.name, pattern)) {
|
|
||||||
results.push(fullPath);
|
|
||||||
if (results.length >= limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readLines(filePath) {
|
|
||||||
return fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSnippet(filePath, lineNumber, contextLines = 3) {
|
|
||||||
const lines = readLines(filePath);
|
|
||||||
const start = Math.max(1, Number(lineNumber || 1) - Math.max(0, contextLines));
|
|
||||||
const end = Math.min(lines.length, Number(lineNumber || 1) + Math.max(0, contextLines));
|
|
||||||
const snippet = [];
|
|
||||||
for (let line = start; line <= end; line += 1) {
|
|
||||||
const marker = line === Number(lineNumber || 1) ? '>' : ' ';
|
|
||||||
snippet.push(`${marker} ${String(line).padStart(4, ' ')} | ${lines[line - 1]}`);
|
|
||||||
}
|
|
||||||
return snippet.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceAllLiteral(content, search, replacement) {
|
|
||||||
if (!search) {
|
|
||||||
throw new Error('search text is required.');
|
|
||||||
}
|
|
||||||
return content.split(search).join(replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshAssets(projectPath, targetPath) {
|
|
||||||
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
|
||||||
return 'Asset refresh API is unavailable; Cocos Creator should pick up file changes automatically.';
|
|
||||||
}
|
|
||||||
|
|
||||||
const relative = path.relative(path.join(projectPath, 'assets'), targetPath).replace(/\\/g, '/');
|
|
||||||
if (!relative.startsWith('..')) {
|
|
||||||
const dbUrl = `db://assets/${relative}`;
|
|
||||||
try {
|
|
||||||
await Editor.Message.request('asset-db', 'refresh-asset', dbUrl);
|
|
||||||
return `Refreshed asset database for ${dbUrl}`;
|
|
||||||
} catch (error) {
|
|
||||||
try {
|
|
||||||
await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets');
|
|
||||||
return `Refreshed asset database after writing ${dbUrl}`;
|
|
||||||
} catch (innerError) {
|
|
||||||
return `File written, but asset refresh failed: ${innerError.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'File written outside assets directory; no asset-db refresh was needed.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveNodeUuid(sceneBridge, args) {
|
async function resolveNodeUuid(sceneBridge, args) {
|
||||||
@@ -410,14 +361,17 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[primary] Execute JavaScript in either the scene or editor context. Use context=\"scene\" for live scene/runtime inspection and mutation, or context=\"editor\" for Editor APIs, asset-db workflows, MCP orchestration, local filesystem access, and higher-level automation. Prefer this as the main flexible tool when many narrow tools would be noisy.',
|
description: '[primary] Execute JavaScript in either the scene or editor context. Use context=\"scene\" for live scene/runtime inspection and mutation, or context=\"editor\" for Editor APIs, asset-db workflows, MCP orchestration, local filesystem access, and higher-level automation. Prefer this as the main flexible tool when many narrow tools would be noisy.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
context: { type: 'string', description: 'Execution context: scene or editor.' },
|
context: { type: 'string', enum: ['scene', 'editor'], description: 'Execution context. Scene injects: cc, scene, director, require, Editor, args. Editor injects: require, Editor, args, context, helpers, fs, path, os. Read cocos://mcp/execute-context resource for details.' },
|
||||||
code: { type: 'string', description: 'JavaScript code to execute. May directly return a value, define run(env), or export a function.' },
|
code: { type: 'string', description: 'JavaScript code. May directly return a value, define run(env), or use module.exports. In scene context, use cc.Sprite, cc.Node (not bare Sprite/Node). Load assets via cc.assetManager.loadAny({uuid, type}, cb) callback style.' },
|
||||||
args: { type: 'object', description: 'Optional JSON object passed into the script.' },
|
args: { type: 'object', description: 'Optional JSON object passed into the script.' },
|
||||||
|
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||||
},
|
},
|
||||||
['context', 'code']
|
['context', 'code']
|
||||||
),
|
),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
const context = String(args.context || '').toLowerCase();
|
const context = String(args.context || '').toLowerCase();
|
||||||
|
const runtimeContext = getRuntimeContext();
|
||||||
|
assertToolJavascriptSafety(args, runtimeContext);
|
||||||
if (context === 'scene') {
|
if (context === 'scene') {
|
||||||
return sceneBridge.call('executeCode', { code: args.code, args: args.args || {} });
|
return sceneBridge.call('executeCode', { code: args.code, args: args.args || {} });
|
||||||
}
|
}
|
||||||
@@ -436,12 +390,16 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[compat] Execute JavaScript in the active Cocos scene context. Prefer execute_javascript with context="scene" as the main unified tool; use this when you specifically want the scene-only compatibility entrypoint.',
|
description: '[compat] Execute JavaScript in the active Cocos scene context. Prefer execute_javascript with context="scene" as the main unified tool; use this when you specifically want the scene-only compatibility entrypoint.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
code: { type: 'string', description: 'JavaScript code to execute inside the scene script context.' },
|
code: { type: 'string', description: 'JavaScript code. Injected vars: cc, scene, director, require, Editor, args. Use cc.Sprite etc. (not bare Sprite). See cocos://mcp/execute-context resource.' },
|
||||||
args: { type: 'object', description: 'Optional JSON object passed to the scene script.' },
|
args: { type: 'object', description: 'Optional JSON object passed to the scene script.' },
|
||||||
|
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||||
},
|
},
|
||||||
['code']
|
['code']
|
||||||
),
|
),
|
||||||
handler: async (args) => sceneBridge.call('executeCode', { code: args.code, args: args.args || {} }),
|
handler: async (args) => {
|
||||||
|
assertToolJavascriptSafety(args, getRuntimeContext());
|
||||||
|
return sceneBridge.call('executeCode', { code: args.code, args: args.args || {} });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'execute_editor_script',
|
name: 'execute_editor_script',
|
||||||
@@ -449,12 +407,14 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[compat] Execute JavaScript in the editor/browser context. Prefer execute_javascript with context="editor" as the main unified tool; use this when you specifically want the editor-only compatibility entrypoint.',
|
description: '[compat] Execute JavaScript in the editor/browser context. Prefer execute_javascript with context="editor" as the main unified tool; use this when you specifically want the editor-only compatibility entrypoint.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
code: { type: 'string', description: 'JavaScript code to execute inside the editor context.' },
|
code: { type: 'string', description: 'JavaScript code. Injected vars: require, Editor, args, context, helpers, fs, path, os. See cocos://mcp/execute-context resource.' },
|
||||||
args: { type: 'object', description: 'Optional JSON object passed to the editor script.' },
|
args: { type: 'object', description: 'Optional JSON object passed to the editor script.' },
|
||||||
|
safety_checks: { type: 'boolean', description: 'Override the project default JavaScript safety checks for this call.' },
|
||||||
},
|
},
|
||||||
['code']
|
['code']
|
||||||
),
|
),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
|
assertToolJavascriptSafety(args, getRuntimeContext());
|
||||||
if (typeof editorExecutor !== 'function') {
|
if (typeof editorExecutor !== 'function') {
|
||||||
throw new Error('Editor JavaScript execution is unavailable.');
|
throw new Error('Editor JavaScript execution is unavailable.');
|
||||||
}
|
}
|
||||||
@@ -597,6 +557,22 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
return createProjectSkill(projectPath, args);
|
return createProjectSkill(projectPath, args);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'create_cocos_mcp_project_skill',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a recommended local Codex project skill for Funplay Cocos MCP workflows.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
skillName: { type: 'string', description: 'Optional filesystem-safe project skill name.' },
|
||||||
|
overwrite: { type: 'boolean', description: 'Allow overwriting an existing skill. Defaults to true.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return createCocosMcpProjectSkill(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'set_selection',
|
name: 'set_selection',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -692,7 +668,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
name: { type: 'string', description: 'Name of the node to create.' },
|
name: { type: 'string', description: 'Name of the node to create.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root. Use get_hierarchy to find the full path.' },
|
||||||
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
||||||
scale: { type: 'object', description: 'Optional scale {x,y,z}.' },
|
scale: { type: 'object', description: 'Optional scale {x,y,z}.' },
|
||||||
eulerAngles: { type: 'object', description: 'Optional rotation {x,y,z} in degrees.' },
|
eulerAngles: { type: 'object', description: 'Optional rotation {x,y,z} in degrees.' },
|
||||||
@@ -708,7 +684,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: 'Delete a node by path, uuid, or name.',
|
description: 'Delete a node by path, uuid, or name.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Hierarchy path.' },
|
path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
},
|
},
|
||||||
@@ -722,7 +698,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: 'Update node position, rotation, scale, or active state.',
|
description: 'Update node position, rotation, scale, or active state.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Hierarchy path.' },
|
path: { type: 'string', description: 'Hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
position: { type: 'object', description: 'Position {x,y,z}.' },
|
position: { type: 'object', description: 'Position {x,y,z}.' },
|
||||||
@@ -741,6 +717,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema({}, []),
|
inputSchema: createSchema({}, []),
|
||||||
handler: async () => getRuntimeContext(),
|
handler: async () => getRuntimeContext(),
|
||||||
},
|
},
|
||||||
|
...createCocosProjectTools({ createSchema }),
|
||||||
|
...createSceneManagementTools({ createSchema, sceneBridge }),
|
||||||
{
|
{
|
||||||
name: 'list_scenes',
|
name: 'list_scenes',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -783,6 +761,19 @@ 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: 'create_prefab',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a prefab asset from a scene node.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
nodeUuid: { type: 'string', description: 'UUID of the scene node to save as prefab.' },
|
||||||
|
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/prefabs/Enemy.prefab.' },
|
||||||
|
},
|
||||||
|
['nodeUuid', 'savePath']
|
||||||
|
),
|
||||||
|
handler: async (args) => createPrefab(args),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'inspect_prefab',
|
name: 'inspect_prefab',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -862,7 +853,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
prefabUuid: { type: 'string', description: 'Prefab asset uuid, db url, or path.' },
|
prefabUuid: { type: 'string', description: 'Prefab asset uuid, db url, or path.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||||
name: { type: 'string', description: 'Optional override node name.' },
|
name: { type: 'string', description: 'Optional override node name.' },
|
||||||
position: { type: 'object', description: 'Optional position {x,y,z}; fallback runtime path only.' },
|
position: { type: 'object', description: 'Optional position {x,y,z}; fallback runtime path only.' },
|
||||||
},
|
},
|
||||||
@@ -907,7 +898,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available.',
|
description: '[specialist] Inspect whether a scene node is linked to a prefab instance and return prefab metadata when available.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
},
|
},
|
||||||
@@ -921,7 +912,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message.',
|
description: '[core] Apply a scene prefab instance back to its associated prefab asset using the Cocos editor scene apply-prefab message.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
},
|
},
|
||||||
@@ -935,7 +926,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages.',
|
description: '[core] Revert a scene prefab instance from its associated prefab asset using available Cocos editor prefab revert messages.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
},
|
},
|
||||||
@@ -950,7 +941,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
prefabUuid: { type: 'string', description: 'Prefab asset uuid.' },
|
prefabUuid: { type: 'string', description: 'Prefab asset uuid.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||||
name: { type: 'string', description: 'Optional override node name.' },
|
name: { type: 'string', description: 'Optional override node name.' },
|
||||||
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
||||||
},
|
},
|
||||||
@@ -1046,6 +1037,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
return selectAsset(info.uuid || args.target);
|
return selectAsset(info.uuid || args.target);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...createAssetsAdvancedTools({ createSchema, getRuntimeContext }),
|
||||||
{
|
{
|
||||||
name: 'get_editor_selection',
|
name: 'get_editor_selection',
|
||||||
profile: 'full',
|
profile: 'full',
|
||||||
@@ -1059,7 +1051,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[core] List components attached to a scene node.',
|
description: '[core] List components attached to a scene node.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
},
|
},
|
||||||
@@ -1073,10 +1065,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[core] Inspect a component attached to a node.',
|
description: '[core] Inspect a component attached to a node.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
componentName: { type: 'string', description: 'Component class name.' },
|
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||||
index: { type: 'number', description: 'Optional component index.' },
|
index: { type: 'number', description: 'Optional component index.' },
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -1089,10 +1081,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: 'Add a component to a node by component class name.',
|
description: 'Add a component to a node by component class name.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
componentName: { type: 'string', description: 'Component class name, for example Sprite or cc.UITransform.' },
|
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button, UITransform. Custom script components require the script to be compiled first.' },
|
||||||
},
|
},
|
||||||
['componentName']
|
['componentName']
|
||||||
),
|
),
|
||||||
@@ -1104,10 +1096,10 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: 'Remove a component from a node by name or index.',
|
description: 'Remove a component from a node by name or index.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
componentName: { type: 'string', description: 'Component class name.' },
|
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||||
index: { type: 'number', description: 'Optional component index.' },
|
index: { type: 'number', description: 'Optional component index.' },
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -1120,13 +1112,13 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: 'Set a component property by dot path using a JSON value.',
|
description: 'Set a component property by dot path using a JSON value.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
componentName: { type: 'string', description: 'Component class name.' },
|
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||||
index: { type: 'number', description: 'Optional component index.' },
|
index: { type: 'number', description: 'Optional component index.' },
|
||||||
propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' },
|
propertyPath: { type: 'string', description: 'Property path using dot notation, e.g. "color.r", "enabled", "size.width".' },
|
||||||
valueJson: { type: 'string', description: 'JSON encoded value to assign, for example true, 12, \"hero\", or {\"x\":1}.' },
|
valueJson: { type: 'string', description: 'JSON encoded value, e.g. true, 12, "hero", {"x":1}. NOTE: Cannot set asset references (SpriteFrame, Texture, Material, etc.) — use execute_javascript with cc.assetManager.loadAny({uuid, type}, cb) for those.' },
|
||||||
},
|
},
|
||||||
['propertyPath', 'valueJson']
|
['propertyPath', 'valueJson']
|
||||||
),
|
),
|
||||||
@@ -1146,12 +1138,12 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: 'Reset or clear a component property by dot path.',
|
description: 'Reset or clear a component property by dot path.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
componentName: { type: 'string', description: 'Component class name.' },
|
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||||
index: { type: 'number', description: 'Optional component index.' },
|
index: { type: 'number', description: 'Optional component index.' },
|
||||||
propertyPath: { type: 'string', description: 'Property path such as color.r or enabled.' },
|
propertyPath: { type: 'string', description: 'Property path using dot notation, e.g. "color.r", "enabled", "size.width".' },
|
||||||
},
|
},
|
||||||
['propertyPath']
|
['propertyPath']
|
||||||
),
|
),
|
||||||
@@ -1164,7 +1156,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
name: { type: 'string', description: 'Canvas node name.' },
|
name: { type: 'string', description: 'Canvas node name.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||||
width: { type: 'number', description: 'Canvas width.' },
|
width: { type: 'number', description: 'Canvas width.' },
|
||||||
height: { type: 'number', description: 'Canvas height.' },
|
height: { type: 'number', description: 'Canvas height.' },
|
||||||
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
position: { type: 'object', description: 'Optional position {x,y,z}.' },
|
||||||
@@ -1180,7 +1172,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
name: { type: 'string', description: 'Label node name.' },
|
name: { type: 'string', description: 'Label node name.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||||
text: { type: 'string', description: 'Label text.' },
|
text: { type: 'string', description: 'Label text.' },
|
||||||
fontSize: { type: 'number', description: 'Font size.' },
|
fontSize: { type: 'number', description: 'Font size.' },
|
||||||
width: { type: 'number', description: 'UI width.' },
|
width: { type: 'number', description: 'UI width.' },
|
||||||
@@ -1199,7 +1191,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
name: { type: 'string', description: 'Button node name.' },
|
name: { type: 'string', description: 'Button node name.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||||
text: { type: 'string', description: 'Button text.' },
|
text: { type: 'string', description: 'Button text.' },
|
||||||
width: { type: 'number', description: 'Button width.' },
|
width: { type: 'number', description: 'Button width.' },
|
||||||
height: { type: 'number', description: 'Button height.' },
|
height: { type: 'number', description: 'Button height.' },
|
||||||
@@ -1219,8 +1211,8 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
name: { type: 'string', description: 'Sprite node name.' },
|
name: { type: 'string', description: 'Sprite node name.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||||
spriteFrameUuid: { type: 'string', description: 'Optional SpriteFrame asset uuid.' },
|
spriteFrameUuid: { type: 'string', description: 'SpriteFrame asset uuid, e.g. "57520716-48c8-4a19-8acf-41c9f8777fb0@f9941". Use list_assets to find available SpriteFrames.' },
|
||||||
width: { type: 'number', description: 'UI width.' },
|
width: { type: 'number', description: 'UI width.' },
|
||||||
height: { type: 'number', description: 'UI height.' },
|
height: { type: 'number', description: 'UI height.' },
|
||||||
color: { type: 'string', description: 'Sprite color as #RRGGBB or #RRGGBBAA.' },
|
color: { type: 'string', description: 'Sprite color as #RRGGBB or #RRGGBBAA.' },
|
||||||
@@ -1244,7 +1236,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
name: { type: 'string', description: 'Camera node name.' },
|
name: { type: 'string', description: 'Camera node name.' },
|
||||||
parentPath: { type: 'string', description: 'Optional parent node path.' },
|
parentPath: { type: 'string', description: 'Parent node hierarchy path, e.g. "Canvas/Player". Slash-separated from scene root.' },
|
||||||
priority: { type: 'number', description: 'Camera priority.' },
|
priority: { type: 'number', description: 'Camera priority.' },
|
||||||
visibility: { type: 'number', description: 'Camera visibility mask.' },
|
visibility: { type: 'number', description: 'Camera visibility mask.' },
|
||||||
clearFlags: { type: 'number', description: 'Camera clear flags.' },
|
clearFlags: { type: 'number', description: 'Camera clear flags.' },
|
||||||
@@ -1297,7 +1289,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: 'Add an AnimationClip asset to a node Animation component.',
|
description: 'Add an AnimationClip asset to a node Animation component.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
clipUuid: { type: 'string', description: 'AnimationClip asset uuid.' },
|
clipUuid: { type: 'string', description: 'AnimationClip asset uuid.' },
|
||||||
@@ -1313,7 +1305,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[core] Play an Animation component clip on a node.',
|
description: '[core] Play an Animation component clip on a node.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
clipName: { type: 'string', description: 'Optional clip name.' },
|
clipName: { type: 'string', description: 'Optional clip name.' },
|
||||||
@@ -1328,7 +1320,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[core] Stop an Animation component clip on a node.',
|
description: '[core] Stop an Animation component clip on a node.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
clipName: { type: 'string', description: 'Optional clip name.' },
|
clipName: { type: 'string', description: 'Optional clip name.' },
|
||||||
@@ -1337,195 +1329,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
),
|
),
|
||||||
handler: async (args) => sceneBridge.call('stopAnimation', args),
|
handler: async (args) => sceneBridge.call('stopAnimation', args),
|
||||||
},
|
},
|
||||||
{
|
...createFileTools({ createSchema, getRuntimeContext }),
|
||||||
name: 'read_file',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] Read a file from the Cocos project.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
|
||||||
},
|
|
||||||
['path']
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
throw new Error(`File not found: ${args.path}`);
|
|
||||||
}
|
|
||||||
const content = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
return content.length > 12000 ? `${content.slice(0, 12000)}\n... (truncated)` : content;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'get_file_snippet',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] Read a focused snippet around a file line number.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
|
||||||
line: { type: 'number', description: 'Target line number, starting at 1.' },
|
|
||||||
contextLines: { type: 'number', description: 'Number of surrounding context lines.' },
|
|
||||||
},
|
|
||||||
['path', 'line']
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
throw new Error(`File not found: ${args.path}`);
|
|
||||||
}
|
|
||||||
return buildSnippet(fullPath, args.line, Number.isFinite(args.contextLines) ? args.contextLines : 3);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'write_file',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] Write or overwrite a file in the Cocos project.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
|
||||||
content: { type: 'string', description: 'File content to write.' },
|
|
||||||
},
|
|
||||||
['path', 'content']
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
||||||
fs.writeFileSync(fullPath, args.content, 'utf8');
|
|
||||||
return `Wrote ${args.content.length} chars to ${args.path}\n${await refreshAssets(projectPath, fullPath)}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'replace_in_file',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] Replace text in a file, useful for script auto-fix loops.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
|
||||||
search: { type: 'string', description: 'Literal text to search for.' },
|
|
||||||
replace: { type: 'string', description: 'Replacement text.' },
|
|
||||||
replaceAll: { type: 'boolean', description: 'Replace every occurrence instead of only the first.' },
|
|
||||||
},
|
|
||||||
['path', 'search', 'replace']
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const fullPath = resolveProjectPath(projectPath, args.path);
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
throw new Error(`File not found: ${args.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const original = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
if (!original.includes(args.search)) {
|
|
||||||
throw new Error(`Search text was not found in ${args.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = args.replaceAll
|
|
||||||
? replaceAllLiteral(original, args.search, args.replace)
|
|
||||||
: original.replace(args.search, args.replace);
|
|
||||||
|
|
||||||
fs.writeFileSync(fullPath, updated, 'utf8');
|
|
||||||
return `Updated ${args.path} (${args.replaceAll ? 'all matches' : 'first match'})\n${await refreshAssets(projectPath, fullPath)}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'search_files',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] Search project files by simple wildcard pattern.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
pattern: { type: 'string', description: "Wildcard file pattern such as '*.ts' or 'Player*'." },
|
|
||||||
directory: { type: 'string', description: 'Project-relative search root. Defaults to assets.' },
|
|
||||||
limit: { type: 'number', description: 'Maximum number of results to return.' },
|
|
||||||
},
|
|
||||||
['pattern']
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const searchRoot = resolveProjectPath(projectPath, args.directory || 'assets');
|
|
||||||
if (!fs.existsSync(searchRoot)) {
|
|
||||||
throw new Error(`Directory not found: ${args.directory || 'assets'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(500, args.limit)) : 100;
|
|
||||||
const results = searchFiles(searchRoot, args.pattern, limit).map((fullPath) =>
|
|
||||||
path.relative(projectPath, fullPath).replace(/\\/g, '/')
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
count: results.length,
|
|
||||||
files: results,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'list_directory',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] List files and directories inside a project directory.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
path: { type: 'string', description: 'Project-relative or absolute directory path.' },
|
|
||||||
},
|
|
||||||
['path']
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const targetPath = resolveProjectPath(projectPath, args.path);
|
|
||||||
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
||||||
throw new Error(`Directory not found: ${args.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = fs
|
|
||||||
.readdirSync(targetPath, { withFileTypes: true })
|
|
||||||
.filter((entry) => !entry.name.startsWith('.'))
|
|
||||||
.map((entry) => ({
|
|
||||||
name: entry.name,
|
|
||||||
type: entry.isDirectory() ? 'directory' : 'file',
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
path: args.path,
|
|
||||||
entries,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'exists',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] Check whether a project file or directory exists.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
path: { type: 'string', description: 'Project-relative or absolute path.' },
|
|
||||||
},
|
|
||||||
['path']
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const targetPath = resolveProjectPath(projectPath, args.path);
|
|
||||||
return {
|
|
||||||
path: args.path,
|
|
||||||
exists: fs.existsSync(targetPath),
|
|
||||||
isFile: fs.existsSync(targetPath) ? fs.statSync(targetPath).isFile() : false,
|
|
||||||
isDirectory: fs.existsSync(targetPath) ? fs.statSync(targetPath).isDirectory() : false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'refresh_assets',
|
|
||||||
profile: 'full',
|
|
||||||
description: '[core] Best-effort asset database refresh for a file or the assets root.',
|
|
||||||
inputSchema: createSchema(
|
|
||||||
{
|
|
||||||
path: { type: 'string', description: 'Optional project-relative file path to refresh.' },
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
),
|
|
||||||
handler: async (args) => {
|
|
||||||
const { projectPath } = getRuntimeContext();
|
|
||||||
const targetPath = resolveProjectPath(projectPath, args.path || 'assets');
|
|
||||||
return await refreshAssets(projectPath, targetPath);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'run_script_diagnostics',
|
name: 'run_script_diagnostics',
|
||||||
profile: 'core',
|
profile: 'core',
|
||||||
@@ -1712,7 +1516,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
description: '[core] Emit a custom event on a target scene node with an optional JSON payload.',
|
description: '[core] Emit a custom event on a target scene node with an optional JSON payload.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
eventName: { type: 'string', description: 'Event name to emit.' },
|
eventName: { type: 'string', description: 'Event name to emit.' },
|
||||||
@@ -1736,16 +1540,18 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
|
|||||||
),
|
),
|
||||||
handler: async (args) => sceneBridge.call('simulateButtonClick', args),
|
handler: async (args) => sceneBridge.call('simulateButtonClick', args),
|
||||||
},
|
},
|
||||||
|
...createSceneEventTools({ createSchema, sceneBridge }),
|
||||||
|
...createScriptTools({ createSchema, getRuntimeContext }),
|
||||||
{
|
{
|
||||||
name: 'invoke_component_method',
|
name: 'invoke_component_method',
|
||||||
profile: 'full',
|
profile: 'full',
|
||||||
description: '[core] Invoke a method on a component for runtime validation and test hooks.',
|
description: '[core] Invoke a method on a component for runtime validation and test hooks.',
|
||||||
inputSchema: createSchema(
|
inputSchema: createSchema(
|
||||||
{
|
{
|
||||||
path: { type: 'string', description: 'Node hierarchy path.' },
|
path: { type: 'string', description: 'Node hierarchy path, e.g. "Canvas/Player".' },
|
||||||
uuid: { type: 'string', description: 'Node uuid.' },
|
uuid: { type: 'string', description: 'Node uuid.' },
|
||||||
name: { type: 'string', description: 'Fallback exact node name.' },
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
componentName: { type: 'string', description: 'Component class name.' },
|
componentName: { type: 'string', description: 'Component class name, e.g. Sprite, Label, Button. Custom scripts must be compiled.' },
|
||||||
index: { type: 'number', description: 'Optional component index.' },
|
index: { type: 'number', description: 'Optional component index.' },
|
||||||
methodName: { type: 'string', description: 'Method name to invoke.' },
|
methodName: { type: 'string', description: 'Method name to invoke.' },
|
||||||
args: { type: 'array', description: 'Optional argument array.' },
|
args: { type: 'array', description: 'Optional argument array.' },
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { listAssets, queryAssetInfo } = require('../assets');
|
||||||
|
const { resolveProjectPath } = require('../path-safety');
|
||||||
|
|
||||||
|
const UUID_KEY_PATTERN = /uuid|assetUuid|prefabUuid|sceneUuid|__uuid__/i;
|
||||||
|
const UUID_LITERAL_PATTERN = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[A-Za-z0-9+/=-]{20,32}/g;
|
||||||
|
|
||||||
|
function assetUrlToPath(projectPath, url) {
|
||||||
|
if (!url || !String(url).startsWith('db://assets/')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return path.join(projectPath, String(url).slice('db://'.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function assetFilePath(projectPath, info) {
|
||||||
|
const candidates = [
|
||||||
|
info && info.file,
|
||||||
|
info && info.path,
|
||||||
|
info && info.source,
|
||||||
|
info && info.url ? assetUrlToPath(projectPath, info.url) : '',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const fullPath = resolveProjectPath(projectPath, candidate);
|
||||||
|
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectStructuredUuidReferences(value, refs = [], pointer = '') {
|
||||||
|
if (value == null) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item, index) => collectStructuredUuidReferences(item, refs, `${pointer}/${index}`));
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, child] of Object.entries(value)) {
|
||||||
|
const childPointer = `${pointer}/${key}`;
|
||||||
|
if (typeof child === 'string' && UUID_KEY_PATTERN.test(key)) {
|
||||||
|
refs.push({ uuid: child, path: childPointer, key, source: 'structured' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collectStructuredUuidReferences(child, refs, childPointer);
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectTextUuidReferences(text) {
|
||||||
|
const refs = [];
|
||||||
|
const seen = new Set();
|
||||||
|
let match;
|
||||||
|
while ((match = UUID_LITERAL_PATTERN.exec(String(text || '')))) {
|
||||||
|
const uuid = match[0];
|
||||||
|
if (seen.has(uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(uuid);
|
||||||
|
refs.push({ uuid, path: `@${match.index}`, key: '', source: 'text' });
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeReferences(refs) {
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
for (const ref of refs) {
|
||||||
|
const key = `${ref.uuid}:${ref.path}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
result.push(ref);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUuidReferences(content) {
|
||||||
|
const refs = [];
|
||||||
|
try {
|
||||||
|
refs.push(...collectStructuredUuidReferences(JSON.parse(content)));
|
||||||
|
} catch (error) {
|
||||||
|
// Non-JSON assets still get a literal reference scan below.
|
||||||
|
}
|
||||||
|
refs.push(...collectTextUuidReferences(content));
|
||||||
|
return dedupeReferences(refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspectAssetDependencies(projectPath, target, options = {}) {
|
||||||
|
const info = await queryAssetInfo(target);
|
||||||
|
const isDirectory = info && (info.isDirectory || info.type === 'folder' || (info.url && !path.extname(info.url)));
|
||||||
|
const filePath = assetFilePath(projectPath, info);
|
||||||
|
if (!filePath) {
|
||||||
|
if (isDirectory) {
|
||||||
|
throw new Error(`Cannot inspect dependencies of a directory asset: ${target}. Please specify a file asset (e.g. a .prefab, .scene, or .mat file).`);
|
||||||
|
}
|
||||||
|
throw new Error(`Asset file was not found: ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(500, options.limit)) : 200;
|
||||||
|
const references = collectUuidReferences(content).slice(0, limit);
|
||||||
|
const dependencies = [];
|
||||||
|
const missing = [];
|
||||||
|
|
||||||
|
for (const ref of references) {
|
||||||
|
try {
|
||||||
|
const asset = await queryAssetInfo(ref.uuid);
|
||||||
|
dependencies.push({
|
||||||
|
...ref,
|
||||||
|
exists: true,
|
||||||
|
asset: {
|
||||||
|
uuid: asset.uuid,
|
||||||
|
url: asset.url,
|
||||||
|
type: asset.type,
|
||||||
|
importer: asset.importer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
missing.push({ ...ref, exists: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: missing.length === 0,
|
||||||
|
target,
|
||||||
|
asset: {
|
||||||
|
uuid: info.uuid,
|
||||||
|
url: info.url,
|
||||||
|
type: info.type,
|
||||||
|
},
|
||||||
|
filePath: path.relative(projectPath, filePath).replace(/\\/g, '/'),
|
||||||
|
referenceCount: references.length,
|
||||||
|
dependencyCount: dependencies.length,
|
||||||
|
missingCount: missing.length,
|
||||||
|
dependencies,
|
||||||
|
missing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateAssetDependencies(projectPath, options = {}) {
|
||||||
|
const targets = options.target
|
||||||
|
? [options.target]
|
||||||
|
: (await listAssets({ pattern: options.pattern || 'db://assets/**', ccType: options.ccType }))
|
||||||
|
.slice(0, Number.isFinite(options.limit) ? Math.max(1, Math.min(200, options.limit)) : 50)
|
||||||
|
.map((asset) => asset.uuid || asset.url)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const assets = [];
|
||||||
|
for (const target of targets) {
|
||||||
|
try {
|
||||||
|
assets.push(await inspectAssetDependencies(projectPath, target, options));
|
||||||
|
} catch (error) {
|
||||||
|
assets.push({
|
||||||
|
ok: false,
|
||||||
|
target,
|
||||||
|
error: error.message,
|
||||||
|
missingCount: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingCount = assets.reduce((sum, asset) => sum + (Number(asset.missingCount) || 0), 0);
|
||||||
|
return {
|
||||||
|
ok: missingCount === 0,
|
||||||
|
assetCount: assets.length,
|
||||||
|
missingCount,
|
||||||
|
assets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAssetsAdvancedTools({ createSchema, getRuntimeContext }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'inspect_asset_dependencies',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Inspect UUID-style dependencies referenced by a serialized Cocos asset.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Asset uuid, db url, or project path.' },
|
||||||
|
limit: { type: 'number', description: 'Maximum dependency references to inspect.' },
|
||||||
|
},
|
||||||
|
['target']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return await inspectAssetDependencies(projectPath, args.target, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'validate_asset_dependencies',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Validate UUID-style dependencies for one asset or a project asset query.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
target: { type: 'string', description: 'Optional asset uuid, db url, or project path.' },
|
||||||
|
pattern: { type: 'string', description: 'Asset-db pattern used when target is omitted.' },
|
||||||
|
ccType: { type: 'string', description: 'Optional Cocos asset type filter.' },
|
||||||
|
limit: { type: 'number', description: 'Maximum assets to scan when target is omitted.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return await validateAssetDependencies(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'batch_asset_ops',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Batch import or delete assets in the Cocos asset database.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['import', 'delete'],
|
||||||
|
description: 'Batch operation.',
|
||||||
|
},
|
||||||
|
sourceDirectory: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Local filesystem source path (import only).',
|
||||||
|
},
|
||||||
|
targetDirectory: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset-db target URL, e.g. db://assets/textures (import only).',
|
||||||
|
},
|
||||||
|
urls: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Asset URLs to delete (delete only).',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['action']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const action = String(args.action || '').trim();
|
||||||
|
|
||||||
|
if (action === 'import') {
|
||||||
|
const sourceDirectory = String(args.sourceDirectory || '').trim();
|
||||||
|
const targetDirectory = String(args.targetDirectory || '').trim();
|
||||||
|
if (!sourceDirectory) {
|
||||||
|
throw new Error('sourceDirectory is required for action: import.');
|
||||||
|
}
|
||||||
|
if (!targetDirectory) {
|
||||||
|
throw new Error('targetDirectory is required for action: import.');
|
||||||
|
}
|
||||||
|
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
||||||
|
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
|
||||||
|
}
|
||||||
|
const result = await Editor.Message.request('asset-db', 'import-asset', sourceDirectory, targetDirectory);
|
||||||
|
return { action, sourceDirectory, targetDirectory, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'delete') {
|
||||||
|
const urls = Array.isArray(args.urls) ? args.urls.filter(Boolean) : [];
|
||||||
|
if (urls.length === 0) {
|
||||||
|
throw new Error('urls is required for action: delete.');
|
||||||
|
}
|
||||||
|
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
||||||
|
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
|
||||||
|
}
|
||||||
|
const results = [];
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
await Editor.Message.request('asset-db', 'delete-asset', url);
|
||||||
|
results.push({ url, deleted: true });
|
||||||
|
} catch (error) {
|
||||||
|
results.push({ url, deleted: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { action, urls, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown action: ${action}. Must be one of: import, delete.`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'find_unused_assets',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Find assets not referenced by any scene, prefab, or animation in the project.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
directory: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset-db directory to scan. Default: db://assets.',
|
||||||
|
},
|
||||||
|
excludeDirectories: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Directories to exclude from the reference scan.',
|
||||||
|
},
|
||||||
|
assetTypes: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Filter by Cocos asset type, e.g. ["cc.Texture2D", "cc.AudioClip"].',
|
||||||
|
},
|
||||||
|
limit: { type: 'number', description: 'Max results. Default 200.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
return await findUnusedAssets(projectPath, args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERIALIZED_EXTENSIONS = ['.scene', '.prefab', '.anim', '.mat'];
|
||||||
|
const SKIP_UNUSED_EXTENSIONS = ['.ts', '.scene'];
|
||||||
|
|
||||||
|
async function findUnusedAssets(projectPath, options = {}, listAssetsFn) {
|
||||||
|
const listAssetsRef = typeof listAssetsFn === 'function' ? listAssetsFn : listAssets;
|
||||||
|
const directory = options.directory || 'db://assets';
|
||||||
|
const excludeDirs = Array.isArray(options.excludeDirectories) ? options.excludeDirectories : [];
|
||||||
|
const assetTypes = Array.isArray(options.assetTypes) ? options.assetTypes : undefined;
|
||||||
|
const limit = Number.isFinite(options.limit) ? Math.max(1, options.limit) : 200;
|
||||||
|
|
||||||
|
const allAssets = await listAssetsRef({
|
||||||
|
pattern: directory + '/**',
|
||||||
|
ccType: assetTypes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const referencedUuids = new Set();
|
||||||
|
const assetsDir = path.join(projectPath, 'assets');
|
||||||
|
|
||||||
|
function scanDir(dir) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (entry.name === 'node_modules' || entry.name === 'temp' || entry.name === 'library') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const relativePath = path.relative(assetsDir, fullPath).replace(/\\/g, '/');
|
||||||
|
if (excludeDirs.some((excl) => relativePath.startsWith(String(excl)))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
scanDir(fullPath);
|
||||||
|
} else {
|
||||||
|
const ext = path.extname(entry.name).toLowerCase();
|
||||||
|
if (!SERIALIZED_EXTENSIONS.includes(ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
const refs = collectUuidReferences(content);
|
||||||
|
for (const ref of refs) {
|
||||||
|
referencedUuids.add(ref.uuid);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Skip unreadable files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanDir(assetsDir);
|
||||||
|
|
||||||
|
const unused = allAssets.filter((asset) => {
|
||||||
|
if (!asset || !asset.uuid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (asset.isDirectory) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const url = asset.url || asset.source || '';
|
||||||
|
const ext = path.extname(url).toLowerCase();
|
||||||
|
if (SKIP_UNUSED_EXTENSIONS.includes(ext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !referencedUuids.has(asset.uuid);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalScanned: allAssets.length,
|
||||||
|
unusedCount: unused.length,
|
||||||
|
referencedCount: referencedUuids.size,
|
||||||
|
unused: unused.slice(0, limit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
collectUuidReferences,
|
||||||
|
createAssetsAdvancedTools,
|
||||||
|
findUnusedAssets,
|
||||||
|
inspectAssetDependencies,
|
||||||
|
validateAssetDependencies,
|
||||||
|
};
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function hasEditorMessage() {
|
||||||
|
return Boolean(global.Editor && Editor.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureEditorMessage() {
|
||||||
|
if (!hasEditorMessage()) {
|
||||||
|
throw new Error('Editor.Message is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestEditorMessage(channel, method, ...args) {
|
||||||
|
ensureEditorMessage();
|
||||||
|
if (typeof Editor.Message.request !== 'function') {
|
||||||
|
throw new Error('Editor.Message.request is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
return await Editor.Message.request(channel, method, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryEditorRequests(candidates) {
|
||||||
|
const attempts = [];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const channel = candidate.channel;
|
||||||
|
const method = candidate.method;
|
||||||
|
const args = Array.isArray(candidate.args) ? candidate.args : [];
|
||||||
|
try {
|
||||||
|
const result = await requestEditorMessage(channel, method, ...args);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
channel,
|
||||||
|
method,
|
||||||
|
result,
|
||||||
|
attempts,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
attempts.push({ channel, method, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = attempts.length
|
||||||
|
? attempts.map((attempt) => `${attempt.channel}.${attempt.method}: ${attempt.error}`).join('; ')
|
||||||
|
: 'no editor message candidates were provided';
|
||||||
|
const error = new Error(`No compatible Cocos editor message succeeded: ${message}`);
|
||||||
|
error.attempts = attempts;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryEditorRequestsStatus(candidates) {
|
||||||
|
try {
|
||||||
|
return await tryEditorRequests(candidates);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
available: false,
|
||||||
|
attempts: error.attempts || [],
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPanel(panelName) {
|
||||||
|
const id = String(panelName || 'builder').trim();
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('panelName is required.');
|
||||||
|
}
|
||||||
|
if (!global.Editor || !Editor.Panel || typeof Editor.Panel.open !== 'function') {
|
||||||
|
throw new Error('Editor.Panel.open is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
const result = await Editor.Panel.open(id);
|
||||||
|
return { opened: true, panelName: id, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEditorPreference(scope, key) {
|
||||||
|
if (!global.Editor || !Editor.Profile) {
|
||||||
|
throw new Error('Editor.Profile is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
const normalizedScope = String(scope || 'project').toLowerCase();
|
||||||
|
const target = normalizedScope === 'global' ? Editor.Profile : Editor.Profile;
|
||||||
|
const getters = normalizedScope === 'global'
|
||||||
|
? ['getConfig', 'getGlobal']
|
||||||
|
: ['getProject', 'getConfig'];
|
||||||
|
for (const getter of getters) {
|
||||||
|
if (typeof target[getter] === 'function') {
|
||||||
|
return target[getter](key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('No compatible Editor.Profile getter is available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorPreference(scope, key, value) {
|
||||||
|
if (!global.Editor || !Editor.Profile) {
|
||||||
|
throw new Error('Editor.Profile is unavailable in this Cocos extension host.');
|
||||||
|
}
|
||||||
|
const normalizedScope = String(scope || 'project').toLowerCase();
|
||||||
|
const target = Editor.Profile;
|
||||||
|
const setters = normalizedScope === 'global'
|
||||||
|
? ['setConfig', 'setGlobal']
|
||||||
|
: ['setProject', 'setConfig'];
|
||||||
|
for (const setter of setters) {
|
||||||
|
if (typeof target[setter] === 'function') {
|
||||||
|
const result = target[setter](key, value);
|
||||||
|
return { set: true, scope: normalizedScope, key, value, method: setter, result };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('No compatible Editor.Profile setter is available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastEditorMessage(options = {}) {
|
||||||
|
ensureEditorMessage();
|
||||||
|
const channel = String(options.channel || '').trim();
|
||||||
|
const message = String(options.message || '').trim();
|
||||||
|
if (!message) {
|
||||||
|
throw new Error('message is required.');
|
||||||
|
}
|
||||||
|
const payload = options.payload === undefined ? {} : options.payload;
|
||||||
|
if (channel && typeof Editor.Message.send === 'function') {
|
||||||
|
const result = Editor.Message.send(channel, message, payload);
|
||||||
|
return { sent: true, mode: 'send', channel, message, payload, result };
|
||||||
|
}
|
||||||
|
if (typeof Editor.Message.broadcast === 'function') {
|
||||||
|
const result = Editor.Message.broadcast(message, payload);
|
||||||
|
return { sent: true, mode: 'broadcast', message, payload, result };
|
||||||
|
}
|
||||||
|
throw new Error('Neither Editor.Message.send nor Editor.Message.broadcast is available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCocosProjectTools({ createSchema }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'save_current_scene',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Save the currently open Cocos scene using available editor scene messages.',
|
||||||
|
inputSchema: createSchema({}, []),
|
||||||
|
handler: async () => {
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{ channel: 'scene', method: 'save-scene' },
|
||||||
|
{ channel: 'scene', method: 'save' },
|
||||||
|
]);
|
||||||
|
return { saved: true, ...result };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'open_build_panel',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Open the Cocos build panel, defaulting to the builder panel id.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
panelName: { type: 'string', description: 'Panel id to open. Defaults to builder.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => openPanel(args.panelName || 'builder'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_build_status',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Query Cocos build/preview status using known builder message variants.',
|
||||||
|
inputSchema: createSchema({}, []),
|
||||||
|
handler: async () => await tryEditorRequestsStatus([
|
||||||
|
{ channel: 'builder', method: 'query-build-status' },
|
||||||
|
{ channel: 'builder', method: 'get-build-status' },
|
||||||
|
{ channel: 'builder', method: 'query-build-tasks' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'run_project_preview',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Start Cocos preview/run using known preview and builder message variants.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
platform: { type: 'string', description: 'Optional preview platform or build target.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => await tryEditorRequests([
|
||||||
|
{ channel: 'preview', method: 'open', args: [args || {}] },
|
||||||
|
{ channel: 'preview', method: 'start', args: [args || {}] },
|
||||||
|
{ channel: 'preview', method: 'open-preview', args: [args || {}] },
|
||||||
|
{ channel: 'builder', method: 'preview', args: [args || {}] },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_editor_preference',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Read a Cocos editor preference through Editor.Profile when available.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
scope: { type: 'string', description: 'Preference scope: project or global. Defaults to project.' },
|
||||||
|
key: { type: 'string', description: 'Preference key.' },
|
||||||
|
},
|
||||||
|
['key']
|
||||||
|
),
|
||||||
|
handler: async (args) => ({
|
||||||
|
scope: args.scope || 'project',
|
||||||
|
key: args.key,
|
||||||
|
value: getEditorPreference(args.scope, args.key),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'set_editor_preference',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Write a Cocos editor preference through Editor.Profile when available.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
scope: { type: 'string', description: 'Preference scope: project or global. Defaults to project.' },
|
||||||
|
key: { type: 'string', description: 'Preference key.' },
|
||||||
|
valueJson: { type: 'string', description: 'JSON encoded preference value.' },
|
||||||
|
},
|
||||||
|
['key', 'valueJson']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
let value;
|
||||||
|
try {
|
||||||
|
value = JSON.parse(args.valueJson);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`valueJson must be valid JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
return setEditorPreference(args.scope, args.key, value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'broadcast_editor_message',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Send or broadcast a Cocos editor message for advanced editor automation.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
channel: { type: 'string', description: 'Optional Editor.Message channel for send().' },
|
||||||
|
message: { type: 'string', description: 'Message name to send or broadcast.' },
|
||||||
|
payload: { type: 'object', description: 'Optional JSON payload.' },
|
||||||
|
},
|
||||||
|
['message']
|
||||||
|
),
|
||||||
|
handler: async (args) => broadcastEditorMessage(args),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
broadcastEditorMessage,
|
||||||
|
createCocosProjectTools,
|
||||||
|
getEditorPreference,
|
||||||
|
setEditorPreference,
|
||||||
|
tryEditorRequests,
|
||||||
|
tryEditorRequestsStatus,
|
||||||
|
};
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { resolveProjectPath } = require('../path-safety');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a focused, line-numbered snippet around one file line.
|
||||||
|
* @param {string} filePath Absolute file path.
|
||||||
|
* @param {number} lineNumber One-based target line number.
|
||||||
|
* @param {number} contextLines Surrounding lines to include.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function buildSnippet(filePath, lineNumber, contextLines = 3) {
|
||||||
|
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
||||||
|
const start = Math.max(1, Number(lineNumber || 1) - Math.max(0, contextLines));
|
||||||
|
const end = Math.min(lines.length, Number(lineNumber || 1) + Math.max(0, contextLines));
|
||||||
|
const snippet = [];
|
||||||
|
for (let line = start; line <= end; line += 1) {
|
||||||
|
const marker = line === Number(lineNumber || 1) ? '>' : ' ';
|
||||||
|
snippet.push(`${marker} ${String(line).padStart(4, ' ')} | ${lines[line - 1]}`);
|
||||||
|
}
|
||||||
|
return snippet.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesPattern(fileName, pattern) {
|
||||||
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
||||||
|
return new RegExp(`^${escaped}$`, 'i').test(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchFiles(rootDir, pattern, limit) {
|
||||||
|
const results = [];
|
||||||
|
if (!fs.existsSync(rootDir)) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = [rootDir];
|
||||||
|
while (stack.length && results.length < limit) {
|
||||||
|
const current = stack.pop();
|
||||||
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'temp' || entry.name === 'library') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(current, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
stack.push(fullPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchesPattern(entry.name, pattern)) {
|
||||||
|
results.push(fullPath);
|
||||||
|
if (results.length >= limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceAllLiteral(content, search, replacement) {
|
||||||
|
if (!search) {
|
||||||
|
throw new Error('search text is required.');
|
||||||
|
}
|
||||||
|
return content.split(search).join(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort refresh for Cocos asset database after external file edits.
|
||||||
|
* @param {string} projectPath Active Cocos project root.
|
||||||
|
* @param {string} targetPath Absolute file or directory path.
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async function refreshAssets(projectPath, targetPath) {
|
||||||
|
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
|
||||||
|
return 'Asset refresh API is unavailable; Cocos Creator should pick up file changes automatically.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const relative = path.relative(path.join(projectPath, 'assets'), targetPath).replace(/\\/g, '/');
|
||||||
|
if (!relative.startsWith('..')) {
|
||||||
|
const dbUrl = `db://assets/${relative}`;
|
||||||
|
try {
|
||||||
|
await Editor.Message.request('asset-db', 'refresh-asset', dbUrl);
|
||||||
|
return `Refreshed asset database for ${dbUrl}`;
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets');
|
||||||
|
return `Refreshed asset database after writing ${dbUrl}`;
|
||||||
|
} catch (innerError) {
|
||||||
|
return `File written, but asset refresh failed: ${innerError.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'File written outside assets directory; no asset-db refresh was needed.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File-system tools. Kept separate from the registry so path safety and asset
|
||||||
|
* refresh behavior can be tested and evolved independently.
|
||||||
|
*/
|
||||||
|
function createFileTools({ createSchema, getRuntimeContext }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'read_file',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Read a file from the Cocos project.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||||
|
},
|
||||||
|
['path']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(`File not found: ${args.path}`);
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
return content.length > 12000 ? `${content.slice(0, 12000)}\n... (truncated)` : content;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_file_snippet',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Read a focused snippet around a file line number.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||||
|
line: { type: 'number', description: 'Target line number, starting at 1.' },
|
||||||
|
contextLines: { type: 'number', description: 'Number of surrounding context lines.' },
|
||||||
|
},
|
||||||
|
['path', 'line']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(`File not found: ${args.path}`);
|
||||||
|
}
|
||||||
|
return buildSnippet(fullPath, args.line, Number.isFinite(args.contextLines) ? args.contextLines : 3);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'write_file',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Write or overwrite a file in the Cocos project.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||||
|
content: { type: 'string', description: 'File content to write.' },
|
||||||
|
},
|
||||||
|
['path', 'content']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, args.content, 'utf8');
|
||||||
|
return `Wrote ${args.content.length} chars to ${args.path}\n${await refreshAssets(projectPath, fullPath)}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'replace_in_file',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Replace text in a file, useful for script auto-fix loops.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Project-relative or absolute file path.' },
|
||||||
|
search: { type: 'string', description: 'Literal text to search for.' },
|
||||||
|
replace: { type: 'string', description: 'Replacement text.' },
|
||||||
|
replaceAll: { type: 'boolean', description: 'Replace every occurrence instead of only the first.' },
|
||||||
|
},
|
||||||
|
['path', 'search', 'replace']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const fullPath = resolveProjectPath(projectPath, args.path);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(`File not found: ${args.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const original = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
if (!original.includes(args.search)) {
|
||||||
|
throw new Error(`Search text was not found in ${args.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = args.replaceAll
|
||||||
|
? replaceAllLiteral(original, args.search, args.replace)
|
||||||
|
: original.replace(args.search, args.replace);
|
||||||
|
|
||||||
|
fs.writeFileSync(fullPath, updated, 'utf8');
|
||||||
|
return `Updated ${args.path} (${args.replaceAll ? 'all matches' : 'first match'})\n${await refreshAssets(projectPath, fullPath)}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'search_files',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Search project files by simple wildcard pattern.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
pattern: { type: 'string', description: "Wildcard file pattern such as '*.ts' or 'Player*'." },
|
||||||
|
directory: { type: 'string', description: 'Project-relative search root. Defaults to assets.' },
|
||||||
|
limit: { type: 'number', description: 'Maximum number of results to return.' },
|
||||||
|
},
|
||||||
|
['pattern']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const searchRoot = resolveProjectPath(projectPath, args.directory || 'assets');
|
||||||
|
if (!fs.existsSync(searchRoot)) {
|
||||||
|
throw new Error(`Directory not found: ${args.directory || 'assets'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(500, args.limit)) : 100;
|
||||||
|
const results = searchFiles(searchRoot, args.pattern, limit).map((fullPath) =>
|
||||||
|
path.relative(projectPath, fullPath).replace(/\\/g, '/')
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
count: results.length,
|
||||||
|
files: results,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list_directory',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] List files and directories inside a project directory.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Project-relative or absolute directory path.' },
|
||||||
|
},
|
||||||
|
['path']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const targetPath = resolveProjectPath(projectPath, args.path);
|
||||||
|
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
||||||
|
throw new Error(`Directory not found: ${args.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = fs
|
||||||
|
.readdirSync(targetPath, { withFileTypes: true })
|
||||||
|
.filter((entry) => !entry.name.startsWith('.'))
|
||||||
|
.map((entry) => ({
|
||||||
|
name: entry.name,
|
||||||
|
type: entry.isDirectory() ? 'directory' : 'file',
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
path: args.path,
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'exists',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Check whether a project file or directory exists.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Project-relative or absolute path.' },
|
||||||
|
},
|
||||||
|
['path']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const targetPath = resolveProjectPath(projectPath, args.path);
|
||||||
|
return {
|
||||||
|
path: args.path,
|
||||||
|
exists: fs.existsSync(targetPath),
|
||||||
|
isFile: fs.existsSync(targetPath) ? fs.statSync(targetPath).isFile() : false,
|
||||||
|
isDirectory: fs.existsSync(targetPath) ? fs.statSync(targetPath).isDirectory() : false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'refresh_assets',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Best-effort asset database refresh for a file or the assets root.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Optional project-relative file path to refresh.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const targetPath = resolveProjectPath(projectPath, args.path || 'assets');
|
||||||
|
return await refreshAssets(projectPath, targetPath);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildSnippet,
|
||||||
|
createFileTools,
|
||||||
|
refreshAssets,
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createSceneEventTools({ createSchema, sceneBridge }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'list_button_click_events',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] List click event bindings on a Cocos Button component.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Button node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Button node uuid.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact button node name.' },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('listButtonClickEvents', args),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bind_button_click_event',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Bind a Cocos Button click event to a target node component method.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Button node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Button node uuid.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact button node name.' },
|
||||||
|
targetPath: { type: 'string', description: 'Target node path containing the handler component.' },
|
||||||
|
targetUuid: { type: 'string', description: 'Target node uuid containing the handler component.' },
|
||||||
|
targetName: { type: 'string', description: 'Fallback exact target node name.' },
|
||||||
|
componentName: { type: 'string', description: 'Target component class name.' },
|
||||||
|
handler: { type: 'string', description: 'Method name to invoke on the target component.' },
|
||||||
|
customEventData: { type: 'string', description: 'Optional custom event data string.' },
|
||||||
|
replace: { type: 'boolean', description: 'Replace an identical existing binding.' },
|
||||||
|
},
|
||||||
|
['componentName', 'handler']
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('bindButtonClickEvent', args),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createSceneEventTools,
|
||||||
|
};
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { tryEditorRequests } = require('./cocos-project');
|
||||||
|
const { openAsset } = require('../assets');
|
||||||
|
|
||||||
|
const TEMPLATE_PATH = path.join(__dirname, '..', '..', 'resources', 'template.scene');
|
||||||
|
|
||||||
|
function buildSceneContent(sceneName, templatePath) {
|
||||||
|
const tplPath = templatePath || TEMPLATE_PATH;
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(tplPath, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to read scene template at ${tplPath}: ${err.message}`);
|
||||||
|
}
|
||||||
|
const json = JSON.parse(content);
|
||||||
|
if (json[0] && json[0]._name !== undefined) {
|
||||||
|
json[0]._name = sceneName;
|
||||||
|
}
|
||||||
|
if (json[1] && json[1]._name !== undefined) {
|
||||||
|
json[1]._name = sceneName;
|
||||||
|
}
|
||||||
|
return JSON.stringify(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSceneManagementTools({ createSchema, sceneBridge }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'create_scene',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a new scene asset in the Cocos project and optionally open it.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
sceneName: { type: 'string', description: 'Name of the new scene (without extension).' },
|
||||||
|
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' },
|
||||||
|
open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' },
|
||||||
|
overwrite: { type: 'boolean', description: 'Overwrite if the asset already exists. Default false.' },
|
||||||
|
},
|
||||||
|
['sceneName', 'savePath']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const sceneName = String(args.sceneName || '').trim();
|
||||||
|
const savePath = String(args.savePath || '').trim();
|
||||||
|
if (!sceneName) {
|
||||||
|
throw new Error('sceneName is required.');
|
||||||
|
}
|
||||||
|
if (!savePath) {
|
||||||
|
throw new Error('savePath is required.');
|
||||||
|
}
|
||||||
|
if (!savePath.endsWith('.scene')) {
|
||||||
|
throw new Error('savePath must end with .scene');
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = buildSceneContent(sceneName);
|
||||||
|
const createArgs = [savePath, content];
|
||||||
|
if (args.overwrite === true) {
|
||||||
|
createArgs.push({ overwrite: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{ channel: 'asset-db', method: 'create-asset', args: createArgs },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const url = (result.result && (result.result.url || result.result.source)) || savePath;
|
||||||
|
const uuid = result.result && result.result.uuid;
|
||||||
|
|
||||||
|
if (args.open !== false) {
|
||||||
|
try {
|
||||||
|
await openAsset(url);
|
||||||
|
} catch (_) {
|
||||||
|
// Opening is best-effort; creation succeeded.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: true,
|
||||||
|
sceneName,
|
||||||
|
url,
|
||||||
|
uuid,
|
||||||
|
opened: args.open !== false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'query_scene_state',
|
||||||
|
profile: 'core',
|
||||||
|
description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['is_dirty', 'is_ready', 'soft_reload'],
|
||||||
|
description: 'State query or action.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['action']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const action = String(args.action || '').trim();
|
||||||
|
const IPC_MAP = {
|
||||||
|
is_dirty: [
|
||||||
|
{ channel: 'scene', method: 'query-dirty' },
|
||||||
|
],
|
||||||
|
is_ready: [
|
||||||
|
{ channel: 'scene', method: 'query-is-ready' },
|
||||||
|
],
|
||||||
|
soft_reload: [
|
||||||
|
{ channel: 'scene', method: 'soft-reload' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const candidates = IPC_MAP[action];
|
||||||
|
if (!candidates) {
|
||||||
|
throw new Error(`Unknown action: ${action}. Must be one of: is_dirty, is_ready, soft_reload.`);
|
||||||
|
}
|
||||||
|
const result = await tryEditorRequests(candidates);
|
||||||
|
return {
|
||||||
|
action,
|
||||||
|
result: result.result,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'copy_paste_node',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['copy', 'cut', 'paste', 'duplicate'],
|
||||||
|
description: 'Clipboard or duplication action.',
|
||||||
|
},
|
||||||
|
uuids: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Node UUIDs to copy, cut, or duplicate.',
|
||||||
|
},
|
||||||
|
target: { type: 'string', description: 'Target parent node UUID for paste.' },
|
||||||
|
keepWorldTransform: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Keep world transform on paste. Default false.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['action']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const action = String(args.action || '').trim();
|
||||||
|
|
||||||
|
if (action === 'copy' || action === 'cut') {
|
||||||
|
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
||||||
|
if (uuids.length === 0) {
|
||||||
|
throw new Error(`uuids is required for action: ${action}.`);
|
||||||
|
}
|
||||||
|
const method = action === 'copy' ? 'copy-node' : 'cut-node';
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{ channel: 'scene', method, args: [uuids] },
|
||||||
|
]);
|
||||||
|
return { action, uuids, result: result.result };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'duplicate') {
|
||||||
|
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
||||||
|
if (uuids.length === 0) {
|
||||||
|
throw new Error('uuids is required for action: duplicate.');
|
||||||
|
}
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{ channel: 'scene', method: 'duplicate-node', args: [uuids] },
|
||||||
|
]);
|
||||||
|
return { action, uuids, result: result.result };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'paste') {
|
||||||
|
const target = String(args.target || '').trim();
|
||||||
|
if (!target) {
|
||||||
|
throw new Error('target is required for action: paste.');
|
||||||
|
}
|
||||||
|
const keepWorldTransform = args.keepWorldTransform === true;
|
||||||
|
const pasteOpts = { target, keepWorldTransform };
|
||||||
|
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
||||||
|
if (uuids.length > 0) {
|
||||||
|
pasteOpts.uuids = uuids;
|
||||||
|
}
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{
|
||||||
|
channel: 'scene',
|
||||||
|
method: 'paste-node',
|
||||||
|
args: [pasteOpts],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return { action, target, keepWorldTransform, result: result.result };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown action: ${action}. Must be one of: copy, cut, paste, duplicate.`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rename_node',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Rename a scene node.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Node hierarchy path.' },
|
||||||
|
uuid: { type: 'string', description: 'Node UUID.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
|
newName: { type: 'string', description: 'New name for the node.' },
|
||||||
|
},
|
||||||
|
['newName']
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('renameNode', args),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reparent_node',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Move a node to a new parent in the scene hierarchy.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
path: { type: 'string', description: 'Node hierarchy path to move.' },
|
||||||
|
uuid: { type: 'string', description: 'Node UUID to move.' },
|
||||||
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
||||||
|
targetPath: { type: 'string', description: 'New parent node path.' },
|
||||||
|
targetUuid: { type: 'string', description: 'New parent node UUID.' },
|
||||||
|
targetName: { type: 'string', description: 'Fallback exact new parent name.' },
|
||||||
|
siblingIndex: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Optional insert position among siblings. Default: append.',
|
||||||
|
},
|
||||||
|
keepWorldTransform: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Keep world transform after reparenting. Default false.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
handler: async (args) => sceneBridge.call('reparentNode', args),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildSceneContent,
|
||||||
|
createSceneManagementTools,
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { resolveProjectPath } = require('../path-safety');
|
||||||
|
const { refreshAssets } = require('./files');
|
||||||
|
|
||||||
|
const PRIMITIVE_TYPES = new Set(['Number', 'String', 'Boolean']);
|
||||||
|
|
||||||
|
function generateComponentTemplate(className, properties = []) {
|
||||||
|
const ccTypes = new Set(['Component']);
|
||||||
|
const propLines = [];
|
||||||
|
|
||||||
|
for (const prop of properties) {
|
||||||
|
const propName = String(prop.name || 'property').trim();
|
||||||
|
const propType = String(prop.type || 'Number').trim();
|
||||||
|
const defaultValue = prop.default !== undefined ? prop.default : getDefaultForType(propType);
|
||||||
|
|
||||||
|
if (!PRIMITIVE_TYPES.has(propType)) {
|
||||||
|
ccTypes.add(propType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PRIMITIVE_TYPES.has(propType)) {
|
||||||
|
propLines.push(` @property`);
|
||||||
|
propLines.push(` ${propName}: ${typeToTs(propType)} = ${defaultValue};`);
|
||||||
|
} else {
|
||||||
|
propLines.push(` @property(${propType})`);
|
||||||
|
propLines.push(` ${propName}: ${propType} | null = null;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importTypes = Array.from(ccTypes).join(', ');
|
||||||
|
const propsBlock = propLines.length > 0
|
||||||
|
? propLines.join('\n')
|
||||||
|
: ' // Add properties here';
|
||||||
|
|
||||||
|
return `import { _decorator, ${importTypes} } from 'cc';
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('${className}')
|
||||||
|
export class ${className} extends Component {
|
||||||
|
${propsBlock}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePlainTemplate(className) {
|
||||||
|
return `export class ${className} {
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultForType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'Number': return '0';
|
||||||
|
case 'String': return "''";
|
||||||
|
case 'Boolean': return 'false';
|
||||||
|
default: return 'null';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeToTs(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'Number': return 'number';
|
||||||
|
case 'String': return 'string';
|
||||||
|
case 'Boolean': return 'boolean';
|
||||||
|
default: return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScriptTools({ createSchema, getRuntimeContext }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'create_script',
|
||||||
|
profile: 'full',
|
||||||
|
description: '[core] Create a new TypeScript component script with a standard Cocos template.',
|
||||||
|
inputSchema: createSchema(
|
||||||
|
{
|
||||||
|
scriptName: { type: 'string', description: 'Class name in PascalCase, e.g. PlayerController.' },
|
||||||
|
savePath: { type: 'string', description: 'Asset-db URL or project path, e.g. db://assets/scripts/PlayerController.ts.' },
|
||||||
|
template: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['component', 'plain'],
|
||||||
|
description: 'Script template. Default: component.',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'object' },
|
||||||
|
description: 'Optional @property declarations. Each item: { name, type, default }. Type can be Number, String, Boolean, or a CC class name like Label, Sprite, Node.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['scriptName', 'savePath']
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const scriptName = String(args.scriptName || '').trim();
|
||||||
|
let savePath = String(args.savePath || '').trim();
|
||||||
|
if (!scriptName) {
|
||||||
|
throw new Error('scriptName is required.');
|
||||||
|
}
|
||||||
|
if (!savePath) {
|
||||||
|
throw new Error('savePath is required.');
|
||||||
|
}
|
||||||
|
if (!savePath.endsWith('.ts')) {
|
||||||
|
throw new Error('savePath must end with .ts');
|
||||||
|
}
|
||||||
|
if (savePath.startsWith('db://')) {
|
||||||
|
savePath = savePath.slice('db://'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateType = args.template === 'plain' ? 'plain' : 'component';
|
||||||
|
const properties = Array.isArray(args.properties) ? args.properties : [];
|
||||||
|
|
||||||
|
const content = templateType === 'plain'
|
||||||
|
? generatePlainTemplate(scriptName)
|
||||||
|
: generateComponentTemplate(scriptName, properties);
|
||||||
|
|
||||||
|
const { projectPath } = getRuntimeContext();
|
||||||
|
const fullPath = resolveProjectPath(projectPath, savePath);
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, content, 'utf8');
|
||||||
|
|
||||||
|
const refreshMessage = await refreshAssets(projectPath, fullPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: true,
|
||||||
|
scriptName,
|
||||||
|
savePath,
|
||||||
|
template: templateType,
|
||||||
|
className: scriptName,
|
||||||
|
contentLength: content.length,
|
||||||
|
refreshMessage,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createScriptTools,
|
||||||
|
generateComponentTemplate,
|
||||||
|
generatePlainTemplate,
|
||||||
|
};
|
||||||
+51
-4
@@ -1,17 +1,64 @@
|
|||||||
{
|
{
|
||||||
"name": "funplay-cocos-mcp",
|
"name": "funplay-cocos-mcp",
|
||||||
"package_version": 2,
|
"package_version": 2,
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.",
|
"description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.",
|
||||||
"author": "Funplay",
|
"author": "Funplay",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "browser.js",
|
"main": "browser.js",
|
||||||
|
"bin": {
|
||||||
|
"funplay-cocos-mcp": "bin/funplay-cocos-mcp.js"
|
||||||
|
},
|
||||||
|
"mcpName": "io.github.FunplayAI/funplay-cocos-mcp",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/FunplayAI/funplay-cocos-mcp.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/FunplayAI/funplay-cocos-mcp#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/FunplayAI/funplay-cocos-mcp/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"cocos",
|
||||||
|
"cocos-creator",
|
||||||
|
"funplay",
|
||||||
|
"model-context-protocol",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/",
|
||||||
|
"docs/",
|
||||||
|
"lib/",
|
||||||
|
"panel/",
|
||||||
|
"resources/",
|
||||||
|
"browser.js",
|
||||||
|
"scene.js",
|
||||||
|
"server.json",
|
||||||
|
"README.md",
|
||||||
|
"README_CN.md",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"CONTRIBUTING.md",
|
||||||
|
"LICENSE",
|
||||||
|
"RELEASE_WORKFLOW.md",
|
||||||
|
"RELEASE_CHECKLIST.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"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/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",
|
"check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check bin/funplay-cocos-mcp.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/javascript-safety.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-profiles.js && node --check lib/tool-registry.js && node --check lib/tools/assets-advanced.js && node --check lib/tools/cocos-project.js && node --check lib/tools/files.js && node --check lib/tools/scene-events.js && node --check lib/tools/scene-management.js && node --check lib/tools/scripts.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/generate-tool-docs.js && node --check scripts/release.js",
|
||||||
"test": "node --test",
|
"test": "node --test",
|
||||||
"release:check": "node scripts/release.js check",
|
"docs:generate": "node scripts/generate-tool-docs.js",
|
||||||
|
"docs:check": "node scripts/generate-tool-docs.js --check",
|
||||||
|
"pack:dry-run": "npm pack --dry-run",
|
||||||
|
"registry:validate": "mcp-publisher validate server.json",
|
||||||
|
"release:check": "node scripts/release.js check && npm run docs:check",
|
||||||
"release:package": "node scripts/release.js package",
|
"release:package": "node scripts/release.js package",
|
||||||
"release:verify": "npm run check && npm test && npm run release:check && npm run release:package"
|
"release:verify": "npm run check && npm test && npm run release:check && npm run pack:dry-run && npm run release:package"
|
||||||
},
|
},
|
||||||
"panels": {
|
"panels": {
|
||||||
"default": {
|
"default": {
|
||||||
|
|||||||
+486
-6
@@ -34,6 +34,8 @@ module.exports = Editor.Panel.define({
|
|||||||
</label>
|
</label>
|
||||||
<ui-button id="restartBtn">Restart</ui-button>
|
<ui-button id="restartBtn">Restart</ui-button>
|
||||||
<ui-button id="copyUrlBtn">Copy URL</ui-button>
|
<ui-button id="copyUrlBtn">Copy URL</ui-button>
|
||||||
|
<ui-button id="copyHealthCurlBtn">Copy Health Curl</ui-button>
|
||||||
|
<ui-button id="copyToolsCurlBtn">Copy Tools Curl</ui-button>
|
||||||
<ui-button id="checkUpdatesBtn">Check Updates</ui-button>
|
<ui-button id="checkUpdatesBtn">Check Updates</ui-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -49,6 +51,10 @@ module.exports = Editor.Panel.define({
|
|||||||
<ui-checkbox id="sessionsInput"></ui-checkbox>
|
<ui-checkbox id="sessionsInput"></ui-checkbox>
|
||||||
MCP Sessions
|
MCP Sessions
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox-line">
|
||||||
|
<ui-checkbox id="javascriptSafetyInput"></ui-checkbox>
|
||||||
|
JavaScript Safety Checks
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="updateStatus" class="client-status muted"></div>
|
<div id="updateStatus" class="client-status muted"></div>
|
||||||
<p>Changes auto-save. Port/profile changes restart the server when needed.</p>
|
<p>Changes auto-save. Port/profile changes restart the server when needed.</p>
|
||||||
@@ -62,6 +68,19 @@ module.exports = Editor.Panel.define({
|
|||||||
<ui-button id="useFullBtn">Full</ui-button>
|
<ui-button id="useFullBtn">Full</ui-button>
|
||||||
<ui-button id="useCustomBtn">Custom</ui-button>
|
<ui-button id="useCustomBtn">Custom</ui-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="profile-manager">
|
||||||
|
<div class="row">
|
||||||
|
<ui-input id="toolProfileNameInput" placeholder="Profile name"></ui-input>
|
||||||
|
<ui-select id="savedToolProfileSelect"></ui-select>
|
||||||
|
<ui-button id="saveToolProfileBtn">Save Profile</ui-button>
|
||||||
|
<ui-button id="applyToolProfileBtn">Apply</ui-button>
|
||||||
|
<ui-button id="deleteToolProfileBtn">Delete</ui-button>
|
||||||
|
<ui-button id="exportToolProfilesBtn">Export</ui-button>
|
||||||
|
<ui-button id="importToolProfilesBtn">Import</ui-button>
|
||||||
|
</div>
|
||||||
|
<ui-textarea id="toolProfileImportText"></ui-textarea>
|
||||||
|
</div>
|
||||||
|
<div id="categoryControls" class="category-controls"></div>
|
||||||
<div class="tool-config-grid">
|
<div class="tool-config-grid">
|
||||||
<label>Enabled Categories <ui-textarea id="enabledCategoriesInput"></ui-textarea></label>
|
<label>Enabled Categories <ui-textarea id="enabledCategoriesInput"></ui-textarea></label>
|
||||||
<label>Disabled Categories <ui-textarea id="disabledCategoriesInput"></ui-textarea></label>
|
<label>Disabled Categories <ui-textarea id="disabledCategoriesInput"></ui-textarea></label>
|
||||||
@@ -70,6 +89,20 @@ module.exports = Editor.Panel.define({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Activity</h2>
|
||||||
|
<div class="activity-grid">
|
||||||
|
<div class="activity-column">
|
||||||
|
<h3>Recent Calls</h3>
|
||||||
|
<div id="recentCalls" class="mini-list muted"></div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-column">
|
||||||
|
<h3>Log Preview</h3>
|
||||||
|
<div id="recentLogs" class="mini-list muted"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>MCP Client Config</h2>
|
<h2>MCP Client Config</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -114,6 +147,12 @@ module.exports = Editor.Panel.define({
|
|||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-normal-contrast-weak);
|
||||||
|
}
|
||||||
p {
|
p {
|
||||||
margin: 4px 0 0 0;
|
margin: 4px 0 0 0;
|
||||||
color: var(--color-normal-contrast-weakest);
|
color: var(--color-normal-contrast-weakest);
|
||||||
@@ -158,6 +197,54 @@ module.exports = Editor.Panel.define({
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.profile-manager {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
#toolProfileNameInput {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
#savedToolProfileSelect {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
#toolProfileImportText {
|
||||||
|
min-height: 54px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.category-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.category-row {
|
||||||
|
border: 1px solid var(--color-normal-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(0,0,0,0.10);
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.category-heading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.category-name {
|
||||||
|
color: var(--color-normal-contrast);
|
||||||
|
font-weight: 600;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.category-count {
|
||||||
|
color: var(--color-normal-contrast-weakest);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.category-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -199,6 +286,46 @@ module.exports = Editor.Panel.define({
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
.activity-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.mini-list {
|
||||||
|
min-height: 74px;
|
||||||
|
max-height: 158px;
|
||||||
|
overflow: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--color-normal-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(0,0,0,0.12);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.mini-item {
|
||||||
|
padding: 0 0 8px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
|
.mini-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.mini-title {
|
||||||
|
color: var(--color-normal-contrast);
|
||||||
|
font-weight: 600;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.mini-meta {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--color-normal-contrast-weakest);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.mini-body {
|
||||||
|
margin-top: 2px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
details {
|
details {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -232,6 +359,14 @@ module.exports = Editor.Panel.define({
|
|||||||
.primary {
|
.primary {
|
||||||
border-color: #4aa3ff;
|
border-color: #4aa3ff;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.activity-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.category-controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
$: {
|
$: {
|
||||||
root: '.mcp-root',
|
root: '.mcp-root',
|
||||||
@@ -242,14 +377,26 @@ module.exports = Editor.Panel.define({
|
|||||||
portInput: '#portInput',
|
portInput: '#portInput',
|
||||||
profileSelect: '#profileSelect',
|
profileSelect: '#profileSelect',
|
||||||
sessionsInput: '#sessionsInput',
|
sessionsInput: '#sessionsInput',
|
||||||
|
javascriptSafetyInput: '#javascriptSafetyInput',
|
||||||
restartBtn: '#restartBtn',
|
restartBtn: '#restartBtn',
|
||||||
copyUrlBtn: '#copyUrlBtn',
|
copyUrlBtn: '#copyUrlBtn',
|
||||||
|
copyHealthCurlBtn: '#copyHealthCurlBtn',
|
||||||
|
copyToolsCurlBtn: '#copyToolsCurlBtn',
|
||||||
checkUpdatesBtn: '#checkUpdatesBtn',
|
checkUpdatesBtn: '#checkUpdatesBtn',
|
||||||
updateStatus: '#updateStatus',
|
updateStatus: '#updateStatus',
|
||||||
toolSummary: '#toolSummary',
|
toolSummary: '#toolSummary',
|
||||||
useCoreBtn: '#useCoreBtn',
|
useCoreBtn: '#useCoreBtn',
|
||||||
useFullBtn: '#useFullBtn',
|
useFullBtn: '#useFullBtn',
|
||||||
useCustomBtn: '#useCustomBtn',
|
useCustomBtn: '#useCustomBtn',
|
||||||
|
toolProfileNameInput: '#toolProfileNameInput',
|
||||||
|
savedToolProfileSelect: '#savedToolProfileSelect',
|
||||||
|
saveToolProfileBtn: '#saveToolProfileBtn',
|
||||||
|
applyToolProfileBtn: '#applyToolProfileBtn',
|
||||||
|
deleteToolProfileBtn: '#deleteToolProfileBtn',
|
||||||
|
exportToolProfilesBtn: '#exportToolProfilesBtn',
|
||||||
|
importToolProfilesBtn: '#importToolProfilesBtn',
|
||||||
|
toolProfileImportText: '#toolProfileImportText',
|
||||||
|
categoryControls: '#categoryControls',
|
||||||
enabledCategoriesInput: '#enabledCategoriesInput',
|
enabledCategoriesInput: '#enabledCategoriesInput',
|
||||||
disabledCategoriesInput: '#disabledCategoriesInput',
|
disabledCategoriesInput: '#disabledCategoriesInput',
|
||||||
enabledToolsInput: '#enabledToolsInput',
|
enabledToolsInput: '#enabledToolsInput',
|
||||||
@@ -258,6 +405,8 @@ module.exports = Editor.Panel.define({
|
|||||||
configureClientBtn: '#configureClientBtn',
|
configureClientBtn: '#configureClientBtn',
|
||||||
clientTargetStatus: '#clientTargetStatus',
|
clientTargetStatus: '#clientTargetStatus',
|
||||||
clientConfigText: '#clientConfigText',
|
clientConfigText: '#clientConfigText',
|
||||||
|
recentCalls: '#recentCalls',
|
||||||
|
recentLogs: '#recentLogs',
|
||||||
output: '#output',
|
output: '#output',
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -282,32 +431,160 @@ module.exports = Editor.Panel.define({
|
|||||||
const portText = status.portFallbackActive
|
const portText = status.portFallbackActive
|
||||||
? ` | Port fallback: ${status.requestedPort} -> ${status.port}`
|
? ` | Port fallback: ${status.requestedPort} -> ${status.port}`
|
||||||
: '';
|
: '';
|
||||||
|
const attachText = status.attachedToExisting ? ' | Attached listener' : '';
|
||||||
this.$.statusText.textContent =
|
this.$.statusText.textContent =
|
||||||
`${status.url || ''} | Project: ${status.projectName || ''} | Cocos ${status.cocosVersion || ''}${portText}`;
|
`${status.url || ''} | Project: ${status.projectName || ''} | Cocos ${status.cocosVersion || ''}${portText}${attachText}`;
|
||||||
|
|
||||||
this.$.enabledInput.value = Boolean(isRunning || config.autostart);
|
this.$.enabledInput.value = Boolean(isRunning || config.autostart);
|
||||||
this.$.portInput.value = Number(config.port || status.port || 8765);
|
this.$.portInput.value = Number(config.port || status.port || 8765);
|
||||||
this.$.profileSelect.value = config.toolProfile || status.toolProfile || 'core';
|
this.$.profileSelect.value = config.toolProfile || status.toolProfile || 'core';
|
||||||
this.$.sessionsInput.value = Boolean(config.enableSessions || status.enableSessions);
|
this.$.sessionsInput.value = Boolean(config.enableSessions || status.enableSessions);
|
||||||
|
this.$.javascriptSafetyInput.value = config.executeJavascriptSafetyChecks !== false;
|
||||||
this.$.enabledCategoriesInput.value = this.formatList(config.enabledToolCategories);
|
this.$.enabledCategoriesInput.value = this.formatList(config.enabledToolCategories);
|
||||||
this.$.disabledCategoriesInput.value = this.formatList(config.disabledToolCategories);
|
this.$.disabledCategoriesInput.value = this.formatList(config.disabledToolCategories);
|
||||||
this.$.enabledToolsInput.value = this.formatList(config.enabledTools);
|
this.$.enabledToolsInput.value = this.formatList(config.enabledTools);
|
||||||
this.$.disabledToolsInput.value = this.formatList(config.disabledTools);
|
this.$.disabledToolsInput.value = this.formatList(config.disabledTools);
|
||||||
|
this.renderToolProfiles();
|
||||||
this.renderUpdateStatus();
|
this.renderUpdateStatus();
|
||||||
this.renderToolSummary();
|
this.renderToolSummary();
|
||||||
|
this.renderCategoryControls();
|
||||||
this.$.clientConfigText.value = state.clientConfig ? state.clientConfig.codex : '';
|
|
||||||
this.renderClientTargets();
|
this.renderClientTargets();
|
||||||
|
this.renderActivity();
|
||||||
},
|
},
|
||||||
formatList(value) {
|
formatList(value) {
|
||||||
return Array.isArray(value) ? value.join('\n') : '';
|
return Array.isArray(value) ? value.join('\n') : '';
|
||||||
},
|
},
|
||||||
parseList(value) {
|
parseList(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.split(/[\n,]/)
|
.split(/[\n,]/)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
|
normalizeToolProfile(profile) {
|
||||||
|
const name = String(profile && profile.name || '').trim();
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('Profile name is required.');
|
||||||
|
}
|
||||||
|
const mode = String(profile.toolProfile || 'core').toLowerCase();
|
||||||
|
return {
|
||||||
|
name: name.slice(0, 80),
|
||||||
|
toolProfile: mode === 'full' || mode === 'custom' ? mode : 'core',
|
||||||
|
enabledToolCategories: this.parseList(profile.enabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
disabledToolCategories: this.parseList(profile.disabledToolCategories).map((item) => item.toLowerCase()),
|
||||||
|
enabledTools: this.parseList(profile.enabledTools),
|
||||||
|
disabledTools: this.parseList(profile.disabledTools),
|
||||||
|
updatedAt: profile.updatedAt || new Date().toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
normalizeToolProfiles(value) {
|
||||||
|
const result = [];
|
||||||
|
const seen = new Set();
|
||||||
|
(Array.isArray(value) ? value : []).forEach((profile) => {
|
||||||
|
try {
|
||||||
|
const normalized = this.normalizeToolProfile(profile);
|
||||||
|
const key = normalized.name.toLowerCase();
|
||||||
|
const existing = result.findIndex((item) => item.name.toLowerCase() === key);
|
||||||
|
if (existing >= 0) {
|
||||||
|
result[existing] = normalized;
|
||||||
|
} else if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
result.push(normalized);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore malformed imported entries in the panel; backend normalization repeats this.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
},
|
||||||
|
currentToolProfileSnapshot(name) {
|
||||||
|
return this.normalizeToolProfile({
|
||||||
|
name,
|
||||||
|
toolProfile: this.$.profileSelect.value || 'core',
|
||||||
|
enabledToolCategories: this.parseList(this.$.enabledCategoriesInput.value).map((item) => item.toLowerCase()),
|
||||||
|
disabledToolCategories: this.parseList(this.$.disabledCategoriesInput.value).map((item) => item.toLowerCase()),
|
||||||
|
enabledTools: this.parseList(this.$.enabledToolsInput.value),
|
||||||
|
disabledTools: this.parseList(this.$.disabledToolsInput.value),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getSavedToolProfiles() {
|
||||||
|
const config = this.state && this.state.config ? this.state.config : {};
|
||||||
|
return this.normalizeToolProfiles(config.savedToolProfiles || []);
|
||||||
|
},
|
||||||
|
renderToolProfiles() {
|
||||||
|
const config = this.state && this.state.config ? this.state.config : {};
|
||||||
|
const profiles = this.getSavedToolProfiles();
|
||||||
|
const selected = this.$.savedToolProfileSelect.value || config.activeToolProfileName || (profiles[0] && profiles[0].name) || '';
|
||||||
|
this.$.savedToolProfileSelect.innerHTML = '';
|
||||||
|
if (profiles.length) {
|
||||||
|
profiles.forEach((profile) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = profile.name;
|
||||||
|
option.textContent = profile.name;
|
||||||
|
this.$.savedToolProfileSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = '';
|
||||||
|
option.textContent = 'No saved profiles';
|
||||||
|
this.$.savedToolProfileSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
this.$.savedToolProfileSelect.value = selected;
|
||||||
|
if (!this.$.toolProfileNameInput.value) {
|
||||||
|
this.$.toolProfileNameInput.value = selected || config.activeToolProfileName || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderCategoryControls() {
|
||||||
|
const catalog = (this.state && this.state.toolCatalog) || [];
|
||||||
|
const groups = catalog.reduce((acc, tool) => {
|
||||||
|
const category = tool.category || 'other';
|
||||||
|
if (!acc[category]) {
|
||||||
|
acc[category] = { total: 0, enabled: 0 };
|
||||||
|
}
|
||||||
|
acc[category].total += 1;
|
||||||
|
if (tool.enabled) {
|
||||||
|
acc[category].enabled += 1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.$.categoryControls.innerHTML = '';
|
||||||
|
Object.keys(groups).sort().forEach((category) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'category-row';
|
||||||
|
|
||||||
|
const heading = document.createElement('div');
|
||||||
|
heading.className = 'category-heading';
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'category-name';
|
||||||
|
name.textContent = category;
|
||||||
|
const count = document.createElement('div');
|
||||||
|
count.className = 'category-count';
|
||||||
|
count.textContent = `${groups[category].enabled}/${groups[category].total}`;
|
||||||
|
heading.appendChild(name);
|
||||||
|
heading.appendChild(count);
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'category-actions';
|
||||||
|
[
|
||||||
|
['enable', 'Enable'],
|
||||||
|
['disable', 'Disable'],
|
||||||
|
['clear', 'Clear'],
|
||||||
|
].forEach(([mode, label]) => {
|
||||||
|
const button = document.createElement('ui-button');
|
||||||
|
button.textContent = label;
|
||||||
|
button.dataset.category = category;
|
||||||
|
button.dataset.mode = mode;
|
||||||
|
actions.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(heading);
|
||||||
|
row.appendChild(actions);
|
||||||
|
this.$.categoryControls.appendChild(row);
|
||||||
|
});
|
||||||
|
},
|
||||||
renderUpdateStatus() {
|
renderUpdateStatus() {
|
||||||
const update = this.state && this.state.updateInfo;
|
const update = this.state && this.state.updateInfo;
|
||||||
if (!update) {
|
if (!update) {
|
||||||
@@ -329,6 +606,75 @@ module.exports = Editor.Panel.define({
|
|||||||
this.$.toolSummary.textContent =
|
this.$.toolSummary.textContent =
|
||||||
`Enabled ${enabled.length}/${catalog.length} tools | Categories: ${categories.join(', ')}`;
|
`Enabled ${enabled.length}/${catalog.length} tools | Categories: ${categories.join(', ')}`;
|
||||||
},
|
},
|
||||||
|
renderActivity() {
|
||||||
|
const state = this.state || {};
|
||||||
|
this.renderMiniList(
|
||||||
|
this.$.recentCalls,
|
||||||
|
state.recentInteractions || [],
|
||||||
|
(entry) => ({
|
||||||
|
title: `${String(entry.status || '').toUpperCase()} ${entry.toolName || 'tool'}`,
|
||||||
|
meta: this.formatTimestamp(entry.timestamp),
|
||||||
|
body: entry.summary || '',
|
||||||
|
}),
|
||||||
|
'No recent MCP calls.'
|
||||||
|
);
|
||||||
|
this.renderMiniList(
|
||||||
|
this.$.recentLogs,
|
||||||
|
state.recentRuntimeLogs || [],
|
||||||
|
(entry) => ({
|
||||||
|
title: `${String(entry.level || 'info').toUpperCase()} ${entry.message || ''}`,
|
||||||
|
meta: this.formatTimestamp(entry.timestamp),
|
||||||
|
body: entry.details ? stringify(entry.details) : '',
|
||||||
|
}),
|
||||||
|
'No runtime logs yet.'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
renderMiniList(container, entries, formatEntry, emptyText) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (!entries.length) {
|
||||||
|
container.textContent = emptyText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
entries.slice(0, 6).forEach((entry) => {
|
||||||
|
const formatted = formatEntry(entry);
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'mini-item';
|
||||||
|
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.className = 'mini-title';
|
||||||
|
title.textContent = formatted.title;
|
||||||
|
item.appendChild(title);
|
||||||
|
|
||||||
|
if (formatted.meta) {
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'mini-meta';
|
||||||
|
meta.textContent = formatted.meta;
|
||||||
|
item.appendChild(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formatted.body) {
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'mini-body';
|
||||||
|
body.textContent = formatted.body;
|
||||||
|
item.appendChild(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.appendChild(item);
|
||||||
|
});
|
||||||
|
container.appendChild(fragment);
|
||||||
|
},
|
||||||
|
formatTimestamp(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return date.toLocaleTimeString();
|
||||||
|
},
|
||||||
renderClientTargets() {
|
renderClientTargets() {
|
||||||
const targets = (this.state && this.state.clientTargets) || [];
|
const targets = (this.state && this.state.clientTargets) || [];
|
||||||
const preferred = this.state && this.state.config ? this.state.config.lastClientTargetId : '';
|
const preferred = this.state && this.state.config ? this.state.config.lastClientTargetId : '';
|
||||||
@@ -349,10 +695,30 @@ module.exports = Editor.Panel.define({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$.clientTargetStatus.textContent = `${target.configured ? 'Configured' : 'Not configured'}: ${target.configPath}`;
|
this.$.clientTargetStatus.textContent = `${target.configured ? 'Configured' : 'Not configured'}: ${target.configPath}`;
|
||||||
|
const previews = this.state && this.state.clientConfig && Array.isArray(this.state.clientConfig.targets)
|
||||||
|
? this.state.clientConfig.targets
|
||||||
|
: [];
|
||||||
|
const preview = previews.find((item) => item.id === target.id);
|
||||||
|
this.$.clientConfigText.value = preview && preview.preview
|
||||||
|
? preview.preview
|
||||||
|
: (this.state && this.state.clientConfig ? this.state.clientConfig.codex : '');
|
||||||
},
|
},
|
||||||
showOutput(value) {
|
showOutput(value) {
|
||||||
this.$.output.textContent = stringify(value);
|
this.$.output.textContent = stringify(value);
|
||||||
},
|
},
|
||||||
|
copyText(text, successMessage) {
|
||||||
|
if (!text) {
|
||||||
|
this.showOutput('Nothing to copy.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
.then(() => this.showOutput(successMessage))
|
||||||
|
.catch(() => this.showOutput(text));
|
||||||
|
},
|
||||||
|
getCurlCommand(key) {
|
||||||
|
const curl = this.state && this.state.clientConfig && this.state.clientConfig.curl;
|
||||||
|
return curl && curl[key] ? curl[key] : '';
|
||||||
|
},
|
||||||
async persistConfig(options = {}) {
|
async persistConfig(options = {}) {
|
||||||
const { showOutput = false } = options;
|
const { showOutput = false } = options;
|
||||||
try {
|
try {
|
||||||
@@ -387,11 +753,103 @@ module.exports = Editor.Panel.define({
|
|||||||
enabledTools: this.parseList(this.$.enabledToolsInput.value),
|
enabledTools: this.parseList(this.$.enabledToolsInput.value),
|
||||||
disabledTools: this.parseList(this.$.disabledToolsInput.value),
|
disabledTools: this.parseList(this.$.disabledToolsInput.value),
|
||||||
enableSessions: Boolean(this.$.sessionsInput.value),
|
enableSessions: Boolean(this.$.sessionsInput.value),
|
||||||
|
executeJavascriptSafetyChecks: Boolean(this.$.javascriptSafetyInput.value),
|
||||||
autostart: Boolean(this.$.enabledInput.value),
|
autostart: Boolean(this.$.enabledInput.value),
|
||||||
maxInteractionLogEntries: this.state && this.state.config ? this.state.config.maxInteractionLogEntries : 50,
|
maxInteractionLogEntries: this.state && this.state.config ? this.state.config.maxInteractionLogEntries : 50,
|
||||||
lastClientTargetId: this.$.clientTargetSelect.value || 'claude_code',
|
lastClientTargetId: this.$.clientTargetSelect.value || 'claude_code',
|
||||||
|
activeToolProfileName: this.$.toolProfileNameInput.value || '',
|
||||||
|
savedToolProfiles: this.getSavedToolProfiles(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async saveCurrentToolProfile() {
|
||||||
|
const name = this.$.toolProfileNameInput.value || this.$.savedToolProfileSelect.value;
|
||||||
|
const snapshot = this.currentToolProfileSnapshot(name);
|
||||||
|
const profiles = this.getSavedToolProfiles();
|
||||||
|
const key = snapshot.name.toLowerCase();
|
||||||
|
const existing = profiles.findIndex((profile) => profile.name.toLowerCase() === key);
|
||||||
|
if (existing >= 0) {
|
||||||
|
profiles[existing] = snapshot;
|
||||||
|
} else {
|
||||||
|
profiles.push(snapshot);
|
||||||
|
}
|
||||||
|
this.state.config.savedToolProfiles = this.normalizeToolProfiles(profiles);
|
||||||
|
this.state.config.activeToolProfileName = snapshot.name;
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
|
async applySavedToolProfile() {
|
||||||
|
const name = this.$.savedToolProfileSelect.value;
|
||||||
|
const profile = this.getSavedToolProfiles().find((item) => item.name === name);
|
||||||
|
if (!profile) {
|
||||||
|
this.showOutput('Select a saved profile first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$.profileSelect.value = profile.toolProfile;
|
||||||
|
this.$.enabledCategoriesInput.value = this.formatList(profile.enabledToolCategories);
|
||||||
|
this.$.disabledCategoriesInput.value = this.formatList(profile.disabledToolCategories);
|
||||||
|
this.$.enabledToolsInput.value = this.formatList(profile.enabledTools);
|
||||||
|
this.$.disabledToolsInput.value = this.formatList(profile.disabledTools);
|
||||||
|
this.$.toolProfileNameInput.value = profile.name;
|
||||||
|
this.state.config.activeToolProfileName = profile.name;
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
|
async deleteSavedToolProfile() {
|
||||||
|
const name = this.$.savedToolProfileSelect.value;
|
||||||
|
if (!name) {
|
||||||
|
this.showOutput('Select a saved profile first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.config.savedToolProfiles = this.getSavedToolProfiles()
|
||||||
|
.filter((profile) => profile.name !== name);
|
||||||
|
if (this.state.config.activeToolProfileName === name) {
|
||||||
|
this.state.config.activeToolProfileName = '';
|
||||||
|
}
|
||||||
|
this.$.toolProfileNameInput.value = '';
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
|
exportSavedToolProfiles() {
|
||||||
|
const payload = JSON.stringify({ version: 1, profiles: this.getSavedToolProfiles() }, null, 2);
|
||||||
|
this.$.toolProfileImportText.value = payload;
|
||||||
|
this.copyText(payload, 'Copied tool profiles to clipboard.');
|
||||||
|
},
|
||||||
|
async importSavedToolProfiles() {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(this.$.toolProfileImportText.value || '{}');
|
||||||
|
const incoming = Array.isArray(payload)
|
||||||
|
? payload
|
||||||
|
: Array.isArray(payload.profiles)
|
||||||
|
? payload.profiles
|
||||||
|
: [];
|
||||||
|
if (!incoming.length) {
|
||||||
|
throw new Error('No profiles found.');
|
||||||
|
}
|
||||||
|
this.state.config.savedToolProfiles = this.normalizeToolProfiles([
|
||||||
|
...this.getSavedToolProfiles(),
|
||||||
|
...incoming,
|
||||||
|
]);
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
} catch (error) {
|
||||||
|
this.showOutput(`Import profiles failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setCategoryExposure(category, mode) {
|
||||||
|
const enabled = new Set(this.parseList(this.$.enabledCategoriesInput.value).map((item) => item.toLowerCase()));
|
||||||
|
const disabled = new Set(this.parseList(this.$.disabledCategoriesInput.value).map((item) => item.toLowerCase()));
|
||||||
|
const key = String(category || '').toLowerCase();
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enabled.delete(key);
|
||||||
|
disabled.delete(key);
|
||||||
|
if (mode === 'enable') {
|
||||||
|
enabled.add(key);
|
||||||
|
} else if (mode === 'disable') {
|
||||||
|
disabled.add(key);
|
||||||
|
}
|
||||||
|
this.$.profileSelect.value = 'custom';
|
||||||
|
this.$.enabledCategoriesInput.value = Array.from(enabled).sort().join('\n');
|
||||||
|
this.$.disabledCategoriesInput.value = Array.from(disabled).sort().join('\n');
|
||||||
|
await this.persistConfig({ showOutput: true });
|
||||||
|
},
|
||||||
async handleEnableToggle() {
|
async handleEnableToggle() {
|
||||||
const shouldEnable = Boolean(this.$.enabledInput.value);
|
const shouldEnable = Boolean(this.$.enabledInput.value);
|
||||||
const wasRunning = Boolean(this.state && this.state.status && this.state.status.running);
|
const wasRunning = Boolean(this.state && this.state.status && this.state.status.running);
|
||||||
@@ -415,15 +873,20 @@ module.exports = Editor.Panel.define({
|
|||||||
this.$.copyUrlBtn.addEventListener('click', () => {
|
this.$.copyUrlBtn.addEventListener('click', () => {
|
||||||
const status = this.state && this.state.status;
|
const status = this.state && this.state.status;
|
||||||
const text = status && status.url ? status.url : '';
|
const text = status && status.url ? status.url : '';
|
||||||
navigator.clipboard.writeText(text)
|
this.copyText(text, 'Copied URL to clipboard.');
|
||||||
.then(() => this.showOutput('Copied URL to clipboard.'))
|
});
|
||||||
.catch(() => this.showOutput(text));
|
this.$.copyHealthCurlBtn.addEventListener('click', () => {
|
||||||
|
this.copyText(this.getCurlCommand('health'), 'Copied health curl command.');
|
||||||
|
});
|
||||||
|
this.$.copyToolsCurlBtn.addEventListener('click', () => {
|
||||||
|
this.copyText(this.getCurlCommand('tools'), 'Copied tools curl command.');
|
||||||
});
|
});
|
||||||
this.$.checkUpdatesBtn.addEventListener('click', () => this.runAction(() => request('check-updates')));
|
this.$.checkUpdatesBtn.addEventListener('click', () => this.runAction(() => request('check-updates')));
|
||||||
this.$.enabledInput.addEventListener('change', () => this.handleEnableToggle());
|
this.$.enabledInput.addEventListener('change', () => this.handleEnableToggle());
|
||||||
this.$.portInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.portInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.profileSelect.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.profileSelect.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.sessionsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.sessionsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
|
this.$.javascriptSafetyInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.enabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.enabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.disabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.disabledCategoriesInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
this.$.enabledToolsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
this.$.enabledToolsInput.addEventListener('change', () => this.persistConfig({ showOutput: true }));
|
||||||
@@ -448,6 +911,23 @@ module.exports = Editor.Panel.define({
|
|||||||
this.$.profileSelect.value = 'custom';
|
this.$.profileSelect.value = 'custom';
|
||||||
this.persistConfig({ showOutput: true });
|
this.persistConfig({ showOutput: true });
|
||||||
});
|
});
|
||||||
|
this.$.saveToolProfileBtn.addEventListener('click', () => this.saveCurrentToolProfile());
|
||||||
|
this.$.applyToolProfileBtn.addEventListener('click', () => this.applySavedToolProfile());
|
||||||
|
this.$.deleteToolProfileBtn.addEventListener('click', () => this.deleteSavedToolProfile());
|
||||||
|
this.$.exportToolProfilesBtn.addEventListener('click', () => this.exportSavedToolProfiles());
|
||||||
|
this.$.importToolProfilesBtn.addEventListener('click', () => this.importSavedToolProfiles());
|
||||||
|
this.$.savedToolProfileSelect.addEventListener('change', () => {
|
||||||
|
this.$.toolProfileNameInput.value = this.$.savedToolProfileSelect.value || '';
|
||||||
|
});
|
||||||
|
this.$.categoryControls.addEventListener('click', (event) => {
|
||||||
|
const target = event.target && typeof event.target.closest === 'function'
|
||||||
|
? event.target.closest('ui-button')
|
||||||
|
: event.target;
|
||||||
|
if (!target || !target.dataset || !target.dataset.category) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setCategoryExposure(target.dataset.category, target.dataset.mode);
|
||||||
|
});
|
||||||
this.$.clientTargetSelect.addEventListener('confirm', () => this.renderClientTargetStatus());
|
this.$.clientTargetSelect.addEventListener('confirm', () => this.renderClientTargetStatus());
|
||||||
this.$.clientTargetSelect.addEventListener('change', () => {
|
this.$.clientTargetSelect.addEventListener('change', () => {
|
||||||
this.renderClientTargetStatus();
|
this.renderClientTargetStatus();
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"__type__": "cc.SceneAsset",
|
||||||
|
"_name": "Template",
|
||||||
|
"scene": { "__id__": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.Scene",
|
||||||
|
"_name": "Template",
|
||||||
|
"_children": [],
|
||||||
|
"_globals": { "__id__": 2 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.SceneGlobals",
|
||||||
|
"ambient": { "__id__": 3 },
|
||||||
|
"shadows": { "__id__": 4 },
|
||||||
|
"_skybox": { "__id__": 5 },
|
||||||
|
"fog": { "__id__": 6 },
|
||||||
|
"octree": { "__id__": 7 },
|
||||||
|
"skin": { "__id__": 8 },
|
||||||
|
"lightProbeInfo": { "__id__": 9 },
|
||||||
|
"postSettings": { "__id__": 10 },
|
||||||
|
"bakedWithStationaryMainLight": false,
|
||||||
|
"bakedWithHighpLightmap": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.AmbientInfo",
|
||||||
|
"_skyColorHDR": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0.520833125 },
|
||||||
|
"_skyColor": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0.520833125 },
|
||||||
|
"_skyIllumHDR": 20000,
|
||||||
|
"_skyIllum": 20000,
|
||||||
|
"_groundAlbedoHDR": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0 },
|
||||||
|
"_groundAlbedo": { "__type__": "cc.Vec4", "x": 0, "y": 0, "z": 0, "w": 0 },
|
||||||
|
"_skyColorLDR": { "__type__": "cc.Vec4", "x": 0.2, "y": 0.5, "z": 0.8, "w": 1 },
|
||||||
|
"_skyIllumLDR": 20000,
|
||||||
|
"_groundAlbedoLDR": { "__type__": "cc.Vec4", "x": 0.2, "y": 0.2, "z": 0.2, "w": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.ShadowsInfo",
|
||||||
|
"_enabled": false,
|
||||||
|
"_type": 0,
|
||||||
|
"_normal": { "__type__": "cc.Vec3", "x": 0, "y": 1, "z": 0 },
|
||||||
|
"_distance": 0,
|
||||||
|
"_planeBias": 1,
|
||||||
|
"_shadowColor": { "__type__": "cc.Color", "r": 76, "g": 76, "b": 76, "a": 255 },
|
||||||
|
"_maxReceived": 4,
|
||||||
|
"_size": { "__type__": "cc.Vec2", "x": 512, "y": 512 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.SkyboxInfo",
|
||||||
|
"_envLightingType": 0,
|
||||||
|
"_envmapHDR": null,
|
||||||
|
"_envmap": null,
|
||||||
|
"_envmapLDR": null,
|
||||||
|
"_diffuseMapHDR": null,
|
||||||
|
"_diffuseMapLDR": null,
|
||||||
|
"_enabled": false,
|
||||||
|
"_useHDR": true,
|
||||||
|
"_editableMaterial": null,
|
||||||
|
"_reflectionHDR": null,
|
||||||
|
"_reflectionLDR": null,
|
||||||
|
"_rotationAngle": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.FogInfo",
|
||||||
|
"_type": 0,
|
||||||
|
"_fogColor": { "__type__": "cc.Color", "r": 200, "g": 200, "b": 200, "a": 255 },
|
||||||
|
"_enabled": false,
|
||||||
|
"_fogDensity": 0.3,
|
||||||
|
"_fogStart": 0.5,
|
||||||
|
"_fogEnd": 300,
|
||||||
|
"_fogAtten": 5,
|
||||||
|
"_fogTop": 1.5,
|
||||||
|
"_fogRange": 1.2,
|
||||||
|
"_accurate": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.OctreeInfo",
|
||||||
|
"_enabled": false,
|
||||||
|
"_minPos": { "__type__": "cc.Vec3", "x": -1024, "y": -1024, "z": -1024 },
|
||||||
|
"_maxPos": { "__type__": "cc.Vec3", "x": 1024, "y": 1024, "z": 1024 },
|
||||||
|
"_depth": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.SkinInfo",
|
||||||
|
"_enabled": false,
|
||||||
|
"_blurRadius": 0.01,
|
||||||
|
"_sssIntensity": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.LightProbeInfo",
|
||||||
|
"_giScale": 1,
|
||||||
|
"_giSamples": 1024,
|
||||||
|
"_bounces": 2,
|
||||||
|
"_reduceRinging": 0,
|
||||||
|
"_showProbe": true,
|
||||||
|
"_showWireframe": true,
|
||||||
|
"_showConvex": false,
|
||||||
|
"_data": null,
|
||||||
|
"_lightProbeSphereVolume": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.PostSettingsInfo",
|
||||||
|
"_toneMappingType": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -16,6 +16,7 @@ const {
|
|||||||
SceneAsset,
|
SceneAsset,
|
||||||
js,
|
js,
|
||||||
Component,
|
Component,
|
||||||
|
EventHandler,
|
||||||
Canvas,
|
Canvas,
|
||||||
UITransform,
|
UITransform,
|
||||||
Label,
|
Label,
|
||||||
@@ -241,6 +242,23 @@ function findComponent(node, options = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEventHandlerClass() {
|
||||||
|
return (Component && Component.EventHandler) || EventHandler || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeEventHandler(handler) {
|
||||||
|
if (!handler) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
target: handler.target && handler.target.name ? getNodePath(handler.target) : '',
|
||||||
|
targetUuid: handler.target && handler.target.uuid ? handler.target.uuid : '',
|
||||||
|
component: handler.component || '',
|
||||||
|
handler: handler.handler || '',
|
||||||
|
customEventData: handler.customEventData || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getOrAddComponent(node, componentClass) {
|
function getOrAddComponent(node, componentClass) {
|
||||||
return node.getComponent(componentClass) || node.addComponent(componentClass);
|
return node.getComponent(componentClass) || node.addComponent(componentClass);
|
||||||
}
|
}
|
||||||
@@ -743,6 +761,82 @@ exports.methods = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async renameNode(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Target node was not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldName = node.name;
|
||||||
|
const oldPath = getNodePath(node);
|
||||||
|
const newName = String(options.newName || '').trim();
|
||||||
|
if (!newName) {
|
||||||
|
throw new Error('newName is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
node.name = newName;
|
||||||
|
|
||||||
|
return {
|
||||||
|
renamed: true,
|
||||||
|
oldName,
|
||||||
|
newName,
|
||||||
|
oldPath,
|
||||||
|
newPath: getNodePath(node),
|
||||||
|
uuid: node.uuid,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async reparentNode(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Target node was not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPath = getNodePath(node);
|
||||||
|
const oldParent = node.parent;
|
||||||
|
const keepWorldTransform = options.keepWorldTransform === true;
|
||||||
|
const oldWorldPos = keepWorldTransform ? node.worldPosition.clone() : null;
|
||||||
|
const oldWorldRot = keepWorldTransform ? node.worldRotation.clone() : null;
|
||||||
|
const oldWorldScale = keepWorldTransform ? node.worldScale.clone() : null;
|
||||||
|
|
||||||
|
let newParent = null;
|
||||||
|
if (options.targetUuid) {
|
||||||
|
newParent = findNodeByUuid(options.targetUuid);
|
||||||
|
} else if (options.targetPath) {
|
||||||
|
newParent = findNodeByPath(options.targetPath);
|
||||||
|
} else if (options.targetName) {
|
||||||
|
newParent = findNodeByName(options.targetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newParent) {
|
||||||
|
throw new Error('Target parent node was not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const siblingIndex = Number.isFinite(options.siblingIndex) ? options.siblingIndex : -1;
|
||||||
|
if (siblingIndex >= 0 && siblingIndex <= newParent.children.length) {
|
||||||
|
newParent.insertChild(node, siblingIndex);
|
||||||
|
} else {
|
||||||
|
node.parent = newParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepWorldTransform) {
|
||||||
|
node.worldPosition = oldWorldPos;
|
||||||
|
node.worldRotation = oldWorldRot;
|
||||||
|
node.worldScale = oldWorldScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reparented: true,
|
||||||
|
uuid: node.uuid,
|
||||||
|
oldPath,
|
||||||
|
newPath: getNodePath(node),
|
||||||
|
oldParent: oldParent ? oldParent.name : null,
|
||||||
|
newParent: newParent.name,
|
||||||
|
siblingIndex: node.getSiblingIndex(),
|
||||||
|
keepWorldTransform,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async setNodeTransform(options = {}) {
|
async setNodeTransform(options = {}) {
|
||||||
const node = findNode(options);
|
const node = findNode(options);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
@@ -829,7 +923,7 @@ exports.methods = {
|
|||||||
|
|
||||||
const component = findComponent(node, options);
|
const component = findComponent(node, options);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw new Error('Target component was not found.');
|
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentName = component.constructor ? component.constructor.name : 'UnknownComponent';
|
const componentName = component.constructor ? component.constructor.name : 'UnknownComponent';
|
||||||
@@ -849,7 +943,7 @@ exports.methods = {
|
|||||||
|
|
||||||
const component = findComponent(node, options);
|
const component = findComponent(node, options);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw new Error('Target component was not found.');
|
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -874,7 +968,7 @@ exports.methods = {
|
|||||||
|
|
||||||
const component = findComponent(node, options);
|
const component = findComponent(node, options);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw new Error('Target component was not found.');
|
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setValueByPath(component, options.propertyPath, options.value);
|
setValueByPath(component, options.propertyPath, options.value);
|
||||||
@@ -895,7 +989,7 @@ exports.methods = {
|
|||||||
|
|
||||||
const component = findComponent(node, options);
|
const component = findComponent(node, options);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw new Error('Target component was not found.');
|
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetValueByPath(component, options.propertyPath);
|
resetValueByPath(component, options.propertyPath);
|
||||||
@@ -1345,6 +1439,101 @@ exports.methods = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async listButtonClickEvents(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Target node was not found.');
|
||||||
|
}
|
||||||
|
const button = node.getComponent(Button);
|
||||||
|
if (!button) {
|
||||||
|
throw new Error('Button component was not found on target node.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: getNodePath(node),
|
||||||
|
uuid: node.uuid,
|
||||||
|
clickEventCount: Array.isArray(button.clickEvents) ? button.clickEvents.length : 0,
|
||||||
|
clickEvents: Array.isArray(button.clickEvents)
|
||||||
|
? button.clickEvents.map(serializeEventHandler).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async bindButtonClickEvent(options = {}) {
|
||||||
|
const node = findNode(options);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Button node was not found.');
|
||||||
|
}
|
||||||
|
const button = node.getComponent(Button);
|
||||||
|
if (!button) {
|
||||||
|
throw new Error('Button component was not found on target node.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = findNode({
|
||||||
|
path: options.targetPath,
|
||||||
|
uuid: options.targetUuid,
|
||||||
|
name: options.targetName,
|
||||||
|
});
|
||||||
|
if (!target) {
|
||||||
|
throw new Error('Event target node was not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentName = String(options.componentName || '').trim();
|
||||||
|
const handlerName = String(options.handler || options.handlerName || '').trim();
|
||||||
|
if (!componentName || !handlerName) {
|
||||||
|
throw new Error('componentName and handler are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = findComponent(target, { componentName });
|
||||||
|
if (!component) {
|
||||||
|
throw new Error(`Target component was not found: ${componentName}. Ensure the script is compiled and the component is attached to the target node.`);
|
||||||
|
}
|
||||||
|
if (typeof component[handlerName] !== 'function') {
|
||||||
|
throw new Error(`Target component method was not found: ${componentName}.${handlerName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HandlerClass = getEventHandlerClass();
|
||||||
|
if (!HandlerClass) {
|
||||||
|
throw new Error('Cocos EventHandler class is unavailable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = Array.isArray(button.clickEvents) ? button.clickEvents : [];
|
||||||
|
const duplicate = existing.find((event) => (
|
||||||
|
event &&
|
||||||
|
event.target === target &&
|
||||||
|
event.component === componentName &&
|
||||||
|
event.handler === handlerName &&
|
||||||
|
String(event.customEventData || '') === String(options.customEventData || '')
|
||||||
|
));
|
||||||
|
if (duplicate && options.replace !== true) {
|
||||||
|
return {
|
||||||
|
bound: false,
|
||||||
|
duplicate: true,
|
||||||
|
node: getNodePath(node),
|
||||||
|
event: serializeEventHandler(duplicate),
|
||||||
|
clickEventCount: existing.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new HandlerClass();
|
||||||
|
event.target = target;
|
||||||
|
event.component = componentName;
|
||||||
|
event.handler = handlerName;
|
||||||
|
event.customEventData = String(options.customEventData || '');
|
||||||
|
|
||||||
|
button.clickEvents = options.replace === true
|
||||||
|
? existing.filter((item) => item !== duplicate).concat(event)
|
||||||
|
: existing.concat(event);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bound: true,
|
||||||
|
node: getNodePath(node),
|
||||||
|
uuid: node.uuid,
|
||||||
|
event: serializeEventHandler(event),
|
||||||
|
clickEventCount: button.clickEvents.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async invokeComponentMethod(options = {}) {
|
async invokeComponentMethod(options = {}) {
|
||||||
const node = findNode(options);
|
const node = findNode(options);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
@@ -1352,7 +1541,7 @@ exports.methods = {
|
|||||||
}
|
}
|
||||||
const component = findComponent(node, options);
|
const component = findComponent(node, options);
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw new Error('Target component was not found.');
|
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
|
||||||
}
|
}
|
||||||
const methodName = String(options.methodName || '').trim();
|
const methodName = String(options.methodName || '').trim();
|
||||||
if (!methodName || typeof component[methodName] !== 'function') {
|
if (!methodName || typeof component[methodName] !== 'function') {
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { createToolRegistry } = require('../lib/tool-registry');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
const OUTPUT_PATH = path.join(ROOT, 'docs', 'TOOLS.md');
|
||||||
|
|
||||||
|
function createRegistry(profile) {
|
||||||
|
return createToolRegistry({
|
||||||
|
getRuntimeContext: () => ({
|
||||||
|
config: { toolProfile: profile },
|
||||||
|
projectPath: '/tmp/funplay-cocos-docs-project',
|
||||||
|
version: '0.0.0-docs',
|
||||||
|
}),
|
||||||
|
interactionLog: { add() {} },
|
||||||
|
runtimeLog: { add() {}, list: () => [], clear: () => 0 },
|
||||||
|
sceneBridge: { call: async () => ({ ok: true }) },
|
||||||
|
editorExecutor: async () => ({ ok: true }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToolModel() {
|
||||||
|
const fullRegistry = createRegistry('full');
|
||||||
|
const coreNames = new Set(createRegistry('core').listTools().map((tool) => tool.name));
|
||||||
|
const fullNames = new Set(fullRegistry.listTools().map((tool) => tool.name));
|
||||||
|
const catalog = fullRegistry.listToolCatalog()
|
||||||
|
.map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
category: tool.category || 'other',
|
||||||
|
profile: tool.profile || 'full',
|
||||||
|
enabledInCore: coreNames.has(tool.name),
|
||||||
|
enabledInFull: fullNames.has(tool.name),
|
||||||
|
readOnly: Boolean(tool.annotations && tool.annotations.readOnlyHint),
|
||||||
|
destructive: Boolean(tool.annotations && tool.annotations.destructiveHint),
|
||||||
|
description: normalizeDescription(tool.description),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
coreCount: coreNames.size,
|
||||||
|
fullCount: fullNames.size,
|
||||||
|
catalog,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDescription(description) {
|
||||||
|
return String(description || '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMarkdown(model) {
|
||||||
|
const categories = groupBy(model.catalog, (tool) => tool.category);
|
||||||
|
const lines = [
|
||||||
|
'# Tool Reference',
|
||||||
|
'',
|
||||||
|
'<!-- This file is generated by `npm run docs:generate`. Do not edit by hand. -->',
|
||||||
|
'',
|
||||||
|
`Generated from \`lib/tool-registry.js\`. The default \`core\` profile exposes ${model.coreCount} tools; the \`full\` profile exposes ${model.fullCount} tools.`,
|
||||||
|
'',
|
||||||
|
'## Profile Summary',
|
||||||
|
'',
|
||||||
|
'| Profile | Tool Count | Purpose |',
|
||||||
|
'|---|---:|---|',
|
||||||
|
`| \`core\` | ${model.coreCount} | Focused default surface for common editor automation. |`,
|
||||||
|
`| \`full\` | ${model.fullCount} | All built-in tools, including destructive and low-level helpers. |`,
|
||||||
|
'',
|
||||||
|
'## Core Tools',
|
||||||
|
'',
|
||||||
|
model.catalog
|
||||||
|
.filter((tool) => tool.enabledInCore)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((tool) => `\`${tool.name}\``)
|
||||||
|
.join(', '),
|
||||||
|
'',
|
||||||
|
'## Tools By Category',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const category of Object.keys(categories).sort()) {
|
||||||
|
const tools = categories[category];
|
||||||
|
lines.push(`### ${titleCase(category)}`);
|
||||||
|
lines.push('');
|
||||||
|
lines.push('| Tool | Profiles | Access | Description |');
|
||||||
|
lines.push('|---|---|---|---|');
|
||||||
|
for (const tool of tools) {
|
||||||
|
lines.push(`| \`${tool.name}\` | ${profileLabel(tool)} | ${accessLabel(tool)} | ${escapeTableCell(tool.description)} |`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function profileLabel(tool) {
|
||||||
|
return tool.enabledInCore ? '`core`, `full`' : '`full`';
|
||||||
|
}
|
||||||
|
|
||||||
|
function accessLabel(tool) {
|
||||||
|
if (tool.readOnly) {
|
||||||
|
return 'read-only';
|
||||||
|
}
|
||||||
|
if (tool.destructive) {
|
||||||
|
return 'mutating';
|
||||||
|
}
|
||||||
|
return 'stateful';
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleCase(value) {
|
||||||
|
return String(value)
|
||||||
|
.split(/[-_\s]+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeTableCell(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/\|/g, '\\|')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBy(values, getKey) {
|
||||||
|
return values.reduce((groups, value) => {
|
||||||
|
const key = getKey(value);
|
||||||
|
if (!groups[key]) {
|
||||||
|
groups[key] = [];
|
||||||
|
}
|
||||||
|
groups[key].push(value);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDocs(markdown) {
|
||||||
|
const directory = path.dirname(OUTPUT_PATH);
|
||||||
|
if (!fs.existsSync(directory)) {
|
||||||
|
fs.mkdirSync(directory, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(OUTPUT_PATH, markdown, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDocs(markdown) {
|
||||||
|
const existing = fs.existsSync(OUTPUT_PATH) ? fs.readFileSync(OUTPUT_PATH, 'utf8') : '';
|
||||||
|
if (existing !== markdown) {
|
||||||
|
console.error('docs/TOOLS.md is out of date. Run `npm run docs:generate`.');
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const markdown = buildMarkdown(buildToolModel());
|
||||||
|
if (args.includes('--check')) {
|
||||||
|
checkDocs(markdown);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeDocs(markdown);
|
||||||
|
console.log(`Wrote ${path.relative(ROOT, OUTPUT_PATH)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -17,11 +17,14 @@ const REQUIRED_REPO_FILES = [
|
|||||||
'package.json',
|
'package.json',
|
||||||
'README.md',
|
'README.md',
|
||||||
'README_CN.md',
|
'README_CN.md',
|
||||||
|
'docs/TOOLS.md',
|
||||||
'RELEASE_WORKFLOW.md',
|
'RELEASE_WORKFLOW.md',
|
||||||
'RELEASE_CHECKLIST.md',
|
'RELEASE_CHECKLIST.md',
|
||||||
'CHANGELOG.md',
|
'CHANGELOG.md',
|
||||||
'CONTRIBUTING.md',
|
'CONTRIBUTING.md',
|
||||||
'LICENSE',
|
'LICENSE',
|
||||||
|
'server.json',
|
||||||
|
'bin/funplay-cocos-mcp.js',
|
||||||
'browser.js',
|
'browser.js',
|
||||||
'scene.js',
|
'scene.js',
|
||||||
'panel/index.js',
|
'panel/index.js',
|
||||||
@@ -33,9 +36,12 @@ const PACKAGE_INCLUDES = [
|
|||||||
'package.json',
|
'package.json',
|
||||||
'README.md',
|
'README.md',
|
||||||
'README_CN.md',
|
'README_CN.md',
|
||||||
|
'docs',
|
||||||
'CHANGELOG.md',
|
'CHANGELOG.md',
|
||||||
'CONTRIBUTING.md',
|
'CONTRIBUTING.md',
|
||||||
'LICENSE',
|
'LICENSE',
|
||||||
|
'server.json',
|
||||||
|
'bin',
|
||||||
'browser.js',
|
'browser.js',
|
||||||
'scene.js',
|
'scene.js',
|
||||||
'panel',
|
'panel',
|
||||||
@@ -78,6 +84,13 @@ const FORBIDDEN_NAMES = new Set([
|
|||||||
'.DS_Store'
|
'.DS_Store'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const FORBIDDEN_CONTENT_PATTERNS = [
|
||||||
|
['npm token', /\bnpm_[A-Za-z0-9]{20,}\b/],
|
||||||
|
['GitHub token', /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/],
|
||||||
|
['MCP token', /\bmcp_[A-Za-z0-9_-]{32,}\b/],
|
||||||
|
['private key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/]
|
||||||
|
];
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const command = process.argv[2] || 'check';
|
const command = process.argv[2] || 'check';
|
||||||
const options = parseOptions(process.argv.slice(3));
|
const options = parseOptions(process.argv.slice(3));
|
||||||
@@ -95,6 +108,7 @@ function main() {
|
|||||||
console.log(`- ${artifacts.zipName}`);
|
console.log(`- ${artifacts.zipName}`);
|
||||||
console.log('- release-manifest.json');
|
console.log('- release-manifest.json');
|
||||||
console.log('- SHA256SUMS.txt');
|
console.log('- SHA256SUMS.txt');
|
||||||
|
console.log('- RELEASE_NOTES.md');
|
||||||
console.log('- README.md');
|
console.log('- README.md');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,10 +164,40 @@ function checkRelease(options = {}) {
|
|||||||
errors.push('package.json main must point to an existing file.');
|
errors.push('package.json main must point to an existing file.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!packageJson.bin || packageJson.bin['funplay-cocos-mcp'] !== 'bin/funplay-cocos-mcp.js') {
|
||||||
|
errors.push('package.json bin.funplay-cocos-mcp must point to bin/funplay-cocos-mcp.js.');
|
||||||
|
}
|
||||||
|
|
||||||
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
||||||
errors.push(`package.json version must be semver-like, got: ${version}`);
|
errors.push(`package.json version must be semver-like, got: ${version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverJson = readJson(path.join(ROOT, 'server.json'), errors);
|
||||||
|
if (serverJson) {
|
||||||
|
if (serverJson.name !== packageJson.mcpName) {
|
||||||
|
errors.push('server.json name must match package.json mcpName.');
|
||||||
|
}
|
||||||
|
if (serverJson.version !== version) {
|
||||||
|
errors.push(`server.json version ${serverJson.version} must match package.json version ${version}.`);
|
||||||
|
}
|
||||||
|
const npmPackage = Array.isArray(serverJson.packages)
|
||||||
|
? serverJson.packages.find((entry) => entry && entry.registryType === 'npm')
|
||||||
|
: null;
|
||||||
|
if (!npmPackage) {
|
||||||
|
errors.push('server.json must include an npm package entry.');
|
||||||
|
} else {
|
||||||
|
if (npmPackage.identifier !== packageJson.name) {
|
||||||
|
errors.push(`server.json npm identifier ${npmPackage.identifier} must match package.json name ${packageJson.name}.`);
|
||||||
|
}
|
||||||
|
if (npmPackage.version !== version) {
|
||||||
|
errors.push(`server.json npm package version ${npmPackage.version} must match package.json version ${version}.`);
|
||||||
|
}
|
||||||
|
if (!npmPackage.transport || npmPackage.transport.type !== 'stdio') {
|
||||||
|
errors.push('server.json npm package transport must be stdio.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const relative of REQUIRED_REPO_FILES) {
|
for (const relative of REQUIRED_REPO_FILES) {
|
||||||
if (!fs.existsSync(path.join(ROOT, relative))) {
|
if (!fs.existsSync(path.join(ROOT, relative))) {
|
||||||
errors.push(`Missing required repository file: ${relative}`);
|
errors.push(`Missing required repository file: ${relative}`);
|
||||||
@@ -215,6 +259,7 @@ function packageRelease(context) {
|
|||||||
const stagedFiles = collectFiles(stagingRoot)
|
const stagedFiles = collectFiles(stagingRoot)
|
||||||
.map((filePath) => path.relative(TEMP_DIR, filePath).split(path.sep).join('/'));
|
.map((filePath) => path.relative(TEMP_DIR, filePath).split(path.sep).join('/'));
|
||||||
validateArchivePaths(stagedFiles);
|
validateArchivePaths(stagedFiles);
|
||||||
|
validateArchiveContent(collectFiles(stagingRoot));
|
||||||
|
|
||||||
run('zip', ['-qr', zipPath, PACKAGE_DIR_NAME], { cwd: TEMP_DIR });
|
run('zip', ['-qr', zipPath, PACKAGE_DIR_NAME], { cwd: TEMP_DIR });
|
||||||
validateZipListing(zipPath);
|
validateZipListing(zipPath);
|
||||||
@@ -234,9 +279,13 @@ function packageRelease(context) {
|
|||||||
const readmePath = path.join(releaseDir, 'README.md');
|
const readmePath = path.join(releaseDir, 'README.md');
|
||||||
fs.writeFileSync(readmePath, buildReleaseReadme(context, manifest));
|
fs.writeFileSync(readmePath, buildReleaseReadme(context, manifest));
|
||||||
|
|
||||||
|
const releaseNotesPath = path.join(releaseDir, 'RELEASE_NOTES.md');
|
||||||
|
fs.writeFileSync(releaseNotesPath, buildGitHubReleaseNotes(context, manifest));
|
||||||
|
|
||||||
const checksums = [
|
const checksums = [
|
||||||
checksumLine(zipPath, zipName),
|
checksumLine(zipPath, zipName),
|
||||||
checksumLine(manifestPath, 'release-manifest.json'),
|
checksumLine(manifestPath, 'release-manifest.json'),
|
||||||
|
checksumLine(releaseNotesPath, 'RELEASE_NOTES.md'),
|
||||||
checksumLine(readmePath, 'README.md')
|
checksumLine(readmePath, 'README.md')
|
||||||
].join('');
|
].join('');
|
||||||
fs.writeFileSync(path.join(releaseDir, 'SHA256SUMS.txt'), checksums);
|
fs.writeFileSync(path.join(releaseDir, 'SHA256SUMS.txt'), checksums);
|
||||||
@@ -326,6 +375,101 @@ shasum -a 256 -c SHA256SUMS.txt
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGitHubReleaseNotes(context, manifest) {
|
||||||
|
const sections = parseChangelogSections(context.changelogNotes);
|
||||||
|
const rendered = [];
|
||||||
|
const used = new Set();
|
||||||
|
const order = [
|
||||||
|
['Added', 'Added'],
|
||||||
|
['Optimized', 'Optimized'],
|
||||||
|
['Changed', 'Changed'],
|
||||||
|
['Fixed', 'Fixed'],
|
||||||
|
['Security', 'Security'],
|
||||||
|
['Deprecated', 'Deprecated'],
|
||||||
|
['Removed', 'Removed']
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [sourceHeading, displayHeading] of order) {
|
||||||
|
const section = sections.find((item) => item.heading.toLowerCase() === sourceHeading.toLowerCase());
|
||||||
|
if (!section) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
used.add(section.heading);
|
||||||
|
rendered.push(`## ${displayHeading}`, '', ...section.lines, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const section of sections) {
|
||||||
|
if (used.has(section.heading)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rendered.push(`## ${section.heading}`, '', ...section.lines, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const zip = manifest.artifacts.extensionZip;
|
||||||
|
return [
|
||||||
|
`# Funplay MCP for Cocos ${context.tag}`,
|
||||||
|
'',
|
||||||
|
...rendered,
|
||||||
|
'## Release Assets',
|
||||||
|
'',
|
||||||
|
`- \`${zip.file}\` - Cocos Creator extension package.`,
|
||||||
|
'- `release-manifest.json` - Machine-readable release metadata.',
|
||||||
|
'- `SHA256SUMS.txt` - SHA-256 checksums for release artifacts.',
|
||||||
|
'',
|
||||||
|
'## Publish Channels',
|
||||||
|
'',
|
||||||
|
'- GitHub Release: Cocos Creator extension zip package.',
|
||||||
|
`- npm: \`${context.packageJson.name}@${context.version}\`.`,
|
||||||
|
`- MCP Registry: \`${context.packageJson.mcpName}\` version \`${context.version}\`.`,
|
||||||
|
'',
|
||||||
|
'## Verify',
|
||||||
|
'',
|
||||||
|
'```bash',
|
||||||
|
'shasum -a 256 -c SHA256SUMS.txt',
|
||||||
|
'```',
|
||||||
|
''
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseChangelogSections(notes) {
|
||||||
|
const sections = [];
|
||||||
|
let current = null;
|
||||||
|
|
||||||
|
for (const line of String(notes || '').split(/\r?\n/)) {
|
||||||
|
const heading = /^###\s+(.+?)\s*$/.exec(line);
|
||||||
|
if (heading) {
|
||||||
|
current = {
|
||||||
|
heading: heading[1].trim(),
|
||||||
|
lines: []
|
||||||
|
};
|
||||||
|
sections.push(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
current.lines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections
|
||||||
|
.map((section) => ({
|
||||||
|
heading: section.heading,
|
||||||
|
lines: trimBlankLines(section.lines)
|
||||||
|
}))
|
||||||
|
.filter((section) => section.lines.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimBlankLines(lines) {
|
||||||
|
const trimmed = lines.slice();
|
||||||
|
while (trimmed.length && !trimmed[0].trim()) {
|
||||||
|
trimmed.shift();
|
||||||
|
}
|
||||||
|
while (trimmed.length && !trimmed[trimmed.length - 1].trim()) {
|
||||||
|
trimmed.pop();
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
function validateArchivePaths(paths) {
|
function validateArchivePaths(paths) {
|
||||||
const bad = [];
|
const bad = [];
|
||||||
const prefix = `${PACKAGE_DIR_NAME}/`;
|
const prefix = `${PACKAGE_DIR_NAME}/`;
|
||||||
@@ -373,6 +517,30 @@ function validateZipListing(zipPath) {
|
|||||||
validateArchivePaths(listing);
|
validateArchivePaths(listing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateArchiveContent(filePaths) {
|
||||||
|
const bad = [];
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.size > 1024 * 1024) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const buffer = fs.readFileSync(filePath);
|
||||||
|
if (buffer.includes(0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const text = buffer.toString('utf8');
|
||||||
|
for (const [label, pattern] of FORBIDDEN_CONTENT_PATTERNS) {
|
||||||
|
if (pattern.test(text)) {
|
||||||
|
bad.push(`${path.relative(ROOT, filePath)} (${label})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bad.length > 0) {
|
||||||
|
throw new Error(`Release archive contains sensitive-looking content:\n- ${bad.join('\n- ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isForbiddenTrackedPath(relative) {
|
function isForbiddenTrackedPath(relative) {
|
||||||
const parts = relative.split('/');
|
const parts = relative.split('/');
|
||||||
return parts.some((part) => FORBIDDEN_TRACKED_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part));
|
return parts.some((part) => FORBIDDEN_TRACKED_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part));
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||||
|
"name": "io.github.FunplayAI/funplay-cocos-mcp",
|
||||||
|
"title": "Funplay Cocos MCP",
|
||||||
|
"description": "stdio bridge for the local Cocos Creator Editor MCP server from FunplayAI/funplay-cocos-mcp.",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/FunplayAI/funplay-cocos-mcp",
|
||||||
|
"source": "github"
|
||||||
|
},
|
||||||
|
"version": "0.4.0",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"registryType": "npm",
|
||||||
|
"identifier": "funplay-cocos-mcp",
|
||||||
|
"version": "0.4.0",
|
||||||
|
"transport": {
|
||||||
|
"type": "stdio"
|
||||||
|
},
|
||||||
|
"environmentVariables": [
|
||||||
|
{
|
||||||
|
"name": "FUNPLAY_COCOS_MCP_URL",
|
||||||
|
"description": "Optional local Cocos MCP HTTP endpoint. Defaults to http://127.0.0.1:8765/.",
|
||||||
|
"format": "string",
|
||||||
|
"isRequired": false,
|
||||||
|
"isSecret": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FUNPLAY_COCOS_MCP_TIMEOUT_SECONDS",
|
||||||
|
"description": "Optional HTTP timeout per request in seconds. Defaults to 120.",
|
||||||
|
"format": "string",
|
||||||
|
"isRequired": false,
|
||||||
|
"isSecret": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const { collectUuidReferences } = require('../lib/tools/assets-advanced');
|
||||||
|
|
||||||
|
test('collectUuidReferences finds structured and literal UUID references', () => {
|
||||||
|
const refs = collectUuidReferences(JSON.stringify({
|
||||||
|
__type__: 'cc.Prefab',
|
||||||
|
sprite: { __uuid__: '2d3KcYpS5HCKb6wU0v5c9x' },
|
||||||
|
nested: [{ assetUuid: '550e8400-e29b-41d4-a716-446655440000' }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.equal(refs.some((ref) => ref.uuid === '2d3KcYpS5HCKb6wU0v5c9x' && ref.source === 'structured'), true);
|
||||||
|
assert.equal(refs.some((ref) => ref.uuid === '550e8400-e29b-41d4-a716-446655440000'), true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const {
|
||||||
|
broadcastEditorMessage,
|
||||||
|
getEditorPreference,
|
||||||
|
setEditorPreference,
|
||||||
|
tryEditorRequests,
|
||||||
|
tryEditorRequestsStatus,
|
||||||
|
} = require('../lib/tools/cocos-project');
|
||||||
|
|
||||||
|
test('tryEditorRequests returns the first successful editor message candidate', async () => {
|
||||||
|
const calls = [];
|
||||||
|
global.Editor = {
|
||||||
|
Message: {
|
||||||
|
request: async (channel, method, payload) => {
|
||||||
|
calls.push({ channel, method, payload });
|
||||||
|
if (method === 'bad') {
|
||||||
|
throw new Error('nope');
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tryEditorRequests([
|
||||||
|
{ channel: 'scene', method: 'bad' },
|
||||||
|
{ channel: 'scene', method: 'save-scene', args: [{ force: true }] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.method, 'save-scene');
|
||||||
|
assert.equal(calls.length, 2);
|
||||||
|
} finally {
|
||||||
|
delete global.Editor;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tryEditorRequestsStatus returns an unavailable payload instead of throwing', async () => {
|
||||||
|
global.Editor = {
|
||||||
|
Message: {
|
||||||
|
request: async () => {
|
||||||
|
throw new Error('missing');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tryEditorRequestsStatus([{ channel: 'builder', method: 'query-build-status' }]);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.available, false);
|
||||||
|
assert.equal(result.attempts.length, 1);
|
||||||
|
} finally {
|
||||||
|
delete global.Editor;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preference helpers and broadcast use available Editor APIs', () => {
|
||||||
|
const sent = [];
|
||||||
|
const store = new Map();
|
||||||
|
global.Editor = {
|
||||||
|
Message: {
|
||||||
|
send(channel, message, payload) {
|
||||||
|
sent.push({ channel, message, payload });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Profile: {
|
||||||
|
getProject(key) {
|
||||||
|
return store.get(key);
|
||||||
|
},
|
||||||
|
setProject(key, value) {
|
||||||
|
store.set(key, value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setEditorPreference('project', 'preview.port', 7456);
|
||||||
|
assert.equal(getEditorPreference('project', 'preview.port'), 7456);
|
||||||
|
|
||||||
|
const result = broadcastEditorMessage({ channel: 'scene', message: 'custom-event', payload: { ok: true } });
|
||||||
|
assert.equal(result.sent, true);
|
||||||
|
assert.deepEqual(sent[0], { channel: 'scene', message: 'custom-event', payload: { ok: true } });
|
||||||
|
} finally {
|
||||||
|
delete global.Editor;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const { findUnusedAssets, collectUuidReferences } = require('../lib/tools/assets-advanced');
|
||||||
|
|
||||||
|
function createMockAssetsDir() {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-unused-test-'));
|
||||||
|
const assetsDir = path.join(tmpDir, 'assets');
|
||||||
|
fs.mkdirSync(assetsDir, { recursive: true });
|
||||||
|
return { tmpDir, assetsDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(dir, relPath, content) {
|
||||||
|
const fullPath = path.join(dir, relPath);
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, content, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('findUnusedAssets returns assets not referenced by any serialized file', async () => {
|
||||||
|
const { tmpDir, assetsDir } = createMockAssetsDir();
|
||||||
|
try {
|
||||||
|
const usedUuid = 'aaa11111-bbbb-cccc-dddd-eeeeeeee1111';
|
||||||
|
writeFile(assetsDir, 'textures/used.png', 'fake-png');
|
||||||
|
writeFile(assetsDir, 'textures/used.png.meta', JSON.stringify({ uuid: usedUuid }));
|
||||||
|
|
||||||
|
const unusedUuid = 'aaa22222-bbbb-cccc-dddd-eeeeeeee2222';
|
||||||
|
writeFile(assetsDir, 'textures/unused.png', 'fake-png');
|
||||||
|
writeFile(assetsDir, 'textures/unused.png.meta', JSON.stringify({ uuid: unusedUuid }));
|
||||||
|
|
||||||
|
writeFile(assetsDir, 'scenes/game.scene', JSON.stringify({
|
||||||
|
__type__: 'cc.SceneAsset',
|
||||||
|
scene: { __uuid__: usedUuid },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockListAssets = async () => [
|
||||||
|
{ uuid: usedUuid, url: 'db://assets/textures/used.png', type: 'cc.Texture2D' },
|
||||||
|
{ uuid: unusedUuid, url: 'db://assets/textures/unused.png', type: 'cc.Texture2D' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await findUnusedAssets(tmpDir, {}, mockListAssets);
|
||||||
|
assert.equal(result.totalScanned, 2);
|
||||||
|
assert.equal(result.unusedCount, 1);
|
||||||
|
assert.equal(result.unused[0].uuid, unusedUuid);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('findUnusedAssets skips .ts and .scene files from unused list', async () => {
|
||||||
|
const { tmpDir, assetsDir } = createMockAssetsDir();
|
||||||
|
try {
|
||||||
|
const scriptUuid = 'bbb11111-bbbb-cccc-dddd-eeeeeeee3333';
|
||||||
|
const sceneUuid = 'bbb22222-bbbb-cccc-dddd-eeeeeeee4444';
|
||||||
|
|
||||||
|
writeFile(assetsDir, 'scripts/MyScript.ts', 'export class MyScript {}');
|
||||||
|
writeFile(assetsDir, 'scripts/MyScript.ts.meta', JSON.stringify({ uuid: scriptUuid }));
|
||||||
|
writeFile(assetsDir, 'scenes/level.scene', '{}');
|
||||||
|
writeFile(assetsDir, 'scenes/level.scene.meta', JSON.stringify({ uuid: sceneUuid }));
|
||||||
|
|
||||||
|
const mockListAssets = async () => [
|
||||||
|
{ uuid: scriptUuid, url: 'db://assets/scripts/MyScript.ts', type: 'cc.ScriptAsset' },
|
||||||
|
{ uuid: sceneUuid, url: 'db://assets/scenes/level.scene', type: 'cc.SceneAsset' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await findUnusedAssets(tmpDir, {}, mockListAssets);
|
||||||
|
assert.equal(result.totalScanned, 2);
|
||||||
|
assert.equal(result.unusedCount, 0);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('findUnusedAssets respects excludeDirectories', async () => {
|
||||||
|
const { tmpDir, assetsDir } = createMockAssetsDir();
|
||||||
|
try {
|
||||||
|
const referencedUuid = 'ccc11111-bbbb-cccc-dddd-eeeeeeee5555';
|
||||||
|
const unreferencedUuid = 'ccc22222-bbbb-cccc-dddd-eeeeeeee6666';
|
||||||
|
|
||||||
|
writeFile(assetsDir, 'textures/a.png', 'fake');
|
||||||
|
writeFile(assetsDir, 'textures/a.png.meta', JSON.stringify({ uuid: referencedUuid }));
|
||||||
|
writeFile(assetsDir, 'internal/b.png', 'fake');
|
||||||
|
writeFile(assetsDir, 'internal/b.png.meta', JSON.stringify({ uuid: unreferencedUuid }));
|
||||||
|
|
||||||
|
writeFile(assetsDir, 'internal/hidden.scene', JSON.stringify({
|
||||||
|
__type__: 'cc.SceneAsset',
|
||||||
|
tex: { __uuid__: referencedUuid },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockListAssets = async () => [
|
||||||
|
{ uuid: referencedUuid, url: 'db://assets/textures/a.png', type: 'cc.Texture2D' },
|
||||||
|
{ uuid: unreferencedUuid, url: 'db://assets/internal/b.png', type: 'cc.Texture2D' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await findUnusedAssets(tmpDir, {
|
||||||
|
excludeDirectories: ['internal'],
|
||||||
|
}, mockListAssets);
|
||||||
|
|
||||||
|
assert.equal(result.totalScanned, 2);
|
||||||
|
assert.equal(result.unusedCount, 2);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('findUnusedAssets respects limit option', async () => {
|
||||||
|
const { tmpDir, assetsDir } = createMockAssetsDir();
|
||||||
|
try {
|
||||||
|
const assets = [];
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const uuid = `ddd${i}1111-bbbb-cccc-dddd-eeeeeeee777${i}`;
|
||||||
|
writeFile(assetsDir, `textures/tex${i}.png`, 'fake');
|
||||||
|
writeFile(assetsDir, `textures/tex${i}.png.meta`, JSON.stringify({ uuid }));
|
||||||
|
assets.push({ uuid, url: `db://assets/textures/tex${i}.png`, type: 'cc.Texture2D' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockListAssets = async () => assets;
|
||||||
|
|
||||||
|
const result = await findUnusedAssets(tmpDir, { limit: 3 }, mockListAssets);
|
||||||
|
assert.equal(result.totalScanned, 5);
|
||||||
|
assert.equal(result.unusedCount, 5);
|
||||||
|
assert.equal(result.unused.length, 3);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collectUuidReferences is re-exported and works', () => {
|
||||||
|
const refs = collectUuidReferences(JSON.stringify({
|
||||||
|
__uuid__: 'abc12345-def6-7890-abcd-ef1234567890',
|
||||||
|
}));
|
||||||
|
assert.ok(refs.some((r) => r.uuid === 'abc12345-def6-7890-abcd-ef1234567890'));
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
const {
|
||||||
|
assertJavascriptSafety,
|
||||||
|
inspectJavascriptSafety,
|
||||||
|
} = require('../lib/javascript-safety');
|
||||||
|
|
||||||
|
const PROJECT_PATH = path.resolve('/tmp/funplay-cocos-test-project');
|
||||||
|
|
||||||
|
test('JavaScript safety allows project-local write snippets by default', () => {
|
||||||
|
const result = inspectJavascriptSafety(
|
||||||
|
"fs.writeFileSync(path.join(context.projectPath, 'assets/generated.ts'), 'export {};');",
|
||||||
|
{ projectPath: PROJECT_PATH }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks delete operations', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => assertJavascriptSafety("fs.rmSync(path.join(context.projectPath, 'assets'), { recursive: true });", {
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
}),
|
||||||
|
/delete\/truncate/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks traversal and home path literals', () => {
|
||||||
|
const result = inspectJavascriptSafety(
|
||||||
|
"fs.writeFileSync('../outside.txt', 'x'); fs.writeFileSync('~/secret.txt', 'x');",
|
||||||
|
{ projectPath: PROJECT_PATH }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.violations.some((item) => item.includes('path traversal')), true);
|
||||||
|
assert.equal(result.violations.some((item) => item.includes('user-home')), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks absolute paths outside the project', () => {
|
||||||
|
const result = inspectJavascriptSafety("fs.writeFileSync('/tmp/outside.txt', 'x');", {
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.violations.some((item) => item.includes('absolute path outside')), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JavaScript safety blocks child_process usage', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => assertJavascriptSafety("const cp = require('child_process'); cp.execSync('rm -rf /tmp/x');", {
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
}),
|
||||||
|
/child_process/
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ const os = require('node:os');
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const {
|
const {
|
||||||
|
createCocosMcpProjectSkill,
|
||||||
createProjectSkill,
|
createProjectSkill,
|
||||||
listProjectInstructions,
|
listProjectInstructions,
|
||||||
readProjectInstruction,
|
readProjectInstruction,
|
||||||
@@ -39,6 +40,16 @@ test('createProjectSkill writes a Codex project skill', () => {
|
|||||||
assert.equal(listed.skills.some((skill) => skill.path === result.path), true);
|
assert.equal(listed.skills.some((skill) => skill.path === result.path), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createCocosMcpProjectSkill writes the recommended MCP workflow skill', () => {
|
||||||
|
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-default-skill-'));
|
||||||
|
const result = createCocosMcpProjectSkill(projectPath);
|
||||||
|
|
||||||
|
assert.equal(result.path, '.codex/skills/funplay-cocos-mcp-workflow/SKILL.md');
|
||||||
|
const content = readProjectInstruction(projectPath, result.path).content;
|
||||||
|
assert.match(content, /Funplay Cocos MCP Workflow/);
|
||||||
|
assert.match(content, /inspect_asset_dependencies/);
|
||||||
|
});
|
||||||
|
|
||||||
test('project instruction helpers reject traversal outside the project', () => {
|
test('project instruction helpers reject traversal outside the project', () => {
|
||||||
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-safe-'));
|
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-safe-'));
|
||||||
assert.throws(
|
assert.throws(
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const { createSceneManagementTools, buildSceneContent } = require('../lib/tools/scene-management');
|
||||||
|
|
||||||
|
function makeTools(overrides = {}) {
|
||||||
|
const createSchema = (properties, required) => {
|
||||||
|
const schema = { type: 'object', properties };
|
||||||
|
if (required && required.length) {
|
||||||
|
schema.required = required;
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
|
const calls = [];
|
||||||
|
const sceneBridge = {
|
||||||
|
call: async (method, args) => {
|
||||||
|
calls.push({ method, args });
|
||||||
|
return { ok: true, method, args };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const tools = createSceneManagementTools({ createSchema, sceneBridge, ...overrides });
|
||||||
|
const byName = {};
|
||||||
|
for (const tool of tools) {
|
||||||
|
byName[tool.name] = tool;
|
||||||
|
}
|
||||||
|
return { tools, byName, calls, sceneBridge, createSchema };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockEditor(requestImpl) {
|
||||||
|
global.Editor = {
|
||||||
|
Message: {
|
||||||
|
request: requestImpl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmockEditor() {
|
||||||
|
delete global.Editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('create_scene requires sceneName and savePath', async () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await assert.rejects(() => byName.create_scene.handler({ savePath: 'db://assets/x.scene' }), /sceneName is required/);
|
||||||
|
await assert.rejects(() => byName.create_scene.handler({ sceneName: 'X' }), /savePath is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create_scene rejects non-.scene savePath', async () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await assert.rejects(
|
||||||
|
() => byName.create_scene.handler({ sceneName: 'X', savePath: 'db://assets/x.prefab' }),
|
||||||
|
/must end with \.scene/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSceneContent reads template and replaces scene name', () => {
|
||||||
|
const content = buildSceneContent('MyNewScene');
|
||||||
|
const json = JSON.parse(content);
|
||||||
|
assert.equal(json[0]._name, 'MyNewScene');
|
||||||
|
assert.equal(json[1]._name, 'MyNewScene');
|
||||||
|
assert.equal(json[0].__type__, 'cc.SceneAsset');
|
||||||
|
assert.equal(json[1].__type__, 'cc.Scene');
|
||||||
|
assert.ok(Array.isArray(json[1]._children), 'Scene should have _children array');
|
||||||
|
assert.equal(json[1]._children.length, 0, 'Template scene should have no children');
|
||||||
|
const globalTypes = json.slice(2).map((o) => o.__type__);
|
||||||
|
assert.ok(globalTypes.includes('cc.SceneGlobals'));
|
||||||
|
assert.ok(globalTypes.includes('cc.AmbientInfo'));
|
||||||
|
assert.ok(globalTypes.includes('cc.ShadowsInfo'));
|
||||||
|
assert.ok(globalTypes.includes('cc.SkyboxInfo'));
|
||||||
|
assert.ok(globalTypes.includes('cc.FogInfo'));
|
||||||
|
assert.ok(globalTypes.includes('cc.OctreeInfo'));
|
||||||
|
assert.ok(globalTypes.includes('cc.SkinInfo'));
|
||||||
|
assert.ok(globalTypes.includes('cc.LightProbeInfo'));
|
||||||
|
assert.ok(globalTypes.includes('cc.PostSettingsInfo'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSceneContent throws on missing template file', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => buildSceneContent('Test', '/nonexistent/path/template.scene'),
|
||||||
|
/Failed to read scene template/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create_scene creates asset with template content and returns result', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method, ...args) => {
|
||||||
|
requests.push({ channel, method, args });
|
||||||
|
return { uuid: 'scene-uuid-1', url: 'db://assets/scenes/NewScene.scene' };
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.create_scene.handler({
|
||||||
|
sceneName: 'NewScene',
|
||||||
|
savePath: 'db://assets/scenes/NewScene.scene',
|
||||||
|
open: false,
|
||||||
|
});
|
||||||
|
assert.equal(result.created, true);
|
||||||
|
assert.equal(result.sceneName, 'NewScene');
|
||||||
|
assert.equal(result.uuid, 'scene-uuid-1');
|
||||||
|
assert.equal(result.opened, false);
|
||||||
|
assert.equal(requests.length, 1);
|
||||||
|
assert.equal(requests[0].method, 'create-asset');
|
||||||
|
assert.equal(requests[0].args[0], 'db://assets/scenes/NewScene.scene');
|
||||||
|
const content = requests[0].args[1];
|
||||||
|
assert.ok(typeof content === 'string' && content.length > 0, 'content should be non-empty string');
|
||||||
|
const json = JSON.parse(content);
|
||||||
|
assert.equal(json[0]._name, 'NewScene');
|
||||||
|
assert.equal(json[1]._name, 'NewScene');
|
||||||
|
assert.equal(json[0].__type__, 'cc.SceneAsset');
|
||||||
|
assert.equal(json[1].__type__, 'cc.Scene');
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create_scene with overwrite passes option to create-asset', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method, ...args) => {
|
||||||
|
requests.push({ channel, method, args });
|
||||||
|
return { uuid: 'scene-uuid-2', url: 'db://assets/scenes/OverwriteScene.scene' };
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await byName.create_scene.handler({
|
||||||
|
sceneName: 'OverwriteScene',
|
||||||
|
savePath: 'db://assets/scenes/OverwriteScene.scene',
|
||||||
|
open: false,
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
assert.equal(requests[0].args.length, 3, 'should have 3 args: url, content, option');
|
||||||
|
assert.deepEqual(requests[0].args[2], { overwrite: true });
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query_scene_state is_dirty calls query-dirty', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method) => {
|
||||||
|
requests.push({ channel, method });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.query_scene_state.handler({ action: 'is_dirty' });
|
||||||
|
assert.equal(result.action, 'is_dirty');
|
||||||
|
assert.equal(result.result, true);
|
||||||
|
assert.equal(requests[0].method, 'query-dirty');
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query_scene_state is_ready calls query-is-ready', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method) => {
|
||||||
|
requests.push({ channel, method });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.query_scene_state.handler({ action: 'is_ready' });
|
||||||
|
assert.equal(result.action, 'is_ready');
|
||||||
|
assert.equal(requests[0].method, 'query-is-ready');
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query_scene_state soft_reload calls soft-reload', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method) => {
|
||||||
|
requests.push({ channel, method });
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.query_scene_state.handler({ action: 'soft_reload' });
|
||||||
|
assert.equal(result.action, 'soft_reload');
|
||||||
|
assert.equal(requests[0].method, 'soft-reload');
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query_scene_state rejects unknown action', async () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await assert.rejects(() => byName.query_scene_state.handler({ action: 'unknown' }), /Unknown action/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node copy requires uuids', async () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'copy' }), /uuids is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node copy calls copy-node', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method, ...args) => {
|
||||||
|
requests.push({ channel, method, args });
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.copy_paste_node.handler({
|
||||||
|
action: 'copy',
|
||||||
|
uuids: ['uuid-1', 'uuid-2'],
|
||||||
|
});
|
||||||
|
assert.equal(result.action, 'copy');
|
||||||
|
assert.deepEqual(result.uuids, ['uuid-1', 'uuid-2']);
|
||||||
|
assert.equal(requests[0].method, 'copy-node');
|
||||||
|
assert.deepEqual(requests[0].args[0], ['uuid-1', 'uuid-2']);
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node cut calls cut-node', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method, ...args) => {
|
||||||
|
requests.push({ channel, method, args });
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.copy_paste_node.handler({
|
||||||
|
action: 'cut',
|
||||||
|
uuids: ['uuid-1'],
|
||||||
|
});
|
||||||
|
assert.equal(result.action, 'cut');
|
||||||
|
assert.equal(requests[0].method, 'cut-node');
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node paste requires target', async () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'paste' }), /target is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node paste calls paste-node with target and keepWorldTransform', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method, ...args) => {
|
||||||
|
requests.push({ channel, method, args });
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.copy_paste_node.handler({
|
||||||
|
action: 'paste',
|
||||||
|
target: 'parent-uuid',
|
||||||
|
keepWorldTransform: true,
|
||||||
|
});
|
||||||
|
assert.equal(result.action, 'paste');
|
||||||
|
assert.equal(result.target, 'parent-uuid');
|
||||||
|
assert.equal(result.keepWorldTransform, true);
|
||||||
|
assert.equal(requests[0].method, 'paste-node');
|
||||||
|
assert.deepEqual(requests[0].args[0], { target: 'parent-uuid', keepWorldTransform: true });
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node paste with uuids passes them in options', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method, ...args) => {
|
||||||
|
requests.push({ channel, method, args });
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await byName.copy_paste_node.handler({
|
||||||
|
action: 'paste',
|
||||||
|
target: 'parent-uuid',
|
||||||
|
uuids: ['copied-uuid-1'],
|
||||||
|
});
|
||||||
|
assert.equal(requests[0].method, 'paste-node');
|
||||||
|
assert.deepEqual(requests[0].args[0], {
|
||||||
|
target: 'parent-uuid',
|
||||||
|
keepWorldTransform: false,
|
||||||
|
uuids: ['copied-uuid-1'],
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node duplicate calls duplicate-node', async () => {
|
||||||
|
const requests = [];
|
||||||
|
mockEditor(async (channel, method, ...args) => {
|
||||||
|
requests.push({ channel, method, args });
|
||||||
|
return ['new-uuid-1'];
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
const result = await byName.copy_paste_node.handler({
|
||||||
|
action: 'duplicate',
|
||||||
|
uuids: ['uuid-1'],
|
||||||
|
});
|
||||||
|
assert.equal(result.action, 'duplicate');
|
||||||
|
assert.deepEqual(result.uuids, ['uuid-1']);
|
||||||
|
assert.equal(requests[0].method, 'duplicate-node');
|
||||||
|
assert.deepEqual(requests[0].args[0], ['uuid-1']);
|
||||||
|
} finally {
|
||||||
|
unmockEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node duplicate requires uuids', async () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'duplicate' }), /uuids is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copy_paste_node rejects unknown action', async () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'invalid' }), /Unknown action/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rename_node calls sceneBridge with renameNode', async () => {
|
||||||
|
const { byName, calls } = makeTools();
|
||||||
|
await byName.rename_node.handler({ path: 'Canvas/TestNode', newName: 'RenamedNode' });
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0].method, 'renameNode');
|
||||||
|
assert.equal(calls[0].args.path, 'Canvas/TestNode');
|
||||||
|
assert.equal(calls[0].args.newName, 'RenamedNode');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reparent_node calls sceneBridge with reparentNode', async () => {
|
||||||
|
const { byName, calls } = makeTools();
|
||||||
|
await byName.reparent_node.handler({
|
||||||
|
path: 'Canvas/TestNode',
|
||||||
|
targetPath: 'Canvas/OtherParent',
|
||||||
|
siblingIndex: 2,
|
||||||
|
keepWorldTransform: true,
|
||||||
|
});
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0].method, 'reparentNode');
|
||||||
|
assert.equal(calls[0].args.path, 'Canvas/TestNode');
|
||||||
|
assert.equal(calls[0].args.targetPath, 'Canvas/OtherParent');
|
||||||
|
assert.equal(calls[0].args.siblingIndex, 2);
|
||||||
|
assert.equal(calls[0].args.keepWorldTransform, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all 5 tools have correct profiles', () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
assert.equal(byName.create_scene.profile, 'full');
|
||||||
|
assert.equal(byName.query_scene_state.profile, 'core');
|
||||||
|
assert.equal(byName.copy_paste_node.profile, 'full');
|
||||||
|
assert.equal(byName.rename_node.profile, 'full');
|
||||||
|
assert.equal(byName.reparent_node.profile, 'full');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all 5 tools have inputSchema with type object', () => {
|
||||||
|
const { byName } = makeTools();
|
||||||
|
for (const name of Object.keys(byName)) {
|
||||||
|
assert.equal(byName[name].inputSchema.type, 'object', `${name} should have object schema`);
|
||||||
|
assert.ok(byName[name].inputSchema.properties, `${name} should have properties`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const {
|
||||||
|
createScriptTools,
|
||||||
|
generateComponentTemplate,
|
||||||
|
generatePlainTemplate,
|
||||||
|
} = require('../lib/tools/scripts');
|
||||||
|
|
||||||
|
test('generateComponentTemplate produces valid component with start and update', () => {
|
||||||
|
const code = generateComponentTemplate('PlayerController');
|
||||||
|
assert.ok(code.includes("import { _decorator, Component } from 'cc'"));
|
||||||
|
assert.ok(code.includes("@ccclass('PlayerController')"));
|
||||||
|
assert.ok(code.includes('export class PlayerController extends Component'));
|
||||||
|
assert.ok(code.includes('start()'));
|
||||||
|
assert.ok(code.includes('update(deltaTime: number)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generateComponentTemplate includes primitive @property', () => {
|
||||||
|
const code = generateComponentTemplate('Enemy', [
|
||||||
|
{ name: 'speed', type: 'Number', default: 5 },
|
||||||
|
{ name: 'label', type: 'String', default: "'Enemy'" },
|
||||||
|
]);
|
||||||
|
assert.ok(code.includes('@property'));
|
||||||
|
assert.ok(code.includes('speed: number = 5'));
|
||||||
|
assert.ok(code.includes("label: string = 'Enemy'"));
|
||||||
|
assert.ok(!code.includes('Label'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generateComponentTemplate includes CC type @property', () => {
|
||||||
|
const code = generateComponentTemplate('Weapon', [
|
||||||
|
{ name: 'target', type: 'Label' },
|
||||||
|
{ name: 'sprite', type: 'Sprite' },
|
||||||
|
]);
|
||||||
|
assert.ok(code.includes('@property(Label)'));
|
||||||
|
assert.ok(code.includes('target: Label | null = null'));
|
||||||
|
assert.ok(code.includes('@property(Sprite)'));
|
||||||
|
assert.ok(code.includes('sprite: Sprite | null = null'));
|
||||||
|
assert.ok(code.includes('import { _decorator, Component, Label, Sprite }'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generateComponentTemplate uses defaults when property default is omitted', () => {
|
||||||
|
const code = generateComponentTemplate('Item', [
|
||||||
|
{ name: 'count', type: 'Number' },
|
||||||
|
{ name: 'flag', type: 'Boolean' },
|
||||||
|
]);
|
||||||
|
assert.ok(code.includes('count: number = 0'));
|
||||||
|
assert.ok(code.includes('flag: boolean = false'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generateComponentTemplate with no properties has placeholder comment', () => {
|
||||||
|
const code = generateComponentTemplate('Empty');
|
||||||
|
assert.ok(code.includes('// Add properties here'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generatePlainTemplate produces minimal class', () => {
|
||||||
|
const code = generatePlainTemplate('Utils');
|
||||||
|
assert.ok(code.includes('export class Utils'));
|
||||||
|
assert.ok(!code.includes('@ccclass'));
|
||||||
|
assert.ok(!code.includes('@property'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create_script writes file and returns metadata', async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-script-test-'));
|
||||||
|
try {
|
||||||
|
const createSchema = (properties, required) => {
|
||||||
|
const schema = { type: 'object', properties };
|
||||||
|
if (required && required.length) {
|
||||||
|
schema.required = required;
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
|
const getRuntimeContext = () => ({ projectPath: tmpDir });
|
||||||
|
const tools = createScriptTools({ createSchema, getRuntimeContext });
|
||||||
|
const tool = tools[0];
|
||||||
|
|
||||||
|
const result = await tool.handler({
|
||||||
|
scriptName: 'TestComp',
|
||||||
|
savePath: 'assets/scripts/TestComp.ts',
|
||||||
|
template: 'component',
|
||||||
|
properties: [{ name: 'speed', type: 'Number', default: 10 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.created, true);
|
||||||
|
assert.equal(result.scriptName, 'TestComp');
|
||||||
|
assert.equal(result.template, 'component');
|
||||||
|
assert.equal(result.className, 'TestComp');
|
||||||
|
|
||||||
|
const written = fs.readFileSync(path.join(tmpDir, 'assets', 'scripts', 'TestComp.ts'), 'utf8');
|
||||||
|
assert.ok(written.includes('export class TestComp extends Component'));
|
||||||
|
assert.ok(written.includes('speed: number = 10'));
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create_script rejects non-.ts savePath', async () => {
|
||||||
|
const createSchema = () => ({ type: 'object', properties: {} });
|
||||||
|
const getRuntimeContext = () => ({ projectPath: '/tmp' });
|
||||||
|
const tools = createScriptTools({ createSchema, getRuntimeContext });
|
||||||
|
await assert.rejects(
|
||||||
|
() => tools[0].handler({ scriptName: 'X', savePath: 'assets/scripts/X.js' }),
|
||||||
|
/must end with \.ts/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create_script requires scriptName and savePath', async () => {
|
||||||
|
const createSchema = () => ({ type: 'object', properties: {} });
|
||||||
|
const getRuntimeContext = () => ({ projectPath: '/tmp' });
|
||||||
|
const tools = createScriptTools({ createSchema, getRuntimeContext });
|
||||||
|
await assert.rejects(() => tools[0].handler({ savePath: 'X.ts' }), /scriptName is required/);
|
||||||
|
await assert.rejects(() => tools[0].handler({ scriptName: 'X' }), /savePath is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create_script plain template writes minimal class', async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-script-test-'));
|
||||||
|
try {
|
||||||
|
const createSchema = () => ({ type: 'object', properties: {} });
|
||||||
|
const getRuntimeContext = () => ({ projectPath: tmpDir });
|
||||||
|
const tools = createScriptTools({ createSchema, getRuntimeContext });
|
||||||
|
|
||||||
|
const result = await tools[0].handler({
|
||||||
|
scriptName: 'Helper',
|
||||||
|
savePath: 'assets/scripts/Helper.ts',
|
||||||
|
template: 'plain',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.template, 'plain');
|
||||||
|
const written = fs.readFileSync(path.join(tmpDir, 'assets', 'scripts', 'Helper.ts'), 'utf8');
|
||||||
|
assert.ok(written.includes('export class Helper'));
|
||||||
|
assert.ok(!written.includes('@ccclass'));
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
+131
-3
@@ -9,7 +9,7 @@ const {
|
|||||||
SUPPORTED_PROTOCOL_VERSIONS,
|
SUPPORTED_PROTOCOL_VERSIONS,
|
||||||
} = require('../lib/server');
|
} = require('../lib/server');
|
||||||
|
|
||||||
function createServer(toolRegistry = {}, config = {}) {
|
function createServer(toolRegistry = {}, config = {}, options = {}) {
|
||||||
return new McpServer({
|
return new McpServer({
|
||||||
config: { host: '127.0.0.1', port: 8765, ...config },
|
config: { host: '127.0.0.1', port: 8765, ...config },
|
||||||
toolRegistry: {
|
toolRegistry: {
|
||||||
@@ -28,8 +28,10 @@ function createServer(toolRegistry = {}, config = {}) {
|
|||||||
},
|
},
|
||||||
interactionLog: { add() {} },
|
interactionLog: { add() {} },
|
||||||
runtimeLog: { add() {} },
|
runtimeLog: { add() {} },
|
||||||
serverName: 'test-server',
|
serverName: options.serverName || 'test-server',
|
||||||
serverVersion: '0.0.0-test',
|
serverVersion: options.serverVersion || '0.0.0-test',
|
||||||
|
projectName: options.projectName || 'test-project',
|
||||||
|
projectIdentity: options.projectIdentity || 'test-project-id',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +68,33 @@ function httpJson(port, payload, headers = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function httpGet(port, path = '/', headers = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = http.request(
|
||||||
|
{
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
method: 'GET',
|
||||||
|
path,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
const chunks = [];
|
||||||
|
response.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
response.on('end', () => {
|
||||||
|
resolve({
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
headers: response.headers,
|
||||||
|
body: Buffer.concat(chunks).toString('utf8'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
request.on('error', reject);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('initialize negotiates the current MCP protocol version by default', async () => {
|
test('initialize negotiates the current MCP protocol version by default', async () => {
|
||||||
const server = createServer();
|
const server = createServer();
|
||||||
const response = await server.handleRpcRequest({
|
const response = await server.handleRpcRequest({
|
||||||
@@ -77,6 +106,7 @@ test('initialize negotiates the current MCP protocol version by default', async
|
|||||||
|
|
||||||
assert.equal(response.result.protocolVersion, MCP_PROTOCOL_VERSION);
|
assert.equal(response.result.protocolVersion, MCP_PROTOCOL_VERSION);
|
||||||
assert.equal(response.result.serverInfo.name, 'test-server');
|
assert.equal(response.result.serverInfo.name, 'test-server');
|
||||||
|
assert.equal(response.result.funplay.projectIdentity, 'test-project-id');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initialize can negotiate an older supported MCP protocol version', async () => {
|
test('initialize can negotiate an older supported MCP protocol version', async () => {
|
||||||
@@ -222,6 +252,104 @@ test('HTTP notifications return 202 Accepted with no body', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('HTTP GET /tools returns debug tool metadata and curl examples', async () => {
|
||||||
|
const server = createServer({
|
||||||
|
listTools: () => [
|
||||||
|
{
|
||||||
|
name: 'get_project_info',
|
||||||
|
description: 'Return project info.',
|
||||||
|
inputSchema: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
listToolCatalog: () => [
|
||||||
|
{
|
||||||
|
name: 'get_project_info',
|
||||||
|
description: 'Return project info.',
|
||||||
|
category: 'project',
|
||||||
|
profile: 'core',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, { port: 0 });
|
||||||
|
await server.start();
|
||||||
|
try {
|
||||||
|
const response = await httpGet(server.getPort(), '/tools?catalog=1');
|
||||||
|
const payload = JSON.parse(response.body);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.name, 'test-server');
|
||||||
|
assert.equal(payload.version, '0.0.0-test');
|
||||||
|
assert.equal(payload.count, 1);
|
||||||
|
assert.equal(payload.tools[0].category, 'project');
|
||||||
|
assert.match(payload.examples.health, /curl http:\/\/127\.0\.0\.1:\d+\/health/);
|
||||||
|
assert.match(payload.examples.tools, /curl http:\/\/127\.0\.0\.1:\d+\/tools/);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTTP GET /health returns project identity metadata', async () => {
|
||||||
|
const server = createServer({}, { port: 0 }, { projectIdentity: 'health-project-id' });
|
||||||
|
await server.start();
|
||||||
|
try {
|
||||||
|
const response = await httpGet(server.getPort(), '/health');
|
||||||
|
const payload = JSON.parse(response.body);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.projectName, 'test-project');
|
||||||
|
assert.equal(payload.projectIdentity, 'health-project-id');
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start attaches to an existing same-project listener on the configured port', async () => {
|
||||||
|
const owner = createServer({}, { port: 0 }, { projectIdentity: 'same-project' });
|
||||||
|
await owner.start();
|
||||||
|
const attached = createServer({}, { port: owner.getPort() }, { projectIdentity: 'same-project' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await attached.start();
|
||||||
|
|
||||||
|
assert.equal(attached.isRunning(), true);
|
||||||
|
assert.equal(attached.getPort(), owner.getPort());
|
||||||
|
assert.equal(attached.getAttachInfo().projectIdentity, 'same-project');
|
||||||
|
|
||||||
|
await attached.stop();
|
||||||
|
assert.equal(attached.isRunning(), false);
|
||||||
|
|
||||||
|
const response = await httpGet(owner.getPort(), '/health');
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
} finally {
|
||||||
|
if (attached.isRunning()) {
|
||||||
|
await attached.stop();
|
||||||
|
}
|
||||||
|
await owner.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start falls back instead of attaching to a different project listener', async () => {
|
||||||
|
const owner = createServer({}, { port: 0 }, { projectIdentity: 'owner-project' });
|
||||||
|
await owner.start();
|
||||||
|
const contender = createServer({}, { port: owner.getPort() }, { projectIdentity: 'other-project' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await contender.start();
|
||||||
|
|
||||||
|
assert.equal(contender.isRunning(), true);
|
||||||
|
assert.notEqual(contender.getPort(), owner.getPort());
|
||||||
|
assert.equal(contender.getAttachInfo(), null);
|
||||||
|
assert.equal(contender.getPortFallbackInfo().requestedPort, owner.getPort());
|
||||||
|
} finally {
|
||||||
|
if (contender.isRunning()) {
|
||||||
|
await contender.stop();
|
||||||
|
}
|
||||||
|
await owner.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('HTTP initialize can return an optional session id when sessions are enabled', async () => {
|
test('HTTP initialize can return an optional session id when sessions are enabled', async () => {
|
||||||
const server = createServer({}, { port: 0, enableSessions: true });
|
const server = createServer({}, { port: 0, enableSessions: true });
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const childProcess = require('node:child_process');
|
||||||
|
const http = require('node:http');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
const WRAPPER = path.join(ROOT, 'bin', 'funplay-cocos-mcp.js');
|
||||||
|
const PACKAGE = require('../package.json');
|
||||||
|
|
||||||
|
function writeFramed(stream, value) {
|
||||||
|
const payload = Buffer.from(JSON.stringify(value), 'utf8');
|
||||||
|
stream.write(`Content-Length: ${payload.length}\r\n\r\n`);
|
||||||
|
stream.write(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFramedReader(stream) {
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
const waiters = [];
|
||||||
|
|
||||||
|
function readFromBuffer() {
|
||||||
|
const marker = buffer.indexOf('\r\n\r\n');
|
||||||
|
if (marker < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = buffer.slice(0, marker).toString('ascii');
|
||||||
|
const match = /^content-length:\s*(\d+)$/im.exec(header);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Missing Content-Length header: ${header}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = Number.parseInt(match[1], 10);
|
||||||
|
const start = marker + 4;
|
||||||
|
const end = start + length;
|
||||||
|
if (buffer.length < end) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buffer.slice(start, end).toString('utf8');
|
||||||
|
buffer = buffer.slice(end);
|
||||||
|
return JSON.parse(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pump() {
|
||||||
|
while (waiters.length > 0) {
|
||||||
|
const value = readFromBuffer();
|
||||||
|
if (value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
waiters.shift().resolve(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
buffer = Buffer.concat([buffer, chunk]);
|
||||||
|
pump();
|
||||||
|
});
|
||||||
|
stream.on('error', (error) => {
|
||||||
|
while (waiters.length > 0) {
|
||||||
|
waiters.shift().reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return function readNext() {
|
||||||
|
const value = readFromBuffer();
|
||||||
|
if (value !== null) {
|
||||||
|
return Promise.resolve(value);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
waiters.push({ resolve, reject });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHttpProxyTarget(handler) {
|
||||||
|
const server = http.createServer(handler);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
resolve({
|
||||||
|
server,
|
||||||
|
url: `http://127.0.0.1:${server.address().port}/`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBody(request) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = [];
|
||||||
|
request.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||||
|
request.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForExit(child) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
child.once('error', reject);
|
||||||
|
child.once('exit', (code) => resolve(code));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('stdio wrapper prints package version', () => {
|
||||||
|
const result = childProcess.spawnSync(process.execPath, [WRAPPER, '--version'], {
|
||||||
|
cwd: ROOT,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.status, 0);
|
||||||
|
assert.match(result.stderr, new RegExp(`funplay-cocos-mcp ${PACKAGE.version}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stdio wrapper proxies framed JSON-RPC to the Cocos HTTP endpoint', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const target = await createHttpProxyTarget(async (request, response) => {
|
||||||
|
const body = await readBody(request);
|
||||||
|
requests.push({
|
||||||
|
headers: request.headers,
|
||||||
|
body: JSON.parse(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
response.setHeader('Content-Type', 'application/json');
|
||||||
|
if (requests.length === 1) {
|
||||||
|
response.setHeader('Mcp-Session-Id', 'session-1');
|
||||||
|
}
|
||||||
|
response.end(JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: requests.length,
|
||||||
|
result: { ok: true, index: requests.length }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const child = childProcess.spawn(process.execPath, [WRAPPER, '--url', target.url], {
|
||||||
|
cwd: ROOT,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
const readNext = createFramedReader(child.stdout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFramed(child.stdin, {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
method: 'initialize',
|
||||||
|
params: {}
|
||||||
|
});
|
||||||
|
assert.deepEqual(await readNext(), {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
result: { ok: true, index: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
writeFramed(child.stdin, {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 2,
|
||||||
|
method: 'tools/list'
|
||||||
|
});
|
||||||
|
assert.deepEqual(await readNext(), {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 2,
|
||||||
|
result: { ok: true, index: 2 }
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(requests.length, 2);
|
||||||
|
assert.equal(requests[0].headers.accept, 'application/json, text/event-stream');
|
||||||
|
assert.equal(requests[1].headers['mcp-session-id'], 'session-1');
|
||||||
|
assert.equal(requests[1].body.method, 'tools/list');
|
||||||
|
} finally {
|
||||||
|
child.stdin.end();
|
||||||
|
await waitForExit(child);
|
||||||
|
await new Promise((resolve) => target.server.close(resolve));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const {
|
||||||
|
applyToolProfile,
|
||||||
|
createToolProfileSnapshot,
|
||||||
|
deleteToolProfile,
|
||||||
|
exportToolProfiles,
|
||||||
|
importToolProfiles,
|
||||||
|
normalizeSavedToolProfiles,
|
||||||
|
upsertToolProfile,
|
||||||
|
} = require('../lib/tool-profiles');
|
||||||
|
|
||||||
|
test('tool profiles normalize, upsert, and dedupe by name', () => {
|
||||||
|
const profiles = normalizeSavedToolProfiles([
|
||||||
|
{ name: 'QA', toolProfile: 'custom', enabledToolCategories: 'assets\nlogs' },
|
||||||
|
{ name: 'qa', toolProfile: 'full', disabledTools: ['delete_asset'] },
|
||||||
|
{ name: '' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(profiles.length, 1);
|
||||||
|
assert.equal(profiles[0].name, 'qa');
|
||||||
|
assert.equal(profiles[0].toolProfile, 'full');
|
||||||
|
|
||||||
|
const updated = upsertToolProfile(profiles, {
|
||||||
|
name: 'Prototype',
|
||||||
|
toolProfile: 'custom',
|
||||||
|
enabledToolCategories: ['ui'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(updated.length, 2);
|
||||||
|
assert.equal(updated.some((profile) => profile.name === 'Prototype'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tool profiles snapshot and apply exposure config', () => {
|
||||||
|
const snapshot = createToolProfileSnapshot({
|
||||||
|
toolProfile: 'custom',
|
||||||
|
enabledToolCategories: ['assets'],
|
||||||
|
disabledToolCategories: ['input'],
|
||||||
|
enabledTools: ['write_file'],
|
||||||
|
disabledTools: ['delete_asset'],
|
||||||
|
}, 'Asset QA');
|
||||||
|
|
||||||
|
const applied = applyToolProfile({ port: 8765 }, snapshot);
|
||||||
|
assert.equal(applied.port, 8765);
|
||||||
|
assert.equal(applied.activeToolProfileName, 'Asset QA');
|
||||||
|
assert.deepEqual(applied.enabledToolCategories, ['assets']);
|
||||||
|
assert.deepEqual(applied.disabledTools, ['delete_asset']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tool profiles import, export, and delete', () => {
|
||||||
|
const imported = importToolProfiles([], JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
profiles: [
|
||||||
|
{ name: 'Core QA', toolProfile: 'core' },
|
||||||
|
{ name: 'Debug', toolProfile: 'custom', enabledToolCategories: ['logs', 'diagnostics'] },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.equal(imported.length, 2);
|
||||||
|
assert.deepEqual(exportToolProfiles(imported).profiles.map((profile) => profile.name), ['Core QA', 'Debug']);
|
||||||
|
|
||||||
|
const remaining = deleteToolProfile(imported, 'Debug');
|
||||||
|
assert.deepEqual(remaining.map((profile) => profile.name), ['Core QA']);
|
||||||
|
});
|
||||||
@@ -21,23 +21,30 @@ function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-
|
|||||||
|
|
||||||
test('core profile exposes the documented focused tool set', () => {
|
test('core profile exposes the documented focused tool set', () => {
|
||||||
const tools = createRegistry('core').listTools();
|
const tools = createRegistry('core').listTools();
|
||||||
assert.equal(tools.length, 34);
|
assert.equal(tools.length, 38);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
|
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'validate_scene'), true);
|
assert.equal(tools.some((tool) => tool.name === 'validate_scene'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'inspect_asset_dependencies'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'get_build_status'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_performance_snapshot'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_performance_snapshot'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), true);
|
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'query_scene_state'), 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, 89);
|
assert.equal(tools.length, 110);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
|
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
|
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);
|
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'create_cocos_mcp_project_skill'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'bind_button_click_event'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'open_build_panel'), true);
|
||||||
|
assert.equal(tools.some((tool) => tool.name === 'broadcast_editor_message'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
|
||||||
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
|
||||||
});
|
});
|
||||||
@@ -98,3 +105,34 @@ test('callToolDetailed preserves screenshot image text while keeping structured
|
|||||||
assert.equal(result.value.data.image, true);
|
assert.equal(result.value.data.image, true);
|
||||||
assert.equal(result.value.data.mimeType, 'image/png');
|
assert.equal(result.value.data.mimeType, 'image/png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('execute_javascript safety checks block risky editor snippets by default', async () => {
|
||||||
|
const registry = createRegistry('core');
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => registry.callToolDetailed('execute_javascript', {
|
||||||
|
context: 'editor',
|
||||||
|
code: "fs.rmSync(path.join(context.projectPath, 'assets'), { recursive: true });",
|
||||||
|
}),
|
||||||
|
/JavaScript safety checks blocked/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('execute_javascript safety checks can be explicitly disabled per call', async () => {
|
||||||
|
let called = false;
|
||||||
|
const registry = createRegistry('core', path.resolve('/tmp/funplay-cocos-test-project'), {}, {
|
||||||
|
editorExecutor: async () => {
|
||||||
|
called = true;
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await registry.callToolDetailed('execute_javascript', {
|
||||||
|
context: 'editor',
|
||||||
|
code: "fs.rmSync(path.join(context.projectPath, 'assets'), { recursive: true });",
|
||||||
|
safety_checks: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(called, true);
|
||||||
|
assert.equal(result.value.ok, true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
'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 { buildSnippet, createFileTools } = require('../lib/tools/files');
|
||||||
|
|
||||||
|
function createSchema(properties, required) {
|
||||||
|
return { type: 'object', properties, required };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTools(projectPath) {
|
||||||
|
return createFileTools({
|
||||||
|
createSchema,
|
||||||
|
getRuntimeContext: () => ({ projectPath }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTool(tools, name) {
|
||||||
|
const tool = tools.find((item) => item.name === name);
|
||||||
|
assert.ok(tool, `Expected ${name} to exist`);
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('buildSnippet returns focused line-numbered context', () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-files-'));
|
||||||
|
try {
|
||||||
|
const filePath = path.join(root, 'sample.ts');
|
||||||
|
fs.writeFileSync(filePath, ['alpha', 'beta', 'gamma', 'delta'].join('\n'), 'utf8');
|
||||||
|
|
||||||
|
const snippet = buildSnippet(filePath, 2, 1);
|
||||||
|
|
||||||
|
assert.match(snippet, / 1 \| alpha/);
|
||||||
|
assert.match(snippet, />\s+2 \| beta/);
|
||||||
|
assert.match(snippet, / 3 \| gamma/);
|
||||||
|
assert.doesNotMatch(snippet, /delta/);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('file tools write, read, replace, search, list, and check project files', async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-files-'));
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.join(root, 'assets'), { recursive: true });
|
||||||
|
const tools = createTools(root);
|
||||||
|
|
||||||
|
const writeResult = await getTool(tools, 'write_file').handler({
|
||||||
|
path: 'assets/player.ts',
|
||||||
|
content: 'const name = "Hero";\nconst clone = "Hero";\n',
|
||||||
|
});
|
||||||
|
assert.match(writeResult, /Wrote \d+ chars/);
|
||||||
|
|
||||||
|
const readResult = await getTool(tools, 'read_file').handler({ path: 'assets/player.ts' });
|
||||||
|
assert.match(readResult, /const name = "Hero"/);
|
||||||
|
|
||||||
|
await getTool(tools, 'replace_in_file').handler({
|
||||||
|
path: 'assets/player.ts',
|
||||||
|
search: 'Hero',
|
||||||
|
replace: 'Player',
|
||||||
|
replaceAll: true,
|
||||||
|
});
|
||||||
|
assert.equal(fs.readFileSync(path.join(root, 'assets', 'player.ts'), 'utf8').includes('Hero'), false);
|
||||||
|
|
||||||
|
const searchResult = await getTool(tools, 'search_files').handler({ pattern: '*.ts', directory: 'assets' });
|
||||||
|
assert.deepEqual(searchResult.files, ['assets/player.ts']);
|
||||||
|
|
||||||
|
const listResult = await getTool(tools, 'list_directory').handler({ path: 'assets' });
|
||||||
|
assert.deepEqual(listResult.entries, [{ name: 'player.ts', type: 'file' }]);
|
||||||
|
|
||||||
|
const existsResult = await getTool(tools, 'exists').handler({ path: 'assets/player.ts' });
|
||||||
|
assert.deepEqual(existsResult, {
|
||||||
|
path: 'assets/player.ts',
|
||||||
|
exists: true,
|
||||||
|
isFile: true,
|
||||||
|
isDirectory: false,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user