2 Commits
Author SHA1 Message Date
winlifes 668898f0c6 Release v0.3.3 2026-05-20 20:13:18 -07:00
winlifes 87cfa48cda Ignore MCP registry token files 2026-05-20 19:29:34 -07:00
19 changed files with 1128 additions and 298 deletions
+3
View File
@@ -76,6 +76,9 @@ jobs:
- name: Run tests
run: npm test
- name: Check generated docs
run: npm run docs:check
- name: Run release metadata checks
run: npm run release:check
+1
View File
@@ -1,4 +1,5 @@
.DS_Store
.mcpregistry_*
node_modules/
temp/
Temp/
+17
View File
@@ -6,6 +6,23 @@ This project follows a simple changelog format inspired by [Keep a Changelog](ht
## [Unreleased]
## [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 CI and release validation for generated tool documentation.
- 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`.
### Changed
- Refined tool category inference so `get_tool_catalog` is grouped with project/context tools.
- Updated the MCP client config panel so the preview follows the selected client target.
- Split file-system tools and asset refresh helpers into `lib/tools/files.js` as the first registry modularization step.
## [0.3.2] - 2026-05-20
### Added
+22 -3
View File
@@ -73,8 +73,10 @@ The panel is intentionally small:
- Change the server port
- Switch tool exposure between `core`, `full`, and `custom`
- Check the installed version against the latest GitHub release
- Inspect recent tool calls and runtime log previews
- Tune tool exposure by category or individual tool
- Configure AI clients with one click
- 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
### 3. Configure Your AI Client
@@ -215,6 +217,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.
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
Try a higher-level prompt in your AI client:
@@ -226,6 +235,7 @@ 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.
- The MCP server listens on `http://127.0.0.1:8765/` by default.
- If the configured port is busy, the server automatically falls back to the next available port and the panel/client config use the actual running port.
- `GET /health` and `GET /tools` are read-only debug endpoints for quick local checks outside an MCP client.
- The default `core` profile exposes 34 high-signal tools. Switch to `full` for all 89 tools, or use `custom` to include/exclude tool categories and individual tools.
- The 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.
@@ -249,7 +259,7 @@ Try a higher-level prompt in your AI client:
- **89 Built-in Tools** — Scene hierarchy, editor state, selection workflows, prefabs, assets, project instructions, UI creation, components, files, logs, script diagnostics, screenshots, runtime control, and input simulation
- **Primary Unified Tool** — `execute_javascript` supports both `scene` and `editor` contexts
- **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
- **Vendor Agnostic** — Works with any AI client that supports MCP over HTTP JSON-RPC
@@ -276,6 +286,8 @@ The current package exposes four capability layers:
- **Prompts** — `fix_script_errors`, `create_playable_prototype`, `scene_validation`, and `auto_wire_scene`
- **Resources** — project context, scene summaries, current selection, script diagnostics, asset selection, logs, and MCP interaction history
For the generated tool reference, including categories, profiles, and read/mutation hints, see [docs/TOOLS.md](./docs/TOOLS.md).
The default `core` set is intentionally small: `execute_javascript`, `execute_scene_script`, `execute_editor_script`, `get_editor_state`, `get_tool_catalog`, `check_for_updates`, `get_selection`, `list_project_instructions`, `read_project_instruction`, `set_selection`, `get_project_info`, `get_scene_info`, `get_hierarchy`, `list_scenes`, `open_scene`, `inspect_prefab`, `validate_prefab_references`, `inspect_prefab_instance`, `list_assets`, `inspect_asset`, `open_asset`, `select_asset`, `run_script_diagnostics`, `get_recent_logs`, `search_project_logs`, `clear_logs`, `validate_scene`, `get_performance_snapshot`, `get_script_diagnostic_context`, `get_runtime_state`, `capture_editor_screenshot`, `capture_scene_screenshot`, `capture_preview_screenshot`, and `list_editor_windows`.
## Built-in Resources
@@ -385,7 +397,7 @@ Cocos Creator Extension
└─ 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
@@ -394,10 +406,17 @@ Run checks before publishing changes:
```bash
npm run check
npm test
npm run docs: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:
```bash
+22 -3
View File
@@ -73,8 +73,10 @@ Funplay > MCP Server
- 修改服务端口
-`core` / `full` / `custom` 工具暴露模式之间切换
- 检查当前安装版本是否落后于 GitHub 最新 Release
- 查看最近工具调用和运行日志预览
- 按工具分类或单个工具调整暴露范围
- 一键配置 AI 客户端
- 一键配置 AI 客户端,并随目标客户端预览对应配置
- 复制 `/health``/tools` 的快速 `curl` 排障命令
- 需要时再展开 Debug Output
### 3. 配置 AI 客户端
@@ -215,6 +217,13 @@ npm install -g funplay-cocos-mcp
如果这些都正常返回,说明 MCP server、resources、prompts 和主执行工具已经连通。
如果要排查本地传输链路,面板可以直接复制下面的命令,也可以手动执行:
```bash
curl http://127.0.0.1:8765/health
curl http://127.0.0.1:8765/tools
```
### 5. 开始构建
可以在 AI 客户端里尝试:
@@ -226,6 +235,7 @@ npm install -g funplay-cocos-mcp
- 这是一个 **仅限 Editor** 的扩展,用于自动化 Cocos Creator,不会给最终游戏包添加运行时依赖。
- MCP Server 默认监听 `http://127.0.0.1:8765/`
- 如果配置端口被占用,服务会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。
- `GET /health``GET /tools` 是只读调试端点,方便不用 MCP 客户端也能快速检查本地服务。
- 默认 `core` profile 暴露 34 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 89 个工具;也可以用 `custom` 按分类或工具名增删。
- 面板提供手动更新检查,会对比当前安装版本和 GitHub 最新 Release。
- Streamable HTTP 响应已补齐 MCP 传输层要求,包括 `Accept``MCP-Protocol-Version`、JSON-RPC notification/response,以及可选 `Mcp-Session-Id` session。
@@ -249,7 +259,7 @@ npm install -g funplay-cocos-mcp
- **89 个内置工具** — 覆盖场景层级、编辑器状态、选择工作流、Prefab、资产、项目指令、UI 创建、组件、文件、日志、脚本诊断、截图、运行态控制和输入模拟
- **统一主工具** — `execute_javascript` 同时支持 `scene``editor` 两种上下文
- **Resources 与 Prompts** — 实时项目/日志资源,以及脚本修复、场景验证、可玩原型等可复用工作流
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露和 MCP 客户端配置
- **Cocos 图形面板** — `Funplay > MCP Server` 提供服务管理、更新检查、工具暴露、最近活动、日志、curl 排障和 MCP 客户端配置
- **截图与输入支持** — 支持编辑器/场景/Game/Preview 截图,以及 Electron 级鼠标键盘事件
- **厂商无关** — 兼容任意支持 HTTP JSON-RPC MCP 的 AI 客户端
@@ -276,6 +286,8 @@ Funplay MCP for Cocos 延续 Funplay MCP for Unity 的设计原则,并针对 C
- **Prompts** — `fix_script_errors``create_playable_prototype``scene_validation``auto_wire_scene`
- **Resources** — 项目上下文、场景摘要、当前选择、脚本诊断、资产选择、日志和 MCP 交互历史
自动生成的工具参考文档见 [docs/TOOLS.md](./docs/TOOLS.md),里面包含工具分类、profile 和读写/变更提示。
当前默认 `core` 工具集刻意保持精简,只包含:`execute_javascript``execute_scene_script``execute_editor_script``get_editor_state``get_tool_catalog``check_for_updates``get_selection``list_project_instructions``read_project_instruction``set_selection``get_project_info``get_scene_info``get_hierarchy``list_scenes``open_scene``inspect_prefab``validate_prefab_references``inspect_prefab_instance``list_assets``inspect_asset``open_asset``select_asset``run_script_diagnostics``get_recent_logs``search_project_logs``clear_logs``validate_scene``get_performance_snapshot``get_script_diagnostic_context``get_runtime_state``capture_editor_screenshot``capture_scene_screenshot``capture_preview_screenshot``list_editor_windows`
## 内置 Resources
@@ -385,7 +397,7 @@ Cocos Creator Extension
└─ server, resources, prompts, tool registry
```
服务使用 MCP 风格的 HTTP JSON-RPC 2.0,支持 tools、resources、resource templates、promptshealth check。
服务使用 MCP 风格的 HTTP JSON-RPC 2.0,支持 tools、resources、resource templates、promptshealth check 和只读 `/tools` 调试端点
## 开发
@@ -394,10 +406,17 @@ Cocos Creator Extension
```bash
npm run check
npm test
npm run docs:check
npm run release:check
npm run pack:dry-run
```
修改 `lib/tool-registry.js` 后可以重新生成工具参考:
```bash
npm run docs:generate
```
生成可上传到 GitHub Release 的扩展包:
```bash
+1
View File
@@ -14,6 +14,7 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
- [ ] `npm run check` passes
- [ ] `npm test` passes
- [ ] `npm run docs:check` passes
- [ ] `npm run release:check` passes
- [ ] `npm run pack:dry-run` passes
- [ ] `npm run registry:validate` passes when `mcp-publisher` is available
+6 -3
View File
@@ -53,12 +53,14 @@ Update:
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:
4. `README.md`
5. `README_CN.md`
6. GitHub Release notes text
5. `README.md`
6. `README_CN.md`
7. GitHub Release notes text
## Release Steps
@@ -90,6 +92,7 @@ This runs:
- JavaScript syntax checks
- Node.js tests
- generated tool documentation validation
- release metadata validation
- npm package dry-run validation
- release package generation
+35 -2
View File
@@ -4,7 +4,7 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
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 { McpServer } = require('./lib/server');
const { createToolRegistry } = require('./lib/tool-registry');
@@ -304,7 +304,21 @@ class ExtensionService {
}
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 {
url,
codex: `[mcp_servers.funplay_cocos]\nurl = "${url}"\n`,
@@ -315,9 +329,28 @@ class ExtensionService {
},
},
}, 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) {
this.ensureRuntime();
this.log('info', `Configuring MCP client target: ${targetId}`);
+203
View File
@@ -0,0 +1,203 @@
# 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 34 tools; the `full` profile exposes 89 tools.
## Profile Summary
| Profile | Tool Count | Purpose |
|---|---:|---|
| `core` | 34 | Focused default surface for common editor automation. |
| `full` | 89 | All built-in tools, including destructive and low-level helpers. |
## Core Tools
`capture_editor_screenshot`, `capture_preview_screenshot`, `capture_scene_screenshot`, `check_for_updates`, `clear_logs`, `execute_editor_script`, `execute_javascript`, `execute_scene_script`, `get_editor_state`, `get_hierarchy`, `get_performance_snapshot`, `get_project_info`, `get_recent_logs`, `get_runtime_state`, `get_scene_info`, `get_script_diagnostic_context`, `get_selection`, `get_tool_catalog`, `inspect_asset`, `inspect_prefab`, `inspect_prefab_instance`, `list_assets`, `list_editor_windows`, `list_project_instructions`, `list_scenes`, `open_asset`, `open_scene`, `read_project_instruction`, `run_script_diagnostics`, `search_project_logs`, `select_asset`, `set_selection`, `validate_prefab_references`, `validate_scene`
## 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 |
|---|---|---|---|
| `delete_asset` | `full` | mutating | Delete an asset from asset-db by uuid, db url, or path. |
| `inspect_asset` | `core`, `full` | read-only | [specialist] Inspect asset-db info, metadata, and serialized asset data by uuid or path. Prefer this when you need a precise structured asset read. |
| `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. |
### 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_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. |
### 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_button_click` | `full` | mutating | [core] Simulate a Cocos Button click by emitting click events on the target button node. |
| `simulate_key_combo` | `full` | mutating | [core] Send a low-level Electron modified key press such as Ctrl+S or Cmd+P. |
| `simulate_key_press` | `full` | mutating | [core] Send a low-level Electron key press to the editor, preview, or simulator window. |
| `simulate_mouse_click` | `full` | mutating | [core] Send a low-level Electron mouse click to the editor, preview, or simulator window. |
| `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_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 |
|---|---|---|---|
| `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_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. |
### 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 |
|---|---|---|---|
| `emit_node_event` | `full` | mutating | [core] Emit a custom event on a target scene node with an optional JSON payload. |
| `get_runtime_state` | `core`, `full` | read-only | [specialist] Return structured Cocos runtime state including pause state, frame count, and scheduler time scale. Prefer this when you want a compact validation snapshot. |
| `pause_runtime` | `full` | stateful | [core] Pause Cocos director game logic execution. |
| `resume_runtime` | `full` | stateful | [core] Resume Cocos director game logic execution. |
| `set_time_scale` | `full` | mutating | [core] Set Cocos scheduler time scale for runtime validation. |
### Scene
| Tool | Profiles | Access | Description |
|---|---|---|---|
| `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. |
| `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. |
+23 -1
View File
@@ -140,11 +140,33 @@ class McpServer {
const requestHandler = async (request, response) => {
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');
return json(response, 200, { ok: true, name: this.serverName, version: this.serverVersion }, 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)) {
this.log('warn', `Rejected ${request.method} ${request.url}: invalid Origin header.`);
return json(response, 403, { error: 'Forbidden: invalid Origin header' }, this.negotiatedProtocolVersion);
+3 -276
View File
@@ -2,7 +2,6 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const {
clearSelection,
deleteAsset,
@@ -37,12 +36,14 @@ const {
revertPrefabInstance,
validatePrefabReferences,
} = require('./prefabs');
const { buildSnippet, createFileTools, refreshAssets } = require('./tools/files');
const { captureDesktopScreenshot, captureEditorWindowScreenshot, capturePanelScreenshot } = require('./screenshots');
const { checkForUpdate } = require('./update-checker');
const { safeStringify } = require('./utils');
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
const TOOL_CATEGORY_RULES = [
['project', /^(get_project_info|get_editor_state|get_tool_catalog)$/],
['updates', /update/],
['logs', /log/],
['diagnostics', /diagnostic|validate/],
@@ -60,7 +61,6 @@ const TOOL_CATEGORY_RULES = [
['runtime', /runtime|time_scale|node_event|invoke_component/],
['scene', /scene|hierarchy|node/],
['execution', /execute_/],
['project', /project|editor_state|tool_catalog/],
];
function createSchema(properties, required) {
@@ -306,91 +306,6 @@ function toOutput(value) {
return safeStringify(value);
}
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 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) {
if (args && args.uuid) {
return String(args.uuid);
@@ -1337,195 +1252,7 @@ function createToolRegistry({ getRuntimeContext, getStatus, interactionLog, runt
),
handler: async (args) => sceneBridge.call('stopAnimation', args),
},
{
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);
},
},
...createFileTools({ createSchema, getRuntimeContext }),
{
name: 'run_script_diagnostics',
profile: 'core',
+303
View File
@@ -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,
};
+6 -3
View File
@@ -1,7 +1,7 @@
{
"name": "funplay-cocos-mcp",
"package_version": 2,
"version": "0.3.2",
"version": "0.3.3",
"description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.",
"author": "Funplay",
"license": "MIT",
@@ -31,6 +31,7 @@
},
"files": [
"bin/",
"docs/",
"lib/",
"panel/",
"browser.js",
@@ -48,11 +49,13 @@
"access": "public"
},
"scripts": {
"check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check bin/funplay-cocos-mcp.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-registry.js && node --check lib/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/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-registry.js && node --check lib/tools/files.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/generate-tool-docs.js && node --check scripts/release.js",
"test": "node --test",
"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",
"release:check": "node scripts/release.js check && npm run docs:check",
"release:package": "node scripts/release.js package",
"release:verify": "npm run check && npm test && npm run release:check && npm run pack:dry-run && npm run release:package"
},
+168 -5
View File
@@ -34,6 +34,8 @@ module.exports = Editor.Panel.define({
</label>
<ui-button id="restartBtn">Restart</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>
</div>
<div class="grid">
@@ -70,6 +72,20 @@ module.exports = Editor.Panel.define({
</div>
</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">
<h2>MCP Client Config</h2>
<div class="row">
@@ -114,6 +130,12 @@ module.exports = Editor.Panel.define({
margin: 0 0 10px 0;
font-size: 15px;
}
h3 {
margin: 0 0 6px 0;
font-size: 12px;
font-weight: 600;
color: var(--color-normal-contrast-weak);
}
p {
margin: 4px 0 0 0;
color: var(--color-normal-contrast-weakest);
@@ -199,6 +221,46 @@ module.exports = Editor.Panel.define({
white-space: pre-wrap;
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 {
display: block;
}
@@ -232,6 +294,11 @@ module.exports = Editor.Panel.define({
.primary {
border-color: #4aa3ff;
}
@media (max-width: 620px) {
.activity-grid {
grid-template-columns: 1fr;
}
}
`,
$: {
root: '.mcp-root',
@@ -244,6 +311,8 @@ module.exports = Editor.Panel.define({
sessionsInput: '#sessionsInput',
restartBtn: '#restartBtn',
copyUrlBtn: '#copyUrlBtn',
copyHealthCurlBtn: '#copyHealthCurlBtn',
copyToolsCurlBtn: '#copyToolsCurlBtn',
checkUpdatesBtn: '#checkUpdatesBtn',
updateStatus: '#updateStatus',
toolSummary: '#toolSummary',
@@ -258,6 +327,8 @@ module.exports = Editor.Panel.define({
configureClientBtn: '#configureClientBtn',
clientTargetStatus: '#clientTargetStatus',
clientConfigText: '#clientConfigText',
recentCalls: '#recentCalls',
recentLogs: '#recentLogs',
output: '#output',
},
methods: {
@@ -295,9 +366,8 @@ module.exports = Editor.Panel.define({
this.$.disabledToolsInput.value = this.formatList(config.disabledTools);
this.renderUpdateStatus();
this.renderToolSummary();
this.$.clientConfigText.value = state.clientConfig ? state.clientConfig.codex : '';
this.renderClientTargets();
this.renderActivity();
},
formatList(value) {
return Array.isArray(value) ? value.join('\n') : '';
@@ -329,6 +399,75 @@ module.exports = Editor.Panel.define({
this.$.toolSummary.textContent =
`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() {
const targets = (this.state && this.state.clientTargets) || [];
const preferred = this.state && this.state.config ? this.state.config.lastClientTargetId : '';
@@ -349,10 +488,30 @@ module.exports = Editor.Panel.define({
return;
}
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) {
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 = {}) {
const { showOutput = false } = options;
try {
@@ -415,9 +574,13 @@ module.exports = Editor.Panel.define({
this.$.copyUrlBtn.addEventListener('click', () => {
const status = this.state && this.state.status;
const text = status && status.url ? status.url : '';
navigator.clipboard.writeText(text)
.then(() => this.showOutput('Copied URL to clipboard.'))
.catch(() => this.showOutput(text));
this.copyText(text, 'Copied URL to clipboard.');
});
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.$.enabledInput.addEventListener('change', () => this.handleEnableToggle());
+164
View File
@@ -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();
+2
View File
@@ -17,6 +17,7 @@ const REQUIRED_REPO_FILES = [
'package.json',
'README.md',
'README_CN.md',
'docs/TOOLS.md',
'RELEASE_WORKFLOW.md',
'RELEASE_CHECKLIST.md',
'CHANGELOG.md',
@@ -35,6 +36,7 @@ const PACKAGE_INCLUDES = [
'package.json',
'README.md',
'README_CN.md',
'docs',
'CHANGELOG.md',
'CONTRIBUTING.md',
'LICENSE',
+2 -2
View File
@@ -7,12 +7,12 @@
"url": "https://github.com/FunplayAI/funplay-cocos-mcp",
"source": "github"
},
"version": "0.3.2",
"version": "0.3.3",
"packages": [
{
"registryType": "npm",
"identifier": "funplay-cocos-mcp",
"version": "0.3.2",
"version": "0.3.3",
"transport": {
"type": "stdio"
},
+64
View File
@@ -66,6 +66,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 () => {
const server = createServer();
const response = await server.handleRpcRequest({
@@ -222,6 +249,43 @@ 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 initialize can return an optional session id when sessions are enabled', async () => {
const server = createServer({}, { port: 0, enableSessions: true });
await server.start();
+83
View File
@@ -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 });
}
});