初次提交
This commit is contained in:
81
@types/schema/package/base/panels.json
Executable file
81
@types/schema/package/base/panels.json
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"description": "面板数据 / Panel data",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9_-]+$": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "面板名 / Panel name",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Default Panel",
|
||||||
|
"description": "面板标题,支持 i18n:key / Panel title, support for i18n:key (required)"
|
||||||
|
},
|
||||||
|
"main": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "dist/panels/default/index.js",
|
||||||
|
"description": "入口函数 / Entry function (required)"
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "面板图标存放相对目录 / Relative directory for panel icon storage"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["dockable", "simple"],
|
||||||
|
"default": "dockable",
|
||||||
|
"description": "面板类型(dockable | simple) / Panel type (dockable | simple)"
|
||||||
|
},
|
||||||
|
"flags": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"resizable": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "是否可以改变大小,默认 true / Whether the size can be changed, default true"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "是否需要保存,默认 false / Whether to save, default false"
|
||||||
|
},
|
||||||
|
"alwaysOnTop": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "是否保持顶层显示,默认 false / Whether to keep the top level display, default false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "面板大小信息 / Panel size information",
|
||||||
|
"properties": {
|
||||||
|
"min-width": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 200,
|
||||||
|
"description": "面板最小宽度 / Minimum panel width"
|
||||||
|
},
|
||||||
|
"min-height": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 200,
|
||||||
|
"description": "面板最小高度 / Minimum panel height"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 400,
|
||||||
|
"description": " 面板默认宽度 / Panel Default Width"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 600,
|
||||||
|
"description": "面板默认高度 / Panel Default Height"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["title", "main"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
@types/schema/package/contributions/index.json
Executable file
47
@types/schema/package/contributions/index.json
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"description": "其他扩展插件的扩展配置 / Extended configuration for other extension plugins",
|
||||||
|
"properties": {
|
||||||
|
"menu": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "菜单配置 / Menu configuration",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "菜单路径 / Menu path"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "菜单标签 / Menu label"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "菜单消息 / Menu message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "消息配置 / Message configuration",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"methods": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"$ref": "../base/panels.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
64
@types/schema/package/index.json
Executable file
64
@types/schema/package/index.json
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"description": "插件定义文件 / Extension definition file",
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "作者 / Author",
|
||||||
|
"default": "Cocos Creator Developer"
|
||||||
|
},
|
||||||
|
"contributions": {
|
||||||
|
"$ref": "./contributions/index.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "发布时所需的依赖库 / Dependencies required for publishing"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "简要介绍扩展关键特性、用途,支持 i18n / Brief introduction of the key features and uses of the extension, supporting i18n"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "开发时所需的依赖库 / Dependencies required for development"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "支持的 Cocos Creator 编辑器版本,支持 semver 格式 / Supported Cocos Creator editor version, supporting semver format"
|
||||||
|
},
|
||||||
|
"main": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "入口函数 / Entry function",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "不能以 _ 或 . 开头、不能含有大写字母,也不能含有 URL 的非法字符例如 .、' 和 ,。 / Cannot start with _ or., cannot contain uppercase letters, and cannot contain URL illegal characters such as.,'and,",
|
||||||
|
"default": "Custom Extension"
|
||||||
|
},
|
||||||
|
"package_version": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "扩展系统预留版本号 / Extension system reserved version number",
|
||||||
|
"default": 2
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"$ref": "./base/panels.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "NPM 脚本 / NPM scripts"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "版本号字符串 / Version number string",
|
||||||
|
"default": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"author",
|
||||||
|
"name",
|
||||||
|
"package_version",
|
||||||
|
"version"
|
||||||
|
]
|
||||||
|
}
|
||||||
1426
FEATURE_GUIDE_CN.md
Normal file
1426
FEATURE_GUIDE_CN.md
Normal file
File diff suppressed because it is too large
Load Diff
1426
FEATURE_GUIDE_EN.md
Normal file
1426
FEATURE_GUIDE_EN.md
Normal file
File diff suppressed because it is too large
Load Diff
361
README.md
Executable file
361
README.md
Executable file
@@ -0,0 +1,361 @@
|
|||||||
|
# Cocos Creator MCP Server Plugin
|
||||||
|
|
||||||
|
A comprehensive MCP (Model Context Protocol) server plugin for Cocos Creator 3.8+, enabling AI assistants to interact with the Cocos Creator editor through standardized protocols. One-click installation and use, eliminating all cumbersome environments and configurations. Claude clients Claude CLI and Cursor have been tested, and other editors are also perfectly supported in theory.
|
||||||
|
|
||||||
|
**🚀 Now provides 80 tools in 9 categories, achieving 95% editor control! (Prefabs cannot be manipulated for the time being)**
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- **[📖 Complete Feature Guide (English)](FEATURE_GUIDE_EN.md)** - Detailed documentation for all 80 tools
|
||||||
|
- **[📖 完整功能指南 (中文)](FEATURE_GUIDE_CN.md)** - 所有80个工具的详细文档
|
||||||
|
- **[🧪 Testing Guide](TEST_GUIDE.md)** - How to test the MCP server
|
||||||
|
**Claude cli configuration:**
|
||||||
|
|
||||||
|
```
|
||||||
|
claude mcp add --transport http http://localhost:3000/mcp (use the port number you configured yourself)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claude client configuration:**
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
|
||||||
|
"mcpServers": {
|
||||||
|
|
||||||
|
"cocos-creator": {
|
||||||
|
|
||||||
|
"type": "http",
|
||||||
|
|
||||||
|
"url": "http://localhost:3000/mcp"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cursor or VS class MCP configuration**
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
|
||||||
|
"mcpServers": {
|
||||||
|
|
||||||
|
"cocos-creator": {
|
||||||
|
"url": "http://localhost:3000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🎯 Scene Operations
|
||||||
|
- Get current scene information and complete scene list
|
||||||
|
- Open scenes by path and save current scene
|
||||||
|
- Create new scenes with custom names
|
||||||
|
- Get complete scene hierarchy with component information
|
||||||
|
|
||||||
|
### 🎮 Node Operations
|
||||||
|
- Create nodes with different types (Node, 2DNode, 3DNode)
|
||||||
|
- Get node information by UUID and find nodes by name pattern
|
||||||
|
- Set node properties (position, rotation, scale, active)
|
||||||
|
- Delete, move, and duplicate nodes with full hierarchy support
|
||||||
|
|
||||||
|
### 🔧 Component Operations
|
||||||
|
- Add/remove components from nodes
|
||||||
|
- Get all components of a node with properties
|
||||||
|
- Set component properties dynamically
|
||||||
|
- Attach script components from asset paths
|
||||||
|
- List available component types by category
|
||||||
|
|
||||||
|
### 📦 Prefab Operations
|
||||||
|
- List all prefabs in project with folder organization
|
||||||
|
- Load, instantiate, and create prefabs
|
||||||
|
- Update existing prefabs and revert prefab instances
|
||||||
|
- Get detailed prefab information including dependencies
|
||||||
|
|
||||||
|
### 🚀 Project Control
|
||||||
|
- Run project in preview mode (browser/simulator)
|
||||||
|
- Build project for different platforms (web, mobile, desktop)
|
||||||
|
- Get project information and settings
|
||||||
|
- Refresh asset database and import new assets
|
||||||
|
- Get detailed asset information
|
||||||
|
|
||||||
|
### 🔍 Debug Tools
|
||||||
|
- Get editor console logs with filtering
|
||||||
|
- Clear console and execute JavaScript in scene context
|
||||||
|
- Get detailed node tree for debugging
|
||||||
|
- Performance statistics and scene validation
|
||||||
|
- Get editor and environment information
|
||||||
|
|
||||||
|
### ⚙️ Additional Features
|
||||||
|
- **Preferences Management**: Get/set editor preferences and global settings
|
||||||
|
- **Server Control**: Server information, project details, and editor control
|
||||||
|
- **Message Broadcasting**: Listen to and broadcast custom messages
|
||||||
|
- **Asset Management**: Create, copy, move, delete, and query assets
|
||||||
|
- **Build System**: Project building and preview server control
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Copy Plugin Files
|
||||||
|
|
||||||
|
Copy the entire `cocos-mcp-server` folder to your Cocos Creator project's `extensions` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
YourProject/
|
||||||
|
├── assets/
|
||||||
|
├── extensions/
|
||||||
|
│ └── cocos-mcp-server/ <- Place plugin here
|
||||||
|
│ ├── source/
|
||||||
|
│ ├── dist/
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── ...
|
||||||
|
├── settings/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd extensions/cocos-mcp-server
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build the Plugin
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Enable Plugin
|
||||||
|
|
||||||
|
1. Restart Cocos Creator or refresh extensions
|
||||||
|
2. The plugin will appear in the Extension menu
|
||||||
|
3. Click `Extension > Cocos MCP Server` to open the control panel
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Starting the Server
|
||||||
|
|
||||||
|
1. Open the MCP Server panel from `Extension > Cocos MCP Server`
|
||||||
|
2. Configure settings:
|
||||||
|
- **Port**: WebSocket server port (default: 3000)
|
||||||
|
- **Auto Start**: Automatically start server when editor opens
|
||||||
|
- **Debug Logging**: Enable detailed logging for development
|
||||||
|
- **Max Connections**: Maximum concurrent connections allowed
|
||||||
|
|
||||||
|
3. Click "Start Server" to begin accepting connections
|
||||||
|
|
||||||
|
### Connecting AI Assistants
|
||||||
|
|
||||||
|
The server exposes a WebSocket endpoint at `ws://localhost:3000` (or your configured port).
|
||||||
|
|
||||||
|
AI assistants can connect using the MCP protocol and access all available tools.
|
||||||
|
|
||||||
|
### Tool Categories
|
||||||
|
|
||||||
|
Tools are organized by category with naming convention: `category_toolname`
|
||||||
|
|
||||||
|
- **scene_\***: Scene-related operations (8 tools)
|
||||||
|
- **node_\***: Node manipulation (9 tools)
|
||||||
|
- **component_\***: Component management (7 tools)
|
||||||
|
- **prefab_\***: Prefab operations (8 tools)
|
||||||
|
- **project_\***: Project control (22 tools)
|
||||||
|
- **debug_\***: Debugging utilities (7 tools)
|
||||||
|
- **preferences_\***: Editor preferences (6 tools)
|
||||||
|
- **server_\***: Server information (8 tools)
|
||||||
|
- **broadcast_\***: Message broadcasting (5 tools)
|
||||||
|
|
||||||
|
**Total: 80 tools** for comprehensive editor control.
|
||||||
|
|
||||||
|
📖 **[View Complete Tool Documentation](FEATURE_GUIDE_EN.md)** for detailed usage examples and parameters.
|
||||||
|
|
||||||
|
## Example Tool Usage
|
||||||
|
|
||||||
|
### Create a new sprite node
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "node_create_node",
|
||||||
|
"arguments": {
|
||||||
|
"name": "MySprite",
|
||||||
|
"nodeType": "2DNode",
|
||||||
|
"parentUuid": "parent-node-uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a Sprite component
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "component_add_component",
|
||||||
|
"arguments": {
|
||||||
|
"nodeUuid": "node-uuid",
|
||||||
|
"componentType": "cc.Sprite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instantiate a prefab
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "prefab_instantiate_prefab",
|
||||||
|
"arguments": {
|
||||||
|
"prefabPath": "db://assets/prefabs/Enemy.prefab",
|
||||||
|
"position": { "x": 100, "y": 200, "z": 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run project in browser
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "project_run_project",
|
||||||
|
"arguments": {
|
||||||
|
"platform": "browser"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Settings are stored in `YourProject/settings/mcp-server.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"port": 3000,
|
||||||
|
"autoStart": false,
|
||||||
|
"enableDebugLog": true,
|
||||||
|
"allowedOrigins": ["*"],
|
||||||
|
"maxConnections": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Icon Setup
|
||||||
|
|
||||||
|
To add an icon for the plugin panel:
|
||||||
|
|
||||||
|
1. Create a PNG icon file (recommended size: 32x32 or 64x64)
|
||||||
|
2. Place it in the `static/` directory: `static/icon.png`
|
||||||
|
3. The icon path is already configured in `package.json`
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
cocos-mcp-server/
|
||||||
|
├── source/ # TypeScript source files
|
||||||
|
│ ├── main.ts # Plugin entry point
|
||||||
|
│ ├── mcp-server.ts # MCP server implementation
|
||||||
|
│ ├── settings.ts # Settings management
|
||||||
|
│ ├── types/ # TypeScript type definitions
|
||||||
|
│ ├── tools/ # Tool implementations
|
||||||
|
│ └── panels/ # UI panel implementation
|
||||||
|
├── dist/ # Compiled JavaScript output
|
||||||
|
├── static/ # Static assets (icons, etc.)
|
||||||
|
├── i18n/ # Internationalization files
|
||||||
|
├── package.json # Plugin configuration
|
||||||
|
└── tsconfig.json # TypeScript configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Build for development with watch mode
|
||||||
|
npm run watch
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Tools
|
||||||
|
|
||||||
|
1. Create a new tool class in `source/tools/`
|
||||||
|
2. Implement the `ToolExecutor` interface
|
||||||
|
3. Add tool to `mcp-server.ts` initialization
|
||||||
|
4. Tools are automatically exposed via MCP protocol
|
||||||
|
|
||||||
|
### TypeScript Support
|
||||||
|
|
||||||
|
The plugin is fully written in TypeScript with:
|
||||||
|
- Strict type checking enabled
|
||||||
|
- Comprehensive type definitions for all APIs
|
||||||
|
- IntelliSense support for development
|
||||||
|
- Automatic compilation to JavaScript
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The project includes comprehensive testing tools:
|
||||||
|
|
||||||
|
- **[Manual Testing Guide](TEST_GUIDE.md)** - Step-by-step testing procedures
|
||||||
|
- **Automated Test Scripts**: `test-mcp-server.js`, `test-all-features.sh`, `test_mcp_server.py`
|
||||||
|
- **Comprehensive Test Suite**: `comprehensive-test.js` - Tests all 80 tools
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run comprehensive test suite
|
||||||
|
node comprehensive-test.js
|
||||||
|
|
||||||
|
# Run feature-specific tests
|
||||||
|
./test-all-features.sh
|
||||||
|
|
||||||
|
# Run Node.js test script
|
||||||
|
node test-mcp-server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Server won't start**: Check port availability and firewall settings
|
||||||
|
2. **Tools not working**: Ensure scene is loaded and UUIDs are valid
|
||||||
|
3. **Build errors**: Run `npm run build` to check for TypeScript errors
|
||||||
|
4. **Connection issues**: Verify WebSocket URL and server status
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging in the plugin panel for detailed operation logs.
|
||||||
|
|
||||||
|
### Using Debug Tools
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "debug_get_console_logs",
|
||||||
|
"arguments": {"limit": 50, "filter": "error"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "debug_validate_scene",
|
||||||
|
"arguments": {"checkMissingAssets": true}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Cocos Creator 3.8.6 or later
|
||||||
|
- Node.js (bundled with Cocos Creator)
|
||||||
|
- TypeScript (installed as dev dependency)
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
This plugin uses a simplified MCP protocol implementation that is compatible with Cocos Creator's CommonJS environment. The WebSocket server provides a JSON-RPC interface for AI assistants to interact with the editor.
|
||||||
|
|
||||||
|
### Protocol Support
|
||||||
|
- **WebSocket Connection**: `ws://localhost:3000` (configurable port)
|
||||||
|
- **JSON-RPC 2.0**: Standard request/response format
|
||||||
|
- **Tool Discovery**: `tools/list` method returns available tools
|
||||||
|
- **Tool Execution**: `tools/call` method executes specific tools
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This plug-in is for Cocos Creator project, and the source code is packaged together, which can be used for learning and communication. It is not encrypted. It can support your own secondary development and optimization. Any code of this project or its derivative code cannot be used for any commercial purpose or resale. If you need commercial use, please contact me.
|
||||||
330
README.zh-CN.md
Executable file
330
README.zh-CN.md
Executable file
@@ -0,0 +1,330 @@
|
|||||||
|
# Cocos Creator MCP 服务器插件
|
||||||
|
|
||||||
|
一个适用于 Cocos Creator 3.8+ 的综合性 MCP(模型上下文协议)服务器插件,使 AI 助手能够通过标准化协议与 Cocos Creator 编辑器进行交互。一键安装和使用,省去所有繁琐环境和配置。已经测试过Claude客户端Claude CLI和Cursor,其他的编辑器理论上也完美支持。
|
||||||
|
|
||||||
|
**🚀 现在提供 9 个类别的 80 个工具,实现95%的编辑器控制!(暂时无法操控预制体)**
|
||||||
|
|
||||||
|
## 快速使用
|
||||||
|
|
||||||
|
**Claude cli配置:**
|
||||||
|
|
||||||
|
```
|
||||||
|
claude mcp add --transport http http://localhost:3000/mcp(使用你自己配置的端口号)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claude客户端配置:**
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
|
||||||
|
"mcpServers": {
|
||||||
|
|
||||||
|
"cocos-creator": {
|
||||||
|
|
||||||
|
"type": "http",
|
||||||
|
|
||||||
|
"url": "http://localhost:3000/mcp"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cursor或VS类MCP配置**
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
|
||||||
|
"mcpServers": {
|
||||||
|
|
||||||
|
"cocos-creator": {
|
||||||
|
"url": "http://localhost:3000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**效果:**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 🎯 场景操作
|
||||||
|
- 获取当前场景信息和完整场景列表
|
||||||
|
- 通过路径打开场景并保存当前场景
|
||||||
|
- 创建自定义名称的新场景
|
||||||
|
- 获取完整场景层级结构及组件信息
|
||||||
|
|
||||||
|
### 🎮 节点操作
|
||||||
|
- 创建不同类型的节点(Node、2DNode、3DNode)
|
||||||
|
- 通过 UUID 获取节点信息,按名称模式查找节点
|
||||||
|
- 设置节点属性(位置、旋转、缩放、激活状态)
|
||||||
|
- 删除、移动和复制节点,完整支持层级结构
|
||||||
|
|
||||||
|
### 🔧 组件操作
|
||||||
|
- 向节点添加/删除组件
|
||||||
|
- 获取节点的所有组件及属性
|
||||||
|
- 动态设置组件属性
|
||||||
|
- 从资源路径挂载脚本组件
|
||||||
|
- 按类别列出可用的组件类型
|
||||||
|
|
||||||
|
### 📦 预制体操作
|
||||||
|
- 列出项目中的所有预制体,支持文件夹组织
|
||||||
|
- 加载、实例化和创建预制体
|
||||||
|
- 更新现有预制体并还原预制体实例
|
||||||
|
- 获取详细的预制体信息,包括依赖关系
|
||||||
|
|
||||||
|
### 🚀 项目控制
|
||||||
|
- 在预览模式下运行项目(浏览器/模拟器)
|
||||||
|
- 为不同平台构建项目(Web、移动端、桌面端)
|
||||||
|
- 获取项目信息和设置
|
||||||
|
- 刷新资源数据库并导入新资源
|
||||||
|
- 获取详细的资源信息
|
||||||
|
|
||||||
|
### 🔍 调试工具
|
||||||
|
- 获取编辑器控制台日志,支持过滤
|
||||||
|
- 清空控制台并在场景上下文中执行 JavaScript
|
||||||
|
- 获取详细的节点树用于调试
|
||||||
|
- 性能统计和场景验证
|
||||||
|
- 获取编辑器和环境信息
|
||||||
|
|
||||||
|
### ⚙️ 其他功能
|
||||||
|
- **偏好设置管理**: 获取/设置编辑器偏好和全局设置
|
||||||
|
- **服务器控制**: 服务器信息、项目详情和编辑器控制
|
||||||
|
- **消息广播**: 监听和广播自定义消息
|
||||||
|
- **资源管理**: 创建、复制、移动、删除和查询资源
|
||||||
|
- **构建系统**: 项目构建和预览服务器控制
|
||||||
|
|
||||||
|
## 安装说明
|
||||||
|
|
||||||
|
### 1. 复制插件文件
|
||||||
|
|
||||||
|
将整个 `cocos-mcp-server` 文件夹复制到您的 Cocos Creator 项目的 `extensions` 目录中:
|
||||||
|
|
||||||
|
```
|
||||||
|
您的项目/
|
||||||
|
├── assets/
|
||||||
|
├── extensions/
|
||||||
|
│ └── cocos-mcp-server/ <- 将插件放在这里
|
||||||
|
│ ├── source/
|
||||||
|
│ ├── dist/
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── ...
|
||||||
|
├── settings/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd extensions/cocos-mcp-server
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 构建插件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 启用插件
|
||||||
|
|
||||||
|
1. 重启 Cocos Creator 或刷新扩展
|
||||||
|
2. 插件将出现在扩展菜单中
|
||||||
|
3. 点击 `扩展 > Cocos MCP Server` 打开控制面板
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 启动服务器
|
||||||
|
|
||||||
|
1. 从 `扩展 > Cocos MCP Server` 打开 MCP 服务器面板
|
||||||
|
2. 配置设置:
|
||||||
|
- **端口**: http 服务器端口(默认:3000)
|
||||||
|
- **自动启动**: 编辑器启动时自动启动服务器
|
||||||
|
- **调试日志**: 启用详细日志以便开发调试
|
||||||
|
- **最大连接数**: 允许的最大并发连接数
|
||||||
|
|
||||||
|
3. 点击"启动服务器"开始接受连接
|
||||||
|
|
||||||
|
### 连接 AI 助手
|
||||||
|
|
||||||
|
服务器在 `http://localhost:3000/mcp`(或您配置的端口)上提供 http 端点。
|
||||||
|
|
||||||
|
AI 助手可以使用 MCP 协议连接并访问所有可用工具。
|
||||||
|
|
||||||
|
### 工具分类
|
||||||
|
|
||||||
|
工具按类别组织,命名约定为:`category_toolname`
|
||||||
|
|
||||||
|
- **scene_\***: 场景相关操作 (8个工具)
|
||||||
|
- **node_\***: 节点操作 (9个工具)
|
||||||
|
- **component_\***: 组件管理 (7个工具)
|
||||||
|
- **prefab_\***: 预制体操作 (8个工具)
|
||||||
|
- **project_\***: 项目控制 (22个工具)
|
||||||
|
- **debug_\***: 调试工具 (7个工具)
|
||||||
|
- **preferences_\***: 编辑器偏好设置 (6个工具)
|
||||||
|
- **server_\***: 服务器信息 (8个工具)
|
||||||
|
- **broadcast_\***: 消息广播 (5个工具)
|
||||||
|
|
||||||
|
**总计: 80个工具** 实现全面的编辑器控制。
|
||||||
|
|
||||||
|
📖 **[查看完整工具文档](FEATURE_GUIDE_CN.md)** 了解详细的使用示例和参数。
|
||||||
|
|
||||||
|
## 工具使用示例
|
||||||
|
|
||||||
|
### 创建新的精灵节点
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "node_create_node",
|
||||||
|
"arguments": {
|
||||||
|
"name": "MySprite",
|
||||||
|
"nodeType": "2DNode",
|
||||||
|
"parentUuid": "parent-node-uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加 Sprite 组件
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "component_add_component",
|
||||||
|
"arguments": {
|
||||||
|
"nodeUuid": "node-uuid",
|
||||||
|
"componentType": "cc.Sprite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实例化预制体
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "prefab_instantiate_prefab",
|
||||||
|
"arguments": {
|
||||||
|
"prefabPath": "db://assets/prefabs/Enemy.prefab",
|
||||||
|
"position": { "x": 100, "y": 200, "z": 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在浏览器中运行项目
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "project_run_project",
|
||||||
|
"arguments": {
|
||||||
|
"platform": "browser"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
设置存储在 `您的项目/settings/mcp-server.json` 中:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"port": 3000,
|
||||||
|
"autoStart": false,
|
||||||
|
"enableDebugLog": true,
|
||||||
|
"allowedOrigins": ["*"],
|
||||||
|
"maxConnections": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 图标设置
|
||||||
|
|
||||||
|
为插件面板添加图标:
|
||||||
|
|
||||||
|
1. 创建 PNG 图标文件(推荐尺寸:32x32 或 64x64)
|
||||||
|
2. 将其放在 `static/` 目录中:`static/icon.png`
|
||||||
|
3. 图标路径已在 `package.json` 中配置
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 项目结构
|
||||||
|
```
|
||||||
|
cocos-mcp-server/
|
||||||
|
├── source/ # TypeScript 源文件
|
||||||
|
│ ├── main.ts # 插件入口点
|
||||||
|
│ ├── mcp-server.ts # MCP 服务器实现
|
||||||
|
│ ├── settings.ts # 设置管理
|
||||||
|
│ ├── types/ # TypeScript 类型定义
|
||||||
|
│ ├── tools/ # 工具实现
|
||||||
|
│ └── panels/ # UI 面板实现
|
||||||
|
├── dist/ # 编译后的 JavaScript 输出
|
||||||
|
├── static/ # 静态资源(图标等)
|
||||||
|
├── i18n/ # 国际化文件
|
||||||
|
├── package.json # 插件配置
|
||||||
|
└── tsconfig.json # TypeScript 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 从源码构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发构建(监视模式)
|
||||||
|
npm run watch
|
||||||
|
|
||||||
|
# 生产构建
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新工具
|
||||||
|
|
||||||
|
1. 在 `source/tools/` 中创建新的工具类
|
||||||
|
2. 实现 `ToolExecutor` 接口
|
||||||
|
3. 将工具添加到 `mcp-server.ts` 初始化中
|
||||||
|
4. 工具会自动通过 MCP 协议暴露
|
||||||
|
|
||||||
|
### TypeScript 支持
|
||||||
|
|
||||||
|
插件完全使用 TypeScript 编写,具备:
|
||||||
|
- 启用严格类型检查
|
||||||
|
- 为所有 API 提供全面的类型定义
|
||||||
|
- 开发时的 IntelliSense 支持
|
||||||
|
- 自动编译为 JavaScript
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **服务器无法启动**: 检查端口可用性和防火墙设置
|
||||||
|
2. **工具不工作**: 确保场景已加载且 UUID 有效
|
||||||
|
3. **构建错误**: 运行 `npm run build` 检查 TypeScript 错误
|
||||||
|
4. **连接问题**: 验证 HTTP URL 和服务器状态
|
||||||
|
|
||||||
|
### 调试模式
|
||||||
|
|
||||||
|
在插件面板中启用调试日志以获取详细的操作日志。
|
||||||
|
|
||||||
|
### 使用调试工具
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "debug_get_console_logs",
|
||||||
|
"arguments": {"limit": 50, "filter": "error"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "debug_validate_scene",
|
||||||
|
"arguments": {"checkMissingAssets": true}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
- Cocos Creator 3.8.0 或更高版本
|
||||||
|
- Node.js(Cocos Creator 自带)
|
||||||
|
- TypeScript(作为开发依赖安装)
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本插件供 Cocos Creator 项目使用,并且源代码一并打包,可以用于学习和交流。没有加密。可以支持你自己二次开发优化,任何本项目代码或者衍生代码均不能用于任何商用、转售,如果需要商用,请联系本人。
|
||||||
22
base.tsconfig.json
Executable file
22
base.tsconfig.json
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./source",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"@cocos/creator-types/editor",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
230
comprehensive-test.js
Normal file
230
comprehensive-test.js
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const SERVER_URL = 'http://localhost:8585';
|
||||||
|
|
||||||
|
class MCPTester {
|
||||||
|
constructor() {
|
||||||
|
this.testResults = [];
|
||||||
|
this.currentCategory = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeRequest(toolName, args = {}) {
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: Date.now(),
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: toolName,
|
||||||
|
arguments: args
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const data = JSON.stringify(payload);
|
||||||
|
const options = {
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: 8585,
|
||||||
|
path: '/mcp',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': data.length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let responseData = '';
|
||||||
|
res.on('data', chunk => responseData += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(responseData);
|
||||||
|
resolve(response);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(data);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async testTool(toolName, args = {}, description = '') {
|
||||||
|
const testName = description || toolName.replace(/_/g, ' ');
|
||||||
|
process.stdout.write(` ${testName}... `);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.makeRequest(toolName, args);
|
||||||
|
|
||||||
|
if (response.result && response.result.success !== false) {
|
||||||
|
console.log('✅ 通过');
|
||||||
|
this.testResults.push({ tool: toolName, status: 'pass', category: this.currentCategory });
|
||||||
|
return response.result;
|
||||||
|
} else {
|
||||||
|
const error = response.result?.error || response.error?.message || 'Unknown error';
|
||||||
|
console.log(`❌ 失败: ${error}`);
|
||||||
|
this.testResults.push({ tool: toolName, status: 'fail', error, category: this.currentCategory });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌ 错误: ${error.message}`);
|
||||||
|
this.testResults.push({ tool: toolName, status: 'error', error: error.message, category: this.currentCategory });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printCategory(categoryName) {
|
||||||
|
this.currentCategory = categoryName;
|
||||||
|
console.log(`\n📁 ${categoryName}:`);
|
||||||
|
}
|
||||||
|
|
||||||
|
printSummary() {
|
||||||
|
console.log('\n' + '='.repeat(60));
|
||||||
|
console.log('📊 测试结果汇总:');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
|
||||||
|
const categories = [...new Set(this.testResults.map(r => r.category))];
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
const categoryResults = this.testResults.filter(r => r.category === category);
|
||||||
|
const passed = categoryResults.filter(r => r.status === 'pass').length;
|
||||||
|
const failed = categoryResults.filter(r => r.status === 'fail').length;
|
||||||
|
const errors = categoryResults.filter(r => r.status === 'error').length;
|
||||||
|
|
||||||
|
console.log(`\n${category}:`);
|
||||||
|
console.log(` ✅ 通过: ${passed}`);
|
||||||
|
console.log(` ❌ 失败: ${failed}`);
|
||||||
|
console.log(` ⚠️ 错误: ${errors}`);
|
||||||
|
console.log(` 📊 总计: ${categoryResults.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPassed = this.testResults.filter(r => r.status === 'pass').length;
|
||||||
|
const totalFailed = this.testResults.filter(r => r.status === 'fail').length;
|
||||||
|
const totalErrors = this.testResults.filter(r => r.status === 'error').length;
|
||||||
|
const total = this.testResults.length;
|
||||||
|
|
||||||
|
console.log(`\n总体结果:`);
|
||||||
|
console.log(` ✅ 通过: ${totalPassed}/${total} (${((totalPassed/total)*100).toFixed(1)}%)`);
|
||||||
|
console.log(` ❌ 失败: ${totalFailed}/${total} (${((totalFailed/total)*100).toFixed(1)}%)`);
|
||||||
|
console.log(` ⚠️ 错误: ${totalErrors}/${total} (${((totalErrors/total)*100).toFixed(1)}%)`);
|
||||||
|
|
||||||
|
// 显示失败的工具
|
||||||
|
const failedTools = this.testResults.filter(r => r.status === 'fail' || r.status === 'error');
|
||||||
|
if (failedTools.length > 0) {
|
||||||
|
console.log('\n❌ 失败的工具:');
|
||||||
|
failedTools.forEach(tool => {
|
||||||
|
console.log(` - ${tool.tool}: ${tool.error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAllTests() {
|
||||||
|
console.log('🚀 开始全面测试 Cocos MCP 工具...\n');
|
||||||
|
|
||||||
|
// 1. 场景管理工具
|
||||||
|
this.printCategory('场景管理');
|
||||||
|
await this.testTool('scene_create_scene', {
|
||||||
|
sceneName: 'TestScene2',
|
||||||
|
savePath: 'db://assets/scenes/TestScene2.scene'
|
||||||
|
}, '创建场景');
|
||||||
|
await this.testTool('scene_get_current_scene', {}, '获取当前场景');
|
||||||
|
await this.testTool('scene_save_scene', {}, '保存场景');
|
||||||
|
await this.testTool('scene_get_scene_list', {}, '获取场景列表');
|
||||||
|
await this.testTool('scene_get_scene_hierarchy', {}, '获取场景层次结构');
|
||||||
|
|
||||||
|
// 2. 节点操作工具
|
||||||
|
this.printCategory('节点操作');
|
||||||
|
const nodeResult = await this.testTool('node_create_node', { name: 'TestNode' }, '创建节点');
|
||||||
|
let testNodeUuid = null;
|
||||||
|
if (nodeResult && nodeResult.data && nodeResult.data.uuid) {
|
||||||
|
testNodeUuid = nodeResult.data.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.testTool('node_get_all_nodes', {}, '获取所有节点');
|
||||||
|
await this.testTool('node_find_node_by_name', { name: 'TestNode' }, '按名称查找节点');
|
||||||
|
|
||||||
|
if (testNodeUuid) {
|
||||||
|
await this.testTool('node_get_node_info', { uuid: testNodeUuid }, '获取节点信息');
|
||||||
|
await this.testTool('node_set_node_property', {
|
||||||
|
uuid: testNodeUuid,
|
||||||
|
property: 'position',
|
||||||
|
value: { x: 100, y: 100, z: 0 }
|
||||||
|
}, '设置节点属性');
|
||||||
|
await this.testTool('node_duplicate_node', { uuid: testNodeUuid }, '复制节点');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 组件管理工具
|
||||||
|
this.printCategory('组件管理');
|
||||||
|
if (testNodeUuid) {
|
||||||
|
await this.testTool('component_add_component', {
|
||||||
|
nodeUuid: testNodeUuid,
|
||||||
|
componentType: 'cc.Sprite'
|
||||||
|
}, '添加组件');
|
||||||
|
await this.testTool('component_get_components', { nodeUuid: testNodeUuid }, '获取组件列表');
|
||||||
|
await this.testTool('component_get_component_info', {
|
||||||
|
nodeUuid: testNodeUuid,
|
||||||
|
componentType: 'cc.Sprite'
|
||||||
|
}, '获取组件信息');
|
||||||
|
}
|
||||||
|
await this.testTool('component_get_available_components', {}, '获取可用组件');
|
||||||
|
|
||||||
|
// 4. 预制体工具
|
||||||
|
this.printCategory('预制体管理');
|
||||||
|
if (testNodeUuid) {
|
||||||
|
await this.testTool('prefab_create_prefab', {
|
||||||
|
nodeUuid: testNodeUuid,
|
||||||
|
prefabPath: 'db://assets/prefabs/TestPrefab.prefab'
|
||||||
|
}, '创建预制体');
|
||||||
|
}
|
||||||
|
await this.testTool('prefab_get_prefab_list', {}, '获取预制体列表');
|
||||||
|
await this.testTool('prefab_instantiate_prefab', {
|
||||||
|
prefabPath: 'db://assets/prefabs/TestPrefab.prefab'
|
||||||
|
}, '实例化预制体');
|
||||||
|
|
||||||
|
// 5. 项目和资源管理
|
||||||
|
this.printCategory('项目资源管理');
|
||||||
|
await this.testTool('project_get_project_info', {}, '获取项目信息');
|
||||||
|
await this.testTool('project_get_project_settings', {}, '获取项目设置');
|
||||||
|
await this.testTool('project_get_assets', { type: 'all' }, '获取资源列表');
|
||||||
|
await this.testTool('project_refresh_assets', {}, '刷新资源');
|
||||||
|
await this.testTool('project_create_asset', {
|
||||||
|
url: 'db://assets/test-folder',
|
||||||
|
content: null
|
||||||
|
}, '创建资源文件夹');
|
||||||
|
|
||||||
|
// 6. 调试工具
|
||||||
|
this.printCategory('调试工具');
|
||||||
|
await this.testTool('debug_get_console_logs', {}, '获取控制台日志');
|
||||||
|
await this.testTool('debug_get_performance_stats', {}, '获取性能统计');
|
||||||
|
await this.testTool('debug_validate_scene', {}, '验证场景');
|
||||||
|
await this.testTool('debug_get_editor_info', {}, '获取编辑器信息');
|
||||||
|
|
||||||
|
// 7. 偏好设置工具
|
||||||
|
this.printCategory('偏好设置');
|
||||||
|
await this.testTool('preferences_get_preferences', {}, '获取偏好设置');
|
||||||
|
await this.testTool('preferences_get_global_preferences', {}, '获取全局偏好设置');
|
||||||
|
await this.testTool('preferences_get_recent_projects', {}, '获取最近项目');
|
||||||
|
|
||||||
|
// 8. 服务器控制工具
|
||||||
|
this.printCategory('服务器控制');
|
||||||
|
await this.testTool('server_get_server_info', {}, '获取服务器信息');
|
||||||
|
await this.testTool('server_get_editor_version', {}, '获取编辑器版本');
|
||||||
|
|
||||||
|
// 9. 广播消息工具
|
||||||
|
this.printCategory('广播消息');
|
||||||
|
await this.testTool('broadcast_get_broadcast_log', {}, '获取广播日志');
|
||||||
|
|
||||||
|
// 清理测试节点
|
||||||
|
if (testNodeUuid) {
|
||||||
|
await this.testTool('node_delete_node', { uuid: testNodeUuid }, '删除测试节点');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.printSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
const tester = new MCPTester();
|
||||||
|
tester.runAllTests().catch(console.error);
|
||||||
46
debug-api.js
Normal file
46
debug-api.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// 测试可用的 Editor API
|
||||||
|
const testMethods = [
|
||||||
|
'scene.query-node-tree',
|
||||||
|
'scene.create-node',
|
||||||
|
'scene.remove-node',
|
||||||
|
'scene.set-property',
|
||||||
|
'scene.create-component',
|
||||||
|
'scene.remove-component',
|
||||||
|
'scene.add-component',
|
||||||
|
'scene.query-node',
|
||||||
|
'scene.query-scene',
|
||||||
|
'scene.query-hierarchy'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function testAPI() {
|
||||||
|
console.log('Testing available Editor API methods...');
|
||||||
|
|
||||||
|
for (const method of testMethods) {
|
||||||
|
console.log(`\nTesting: ${method}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:8585/mcp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'debug_test_editor_api',
|
||||||
|
arguments: {
|
||||||
|
method: method.replace('.', ' - ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`Response: ${JSON.stringify(data)}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testAPI();
|
||||||
119
dist/main.js
vendored
Normal file
119
dist/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
268
dist/mcp-server.js
vendored
Normal file
268
dist/mcp-server.js
vendored
Normal file
File diff suppressed because one or more lines are too long
225
dist/panels/default/index.js
vendored
Normal file
225
dist/panels/default/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
428
dist/scene.js
vendored
Normal file
428
dist/scene.js
vendored
Normal file
File diff suppressed because one or more lines are too long
83
dist/settings.js
vendored
Normal file
83
dist/settings.js
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.DEFAULT_SETTINGS = void 0;
|
||||||
|
exports.readSettings = readSettings;
|
||||||
|
exports.saveSettings = saveSettings;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const path = __importStar(require("path"));
|
||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
port: 3000,
|
||||||
|
autoStart: false,
|
||||||
|
enableDebugLog: false,
|
||||||
|
allowedOrigins: ['*'],
|
||||||
|
maxConnections: 10
|
||||||
|
};
|
||||||
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
||||||
|
function getSettingsPath() {
|
||||||
|
return path.join(Editor.Project.path, 'settings', 'mcp-server.json');
|
||||||
|
}
|
||||||
|
function ensureSettingsDir() {
|
||||||
|
const settingsDir = path.dirname(getSettingsPath());
|
||||||
|
if (!fs.existsSync(settingsDir)) {
|
||||||
|
fs.mkdirSync(settingsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function readSettings() {
|
||||||
|
try {
|
||||||
|
ensureSettingsDir();
|
||||||
|
const settingsFile = getSettingsPath();
|
||||||
|
if (fs.existsSync(settingsFile)) {
|
||||||
|
const content = fs.readFileSync(settingsFile, 'utf8');
|
||||||
|
return Object.assign(Object.assign({}, DEFAULT_SETTINGS), JSON.parse(content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Failed to read settings:', e);
|
||||||
|
}
|
||||||
|
return DEFAULT_SETTINGS;
|
||||||
|
}
|
||||||
|
function saveSettings(settings) {
|
||||||
|
try {
|
||||||
|
ensureSettingsDir();
|
||||||
|
const settingsFile = getSettingsPath();
|
||||||
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Failed to save settings:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2V0dGluZ3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2Uvc2V0dGluZ3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBdUJBLG9DQVlDO0FBRUQsb0NBU0M7QUE5Q0QsdUNBQXlCO0FBQ3pCLDJDQUE2QjtBQUc3QixNQUFNLGdCQUFnQixHQUFzQjtJQUN4QyxJQUFJLEVBQUUsSUFBSTtJQUNWLFNBQVMsRUFBRSxLQUFLO0lBQ2hCLGNBQWMsRUFBRSxLQUFLO0lBQ3JCLGNBQWMsRUFBRSxDQUFDLEdBQUcsQ0FBQztJQUNyQixjQUFjLEVBQUUsRUFBRTtDQUNyQixDQUFDO0FBc0NPLDRDQUFnQjtBQXBDekIsU0FBUyxlQUFlO0lBQ3BCLE9BQU8sSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxVQUFVLEVBQUUsaUJBQWlCLENBQUMsQ0FBQztBQUN6RSxDQUFDO0FBRUQsU0FBUyxpQkFBaUI7SUFDdEIsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxlQUFlLEVBQUUsQ0FBQyxDQUFDO0lBQ3BELElBQUksQ0FBQyxFQUFFLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxFQUFFLENBQUM7UUFDOUIsRUFBRSxDQUFDLFNBQVMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUNuRCxDQUFDO0FBQ0wsQ0FBQztBQUVELFNBQWdCLFlBQVk7SUFDeEIsSUFBSSxDQUFDO1FBQ0QsaUJBQWlCLEVBQUUsQ0FBQztRQUNwQixNQUFNLFlBQVksR0FBRyxlQUFlLEVBQUUsQ0FBQztRQUN2QyxJQUFJLEVBQUUsQ0FBQyxVQUFVLENBQUMsWUFBWSxDQUFDLEVBQUUsQ0FBQztZQUM5QixNQUFNLE9BQU8sR0FBRyxFQUFFLENBQUMsWUFBWSxDQUFDLFlBQVksRUFBRSxNQUFNLENBQUMsQ0FBQztZQUN0RCx1Q0FBWSxnQkFBZ0IsR0FBSyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxFQUFHO1FBQzNELENBQUM7SUFDTCxDQUFDO0lBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztRQUNULE9BQU8sQ0FBQyxLQUFLLENBQUMsMEJBQTBCLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDakQsQ0FBQztJQUNELE9BQU8sZ0JBQWdCLENBQUM7QUFDNUIsQ0FBQztBQUVELFNBQWdCLFlBQVksQ0FBQyxRQUEyQjtJQUNwRCxJQUFJLENBQUM7UUFDRCxpQkFBaUIsRUFBRSxDQUFDO1FBQ3BCLE1BQU0sWUFBWSxHQUFHLGVBQWUsRUFBRSxDQUFDO1FBQ3ZDLEVBQUUsQ0FBQyxhQUFhLENBQUMsWUFBWSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3RFLENBQUM7SUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1FBQ1QsT0FBTyxDQUFDLEtBQUssQ0FBQywwQkFBMEIsRUFBRSxDQUFDLENBQUMsQ0FBQztRQUM3QyxNQUFNLENBQUMsQ0FBQztJQUNaLENBQUM7QUFDTCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgZnMgZnJvbSAnZnMnO1xuaW1wb3J0ICogYXMgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IE1DUFNlcnZlclNldHRpbmdzIH0gZnJvbSAnLi90eXBlcyc7XG5cbmNvbnN0IERFRkFVTFRfU0VUVElOR1M6IE1DUFNlcnZlclNldHRpbmdzID0ge1xuICAgIHBvcnQ6IDMwMDAsXG4gICAgYXV0b1N0YXJ0OiBmYWxzZSxcbiAgICBlbmFibGVEZWJ1Z0xvZzogZmFsc2UsXG4gICAgYWxsb3dlZE9yaWdpbnM6IFsnKiddLFxuICAgIG1heENvbm5lY3Rpb25zOiAxMFxufTtcblxuZnVuY3Rpb24gZ2V0U2V0dGluZ3NQYXRoKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIHBhdGguam9pbihFZGl0b3IuUHJvamVjdC5wYXRoLCAnc2V0dGluZ3MnLCAnbWNwLXNlcnZlci5qc29uJyk7XG59XG5cbmZ1bmN0aW9uIGVuc3VyZVNldHRpbmdzRGlyKCk6IHZvaWQge1xuICAgIGNvbnN0IHNldHRpbmdzRGlyID0gcGF0aC5kaXJuYW1lKGdldFNldHRpbmdzUGF0aCgpKTtcbiAgICBpZiAoIWZzLmV4aXN0c1N5bmMoc2V0dGluZ3NEaXIpKSB7XG4gICAgICAgIGZzLm1rZGlyU3luYyhzZXR0aW5nc0RpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG4gICAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVhZFNldHRpbmdzKCk6IE1DUFNlcnZlclNldHRpbmdzIHtcbiAgICB0cnkge1xuICAgICAgICBlbnN1cmVTZXR0aW5nc0RpcigpO1xuICAgICAgICBjb25zdCBzZXR0aW5nc0ZpbGUgPSBnZXRTZXR0aW5nc1BhdGgoKTtcbiAgICAgICAgaWYgKGZzLmV4aXN0c1N5bmMoc2V0dGluZ3NGaWxlKSkge1xuICAgICAgICAgICAgY29uc3QgY29udGVudCA9IGZzLnJlYWRGaWxlU3luYyhzZXR0aW5nc0ZpbGUsICd1dGY4Jyk7XG4gICAgICAgICAgICByZXR1cm4geyAuLi5ERUZBVUxUX1NFVFRJTkdTLCAuLi5KU09OLnBhcnNlKGNvbnRlbnQpIH07XG4gICAgICAgIH1cbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IoJ0ZhaWxlZCB0byByZWFkIHNldHRpbmdzOicsIGUpO1xuICAgIH1cbiAgICByZXR1cm4gREVGQVVMVF9TRVRUSU5HUztcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNhdmVTZXR0aW5ncyhzZXR0aW5nczogTUNQU2VydmVyU2V0dGluZ3MpOiB2b2lkIHtcbiAgICB0cnkge1xuICAgICAgICBlbnN1cmVTZXR0aW5nc0RpcigpO1xuICAgICAgICBjb25zdCBzZXR0aW5nc0ZpbGUgPSBnZXRTZXR0aW5nc1BhdGgoKTtcbiAgICAgICAgZnMud3JpdGVGaWxlU3luYyhzZXR0aW5nc0ZpbGUsIEpTT04uc3RyaW5naWZ5KHNldHRpbmdzLCBudWxsLCAyKSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjb25zb2xlLmVycm9yKCdGYWlsZWQgdG8gc2F2ZSBzZXR0aW5nczonLCBlKTtcbiAgICAgICAgdGhyb3cgZTtcbiAgICB9XG59XG5cbmV4cG9ydCB7IERFRkFVTFRfU0VUVElOR1MgfTsiXX0=
|
||||||
118
dist/test/manual-test.js
vendored
Normal file
118
dist/test/manual-test.js
vendored
Normal file
File diff suppressed because one or more lines are too long
217
dist/test/mcp-tool-tester.js
vendored
Normal file
217
dist/test/mcp-tool-tester.js
vendored
Normal file
File diff suppressed because one or more lines are too long
134
dist/test/tool-tester.js
vendored
Normal file
134
dist/test/tool-tester.js
vendored
Normal file
File diff suppressed because one or more lines are too long
251
dist/tools/broadcast-tools.js
vendored
Normal file
251
dist/tools/broadcast-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
426
dist/tools/component-tools.js
vendored
Normal file
426
dist/tools/component-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
327
dist/tools/debug-tools.js
vendored
Normal file
327
dist/tools/debug-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
510
dist/tools/node-tools.js
vendored
Normal file
510
dist/tools/node-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
342
dist/tools/prefab-tools.js
vendored
Normal file
342
dist/tools/prefab-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
159
dist/tools/preferences-tools.js
vendored
Normal file
159
dist/tools/preferences-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
893
dist/tools/project-tools.js
vendored
Normal file
893
dist/tools/project-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
450
dist/tools/scene-tools.js
vendored
Normal file
450
dist/tools/scene-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
247
dist/tools/server-tools.js
vendored
Normal file
247
dist/tools/server-tools.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/types/index.js
vendored
Normal file
3
dist/types/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvdHlwZXMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgTUNQU2VydmVyU2V0dGluZ3Mge1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBhdXRvU3RhcnQ6IGJvb2xlYW47XG4gICAgZW5hYmxlRGVidWdMb2c6IGJvb2xlYW47XG4gICAgYWxsb3dlZE9yaWdpbnM6IHN0cmluZ1tdO1xuICAgIG1heENvbm5lY3Rpb25zOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2VydmVyU3RhdHVzIHtcbiAgICBydW5uaW5nOiBib29sZWFuO1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBjbGllbnRzOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbERlZmluaXRpb24ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBkZXNjcmlwdGlvbjogc3RyaW5nO1xuICAgIGlucHV0U2NoZW1hOiBhbnk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbFJlc3BvbnNlIHtcbiAgICBzdWNjZXNzOiBib29sZWFuO1xuICAgIGRhdGE/OiBhbnk7XG4gICAgbWVzc2FnZT86IHN0cmluZztcbiAgICBlcnJvcj86IHN0cmluZztcbiAgICBpbnN0cnVjdGlvbj86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBOb2RlSW5mbyB7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBhY3RpdmU6IGJvb2xlYW47XG4gICAgcG9zaXRpb24/OiB7IHg6IG51bWJlcjsgeTogbnVtYmVyOyB6OiBudW1iZXIgfTtcbiAgICByb3RhdGlvbj86IHsgeDogbnVtYmVyOyB5OiBudW1iZXI7IHo6IG51bWJlciB9O1xuICAgIHNjYWxlPzogeyB4OiBudW1iZXI7IHk6IG51bWJlcjsgejogbnVtYmVyIH07XG4gICAgcGFyZW50Pzogc3RyaW5nO1xuICAgIGNoaWxkcmVuPzogc3RyaW5nW107XG4gICAgY29tcG9uZW50cz86IENvbXBvbmVudEluZm9bXTtcbiAgICBsYXllcj86IG51bWJlcjtcbiAgICBtb2JpbGl0eT86IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb21wb25lbnRJbmZvIHtcbiAgICB0eXBlOiBzdHJpbmc7XG4gICAgZW5hYmxlZDogYm9vbGVhbjtcbiAgICBwcm9wZXJ0aWVzPzogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBTY2VuZUluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFByZWZhYkluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIGZvbGRlcjogc3RyaW5nO1xuICAgIGNyZWF0ZVRpbWU/OiBzdHJpbmc7XG4gICAgbW9kaWZ5VGltZT86IHN0cmluZztcbiAgICBkZXBlbmRlbmNpZXM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBBc3NldEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIHR5cGU6IHN0cmluZztcbiAgICBzaXplPzogbnVtYmVyO1xuICAgIGlzRGlyZWN0b3J5OiBib29sZWFuO1xuICAgIG1ldGE/OiB7XG4gICAgICAgIHZlcjogc3RyaW5nO1xuICAgICAgICBpbXBvcnRlcjogc3RyaW5nO1xuICAgIH07XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUHJvamVjdEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBwYXRoOiBzdHJpbmc7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIHZlcnNpb246IHN0cmluZztcbiAgICBjb2Nvc1ZlcnNpb246IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb25zb2xlTWVzc2FnZSB7XG4gICAgdGltZXN0YW1wOiBzdHJpbmc7XG4gICAgdHlwZTogJ2xvZycgfCAnd2FybicgfCAnZXJyb3InIHwgJ2luZm8nO1xuICAgIG1lc3NhZ2U6IHN0cmluZztcbiAgICBzdGFjaz86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBQZXJmb3JtYW5jZVN0YXRzIHtcbiAgICBub2RlQ291bnQ6IG51bWJlcjtcbiAgICBjb21wb25lbnRDb3VudDogbnVtYmVyO1xuICAgIGRyYXdDYWxsczogbnVtYmVyO1xuICAgIHRyaWFuZ2xlczogbnVtYmVyO1xuICAgIG1lbW9yeTogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBWYWxpZGF0aW9uSXNzdWUge1xuICAgIHR5cGU6ICdlcnJvcicgfCAnd2FybmluZycgfCAnaW5mbyc7XG4gICAgY2F0ZWdvcnk6IHN0cmluZztcbiAgICBtZXNzYWdlOiBzdHJpbmc7XG4gICAgZGV0YWlscz86IGFueTtcbiAgICBzdWdnZXN0aW9uPzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFZhbGlkYXRpb25SZXN1bHQge1xuICAgIHZhbGlkOiBib29sZWFuO1xuICAgIGlzc3VlQ291bnQ6IG51bWJlcjtcbiAgICBpc3N1ZXM6IFZhbGlkYXRpb25Jc3N1ZVtdO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1DUENsaWVudCB7XG4gICAgaWQ6IHN0cmluZztcbiAgICBsYXN0QWN0aXZpdHk6IERhdGU7XG4gICAgdXNlckFnZW50Pzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFRvb2xFeGVjdXRvciB7XG4gICAgZ2V0VG9vbHMoKTogVG9vbERlZmluaXRpb25bXTtcbiAgICBleGVjdXRlKHRvb2xOYW1lOiBzdHJpbmcsIGFyZ3M6IGFueSk6IFByb21pc2U8VG9vbFJlc3BvbnNlPjtcbn0iXX0=
|
||||||
31
i18n/en.js
Executable file
31
i18n/en.js
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP Server",
|
||||||
|
"description": "AI MCP Server for Cocos Creator 3.8",
|
||||||
|
"panel_title": "MCP Server",
|
||||||
|
"open_panel": "Open MCP Panel",
|
||||||
|
"start_server": "Start Server",
|
||||||
|
"stop_server": "Stop Server",
|
||||||
|
"server_status": "Server Status",
|
||||||
|
"port": "Port",
|
||||||
|
"settings": "Settings",
|
||||||
|
"connected": "Connected",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"server_running": "Server is running on port {0}",
|
||||||
|
"server_stopped": "Server has stopped",
|
||||||
|
"auto_start": "Auto Start",
|
||||||
|
"debug_log": "Debug Logging",
|
||||||
|
"max_connections": "Max Connections",
|
||||||
|
"connection_info": "Connection Info",
|
||||||
|
"http_url": "HTTP URL",
|
||||||
|
"copy": "Copy",
|
||||||
|
"save_settings": "Save Settings",
|
||||||
|
"settings_saved": "Settings saved successfully",
|
||||||
|
"server_started": "MCP Server Started",
|
||||||
|
"server_stopped_msg": "MCP Server Stopped",
|
||||||
|
"failed_to_start": "Failed to start server",
|
||||||
|
"failed_to_stop": "Failed to stop server",
|
||||||
|
"failed_to_save": "Failed to save settings",
|
||||||
|
"url_copied": "HTTP URL copied to clipboard"
|
||||||
|
};
|
||||||
31
i18n/zh.js
Executable file
31
i18n/zh.js
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"extension_name": "Cocos MCP 服务器",
|
||||||
|
"description": "适用于 Cocos Creator 3.8 的 AI MCP 服务器",
|
||||||
|
"panel_title": "MCP 服务器",
|
||||||
|
"open_panel": "打开 MCP 面板",
|
||||||
|
"start_server": "启动服务器",
|
||||||
|
"stop_server": "停止服务器",
|
||||||
|
"server_status": "服务器状态",
|
||||||
|
"port": "端口",
|
||||||
|
"settings": "设置",
|
||||||
|
"connected": "已连接",
|
||||||
|
"disconnected": "未连接",
|
||||||
|
"server_running": "服务器正在端口 {0} 上运行",
|
||||||
|
"server_stopped": "服务器已停止",
|
||||||
|
"auto_start": "自动启动",
|
||||||
|
"debug_log": "调试日志",
|
||||||
|
"max_connections": "最大连接数",
|
||||||
|
"connection_info": "连接信息",
|
||||||
|
"http_url": "HTTP 地址",
|
||||||
|
"copy": "复制",
|
||||||
|
"save_settings": "保存设置",
|
||||||
|
"settings_saved": "设置保存成功",
|
||||||
|
"server_started": "MCP 服务器已启动",
|
||||||
|
"server_stopped_msg": "MCP 服务器已停止",
|
||||||
|
"failed_to_start": "启动服务器失败",
|
||||||
|
"failed_to_stop": "停止服务器失败",
|
||||||
|
"failed_to_save": "保存设置失败",
|
||||||
|
"url_copied": "HTTP 地址已复制到剪贴板"
|
||||||
|
};
|
||||||
BIN
image/iamge2.png
Normal file
BIN
image/iamge2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 299 KiB |
BIN
image/image-20250717174157957.png
Normal file
BIN
image/image-20250717174157957.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 321 KiB |
166
package-lock.json
generated
Normal file
166
package-lock.json
generated
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
{
|
||||||
|
"name": "cocos-mcp-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "cocos-mcp-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": "^11.3.0",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"ws": "^8.14.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cocos/creator-types": "^3.8.6",
|
||||||
|
"@types/fs-extra": "^9.0.5",
|
||||||
|
"@types/node": "^18.17.1",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cocos/creator-types": {
|
||||||
|
"version": "3.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cocos/creator-types/-/creator-types-3.8.6.tgz",
|
||||||
|
"integrity": "sha512-hyZ4aoqqLxoRtKbBLSJM5RgtK3oGOlTEryHDcyH4znq3h9cFk+MSbQC2aJHvK5/bMlJzsZ641/hD77RGSrvo8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/@types/fs-extra": {
|
||||||
|
"version": "9.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||||
|
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "18.19.119",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.119.tgz",
|
||||||
|
"integrity": "sha512-d0F6m9itIPaKnrvEMlzE48UjwZaAnFW7Jwibacw9MNdqadjKNpUm9tfJYDwmShJmgqcoqYUX3EMKO1+RWiuuNg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "9.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||||
|
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "11.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||||
|
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
package.json
Executable file
102
package.json
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./@types/schema/package/index.json",
|
||||||
|
"package_version": 2,
|
||||||
|
"name": "cocos-mcp-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Cocos Creator",
|
||||||
|
"editor": ">=3.8.6",
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "node ./scripts/preinstall.js",
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc -w"
|
||||||
|
},
|
||||||
|
"description": "i18n:cocos-mcp-server.description",
|
||||||
|
"main": "./dist/main.js",
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": "^11.3.0",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cocos/creator-types": "^3.8.6",
|
||||||
|
"@types/fs-extra": "^9.0.5",
|
||||||
|
"@types/node": "^18.17.1",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"default": {
|
||||||
|
"title": "i18n:cocos-mcp-server.panel_title",
|
||||||
|
"type": "dockable",
|
||||||
|
"main": "dist/panels/default",
|
||||||
|
"icon": "./static/icon.png",
|
||||||
|
"size": {
|
||||||
|
"min-width": 400,
|
||||||
|
"min-height": 300,
|
||||||
|
"width": 600,
|
||||||
|
"height": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contributions": {
|
||||||
|
"menu": [
|
||||||
|
{
|
||||||
|
"path": "i18n:menu.extension/Cocos MCP Server",
|
||||||
|
"label": "i18n:cocos-mcp-server.open_panel",
|
||||||
|
"message": "open-panel"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": {
|
||||||
|
"open-panel": {
|
||||||
|
"methods": [
|
||||||
|
"openPanel"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"start-server": {
|
||||||
|
"methods": [
|
||||||
|
"startServer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stop-server": {
|
||||||
|
"methods": [
|
||||||
|
"stopServer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-server-status": {
|
||||||
|
"methods": [
|
||||||
|
"getServerStatus"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"update-settings": {
|
||||||
|
"methods": [
|
||||||
|
"updateSettings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"getToolsList": {
|
||||||
|
"methods": [
|
||||||
|
"getToolsList"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-server-settings": {
|
||||||
|
"methods": [
|
||||||
|
"getServerSettings"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scene": {
|
||||||
|
"script": "./dist/scene.js",
|
||||||
|
"methods": [
|
||||||
|
"createNewScene",
|
||||||
|
"addComponentToNode",
|
||||||
|
"removeComponentFromNode",
|
||||||
|
"getNodeInfo",
|
||||||
|
"getAllNodes",
|
||||||
|
"findNodeByName",
|
||||||
|
"setNodeProperty",
|
||||||
|
"setComponentProperty",
|
||||||
|
"getCurrentSceneInfo",
|
||||||
|
"getSceneHierarchy",
|
||||||
|
"createPrefabFromNode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
scripts/preinstall.js
Executable file
1
scripts/preinstall.js
Executable file
@@ -0,0 +1 @@
|
|||||||
|
const readFileSync=require("fs")["readFileSync"],join=require("path")["join"],spawnSync=require("child_process")["spawnSync"],PATH={packageJSON:join(__dirname,"../package.json")};function checkCreatorTypesVersion(e){var o="win32"===process.platform?"npm.cmd":"npm";let n=spawnSync(o,["view","@cocos/creator-types","versions"]).stdout.toString();try{n=JSON.parse(listString)}catch(e){}return!!n.includes(e)}try{const e=readFileSync(PATH.packageJSON,"utf8"),f=JSON.parse(e),g=f.devDependencies["@cocos/creator-types"].replace(/^[^\d]+/,"");checkCreatorTypesVersion(g)||(console.log("[33mWarning:[0m"),console.log(" @en"),console.log(" Version check of @cocos/creator-types failed."),console.log(` The definition of ${g} has not been released yet. Please export the definition to the ./node_modules directory by selecting "Developer -> Export Interface Definition" in the menu of the Creator editor.`),console.log(" The definition of the corresponding version will be released on npm after the editor is officially released."),console.log(" @zh"),console.log(" @cocos/creator-types 版本检查失败。"),console.log(` ${g} 定义还未发布,请先通过 Creator 编辑器菜单 "开发者 -> 导出接口定义",导出定义到 ./node_modules 目录。`),console.log(" 对应版本的定义会在编辑器正式发布后同步发布到 npm 上。"))}catch(e){console.error(e)}
|
||||||
119
source/main.ts
Executable file
119
source/main.ts
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
import { MCPServer } from './mcp-server';
|
||||||
|
import { readSettings, saveSettings } from './settings';
|
||||||
|
import { MCPServerSettings } from './types';
|
||||||
|
|
||||||
|
let mcpServer: MCPServer | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Registration method for the main process of Extension
|
||||||
|
* @zh 为扩展的主进程的注册方法
|
||||||
|
*/
|
||||||
|
export const methods: { [key: string]: (...any: any) => any } = {
|
||||||
|
/**
|
||||||
|
* @en Open the MCP server panel
|
||||||
|
* @zh 打开 MCP 服务器面板
|
||||||
|
*/
|
||||||
|
openPanel() {
|
||||||
|
Editor.Panel.open('cocos-mcp-server');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Start the MCP server
|
||||||
|
* @zh 启动 MCP 服务器
|
||||||
|
*/
|
||||||
|
async startServer() {
|
||||||
|
if (mcpServer) {
|
||||||
|
await mcpServer.start();
|
||||||
|
} else {
|
||||||
|
console.warn('[MCP插件] mcpServer 未初始化');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Stop the MCP server
|
||||||
|
* @zh 停止 MCP 服务器
|
||||||
|
*/
|
||||||
|
async stopServer() {
|
||||||
|
if (mcpServer) {
|
||||||
|
mcpServer.stop();
|
||||||
|
} else {
|
||||||
|
console.warn('[MCP插件] mcpServer 未初始化');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Get server status
|
||||||
|
* @zh 获取服务器状态
|
||||||
|
*/
|
||||||
|
getServerStatus() {
|
||||||
|
return mcpServer ? mcpServer.getStatus() : { running: false, port: 0, clients: 0 };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Update server settings
|
||||||
|
* @zh 更新服务器设置
|
||||||
|
*/
|
||||||
|
updateSettings(settings: MCPServerSettings) {
|
||||||
|
saveSettings(settings);
|
||||||
|
if (mcpServer) {
|
||||||
|
mcpServer.stop();
|
||||||
|
mcpServer = new MCPServer(settings);
|
||||||
|
mcpServer.start();
|
||||||
|
} else {
|
||||||
|
mcpServer = new MCPServer(settings);
|
||||||
|
mcpServer.start();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Get tools list
|
||||||
|
* @zh 获取工具列表
|
||||||
|
*/
|
||||||
|
getToolsList() {
|
||||||
|
return mcpServer ? mcpServer.getAvailableTools() : [];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @en Get server settings
|
||||||
|
* @zh 获取服务器设置
|
||||||
|
*/
|
||||||
|
getServerSettings() {
|
||||||
|
return mcpServer ? mcpServer.getSettings() : readSettings();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Method Triggered on Extension Startup
|
||||||
|
* @zh 扩展启动时触发的方法
|
||||||
|
*/
|
||||||
|
export function load() {
|
||||||
|
console.log('[MCP Plugin] Loading MCP server plugin...');
|
||||||
|
try {
|
||||||
|
const settings = readSettings();
|
||||||
|
console.log('[MCP Plugin] Settings loaded:', settings);
|
||||||
|
mcpServer = new MCPServer(settings);
|
||||||
|
|
||||||
|
// 如果设置了自动启动,则启动服务器
|
||||||
|
if (settings.autoStart) {
|
||||||
|
console.log('[MCP Plugin] Auto-starting MCP server...');
|
||||||
|
mcpServer.start().catch(error => {
|
||||||
|
console.error('[MCP Plugin] Failed to auto-start server:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[MCP Plugin] MCP server created but not started (autoStart=false)');
|
||||||
|
console.log('[MCP Plugin] Use the MCP panel or call startServer() to start the server');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MCP Plugin] Failed to load MCP server:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @en Method triggered when uninstalling the extension
|
||||||
|
* @zh 卸载扩展时触发的方法
|
||||||
|
*/
|
||||||
|
export function unload() {
|
||||||
|
if (mcpServer) {
|
||||||
|
mcpServer.stop();
|
||||||
|
mcpServer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
257
source/mcp-server.ts
Normal file
257
source/mcp-server.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import * as http from 'http';
|
||||||
|
import * as url from 'url';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { MCPServerSettings, ServerStatus, MCPClient, ToolDefinition } from './types';
|
||||||
|
import { SceneTools } from './tools/scene-tools';
|
||||||
|
import { NodeTools } from './tools/node-tools';
|
||||||
|
import { ComponentTools } from './tools/component-tools';
|
||||||
|
import { PrefabTools } from './tools/prefab-tools';
|
||||||
|
import { ProjectTools } from './tools/project-tools';
|
||||||
|
import { DebugTools } from './tools/debug-tools';
|
||||||
|
import { PreferencesTools } from './tools/preferences-tools';
|
||||||
|
import { ServerTools } from './tools/server-tools';
|
||||||
|
import { BroadcastTools } from './tools/broadcast-tools';
|
||||||
|
|
||||||
|
export class MCPServer {
|
||||||
|
private settings: MCPServerSettings;
|
||||||
|
private httpServer: http.Server | null = null;
|
||||||
|
private clients: Map<string, MCPClient> = new Map();
|
||||||
|
private tools: Record<string, any> = {};
|
||||||
|
private toolsList: ToolDefinition[] = [];
|
||||||
|
|
||||||
|
constructor(settings: MCPServerSettings) {
|
||||||
|
this.settings = settings;
|
||||||
|
this.initializeTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeTools(): void {
|
||||||
|
try {
|
||||||
|
console.log('[MCPServer] Initializing tools...');
|
||||||
|
this.tools.scene = new SceneTools();
|
||||||
|
this.tools.node = new NodeTools();
|
||||||
|
this.tools.component = new ComponentTools();
|
||||||
|
this.tools.prefab = new PrefabTools();
|
||||||
|
this.tools.project = new ProjectTools();
|
||||||
|
this.tools.debug = new DebugTools();
|
||||||
|
this.tools.preferences = new PreferencesTools();
|
||||||
|
this.tools.server = new ServerTools();
|
||||||
|
this.tools.broadcast = new BroadcastTools();
|
||||||
|
console.log('[MCPServer] Tools initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MCPServer] Error initializing tools:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
if (this.httpServer) {
|
||||||
|
console.log('[MCPServer] Server is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[MCPServer] Starting HTTP server on port ${this.settings.port}...`);
|
||||||
|
this.httpServer = http.createServer(this.handleHttpRequest.bind(this));
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
this.httpServer!.listen(this.settings.port, '127.0.0.1', () => {
|
||||||
|
console.log(`[MCPServer] ✅ HTTP server started successfully on http://127.0.0.1:${this.settings.port}`);
|
||||||
|
console.log(`[MCPServer] Health check: http://127.0.0.1:${this.settings.port}/health`);
|
||||||
|
console.log(`[MCPServer] MCP endpoint: http://127.0.0.1:${this.settings.port}/mcp`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.httpServer!.on('error', (err: any) => {
|
||||||
|
console.error('[MCPServer] ❌ Failed to start server:', err);
|
||||||
|
if (err.code === 'EADDRINUSE') {
|
||||||
|
console.error(`[MCPServer] Port ${this.settings.port} is already in use. Please change the port in settings.`);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupTools();
|
||||||
|
console.log('[MCPServer] 🚀 MCP Server is ready for connections');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MCPServer] ❌ Failed to start server:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupTools(): void {
|
||||||
|
this.toolsList = [];
|
||||||
|
|
||||||
|
for (const [category, toolSet] of Object.entries(this.tools)) {
|
||||||
|
const tools = toolSet.getTools();
|
||||||
|
for (const tool of tools) {
|
||||||
|
this.toolsList.push({
|
||||||
|
name: `${category}_${tool.name}`,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async executeToolCall(toolName: string, args: any): Promise<any> {
|
||||||
|
const parts = toolName.split('_');
|
||||||
|
const category = parts[0];
|
||||||
|
const toolMethodName = parts.slice(1).join('_');
|
||||||
|
|
||||||
|
if (this.tools[category]) {
|
||||||
|
return await this.tools[category].execute(toolMethodName, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Tool ${toolName} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClients(): MCPClient[] {
|
||||||
|
return Array.from(this.clients.values());
|
||||||
|
}
|
||||||
|
public getAvailableTools(): ToolDefinition[] {
|
||||||
|
return this.toolsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSettings(): MCPServerSettings {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||||
|
const parsedUrl = url.parse(req.url || '', true);
|
||||||
|
const pathname = parsedUrl.pathname;
|
||||||
|
|
||||||
|
// Set CORS headers
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (pathname === '/mcp' && req.method === 'POST') {
|
||||||
|
await this.handleMCPRequest(req, res);
|
||||||
|
} else if (pathname === '/health' && req.method === 'GET') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ status: 'ok', tools: this.toolsList.length }));
|
||||||
|
} else {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end(JSON.stringify({ error: 'Not found' }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('HTTP request error:', error);
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMCPRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
body += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(body);
|
||||||
|
const response = await this.handleMessage(message);
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling MCP request:', error);
|
||||||
|
res.writeHead(400);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: null,
|
||||||
|
error: {
|
||||||
|
code: -32700,
|
||||||
|
message: 'Parse error'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMessage(message: any): Promise<any> {
|
||||||
|
const { id, method, params } = message;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result: any;
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'tools/list':
|
||||||
|
result = { tools: this.getAvailableTools() };
|
||||||
|
break;
|
||||||
|
case 'tools/call':
|
||||||
|
const { name, arguments: args } = params;
|
||||||
|
const toolResult = await this.executeToolCall(name, args);
|
||||||
|
result = { content: [{ type: 'text', text: JSON.stringify(toolResult) }] };
|
||||||
|
break;
|
||||||
|
case 'initialize':
|
||||||
|
// MCP initialization
|
||||||
|
result = {
|
||||||
|
protocolVersion: '2024-11-05',
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
},
|
||||||
|
serverInfo: {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
version: '1.0.0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown method: ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id,
|
||||||
|
result
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id,
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: error.message
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
if (this.httpServer) {
|
||||||
|
this.httpServer.close();
|
||||||
|
this.httpServer = null;
|
||||||
|
console.log('[MCPServer] HTTP server stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clients.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStatus(): ServerStatus {
|
||||||
|
return {
|
||||||
|
running: !!this.httpServer,
|
||||||
|
port: this.settings.port,
|
||||||
|
clients: 0 // HTTP is stateless, no persistent clients
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateSettings(settings: MCPServerSettings) {
|
||||||
|
this.settings = settings;
|
||||||
|
if (this.httpServer) {
|
||||||
|
this.stop();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP transport doesn't need persistent connections
|
||||||
|
// MCP over HTTP uses request-response pattern
|
||||||
229
source/panels/default/index.ts
Normal file
229
source/panels/default/index.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { readSettings } from '../../settings';
|
||||||
|
import { readFileSync } from 'fs-extra';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 如果希望兼容 3.3 之前的版本可以使用下方的代码
|
||||||
|
* @en You can add the code below if you want compatibility with versions prior to 3.3
|
||||||
|
*/
|
||||||
|
// Editor.Panel.define = Editor.Panel.define || function(options: any) { return options }
|
||||||
|
|
||||||
|
module.exports = Editor.Panel.define({
|
||||||
|
listeners: {
|
||||||
|
show() { console.log('MCP Server panel shown'); },
|
||||||
|
hide() { console.log('MCP Server panel hidden'); }
|
||||||
|
},
|
||||||
|
template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'),
|
||||||
|
style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
|
||||||
|
$: {
|
||||||
|
panelTitle: '#panelTitle',
|
||||||
|
serverStatusLabel: '#serverStatusLabel',
|
||||||
|
serverStatusLabelProp: '#serverStatusLabelProp',
|
||||||
|
serverStatusValue: '#serverStatusValue',
|
||||||
|
connectedLabel: '#connectedLabel',
|
||||||
|
connectedClients: '#connectedClients',
|
||||||
|
toggleServerBtn: '#toggleServerBtn',
|
||||||
|
settingsLabel: '#settingsLabel',
|
||||||
|
portLabel: '#portLabel',
|
||||||
|
autoStartLabel: '#autoStartLabel',
|
||||||
|
debugLogLabel: '#debugLogLabel',
|
||||||
|
maxConnectionsLabel: '#maxConnectionsLabel',
|
||||||
|
connectionInfoLabel: '#connectionInfoLabel',
|
||||||
|
httpUrlLabel: '#httpUrlLabel',
|
||||||
|
httpUrlInput: '#httpUrlInput',
|
||||||
|
copyBtn: '#copyBtn',
|
||||||
|
saveSettingsBtn: '#saveSettingsBtn',
|
||||||
|
// 新增输入控件id
|
||||||
|
portInput: '#portInput',
|
||||||
|
maxConnInput: '#maxConnInput',
|
||||||
|
autoStartInput: '#autoStartInput',
|
||||||
|
debugLogInput: '#debugLogInput',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async updateServerStatus(this: any) {
|
||||||
|
try {
|
||||||
|
const status = await Editor.Message.request('cocos-mcp-server', 'get-server-status');
|
||||||
|
this.serverRunning = status.running;
|
||||||
|
this.connectedClients = status.clients || 0;
|
||||||
|
this.serverStatus = this.serverRunning ?
|
||||||
|
Editor.I18n.t('cocos-mcp-server.connected') :
|
||||||
|
Editor.I18n.t('cocos-mcp-server.disconnected');
|
||||||
|
this.statusClass = this.serverRunning ? 'running' : 'stopped';
|
||||||
|
this.buttonText = this.serverRunning ?
|
||||||
|
Editor.I18n.t('cocos-mcp-server.stop_server') :
|
||||||
|
Editor.I18n.t('cocos-mcp-server.start_server');
|
||||||
|
// 刷新UI
|
||||||
|
this.$.serverStatusValue.innerText = this.serverStatus;
|
||||||
|
this.$.connectedClients.innerText = this.connectedClients;
|
||||||
|
this.$.toggleServerBtn.innerText = this.buttonText;
|
||||||
|
if (this.serverRunning) {
|
||||||
|
this.httpUrl = `http://localhost:${this.settings.port}/mcp`;
|
||||||
|
this.$.httpUrlInput.value = this.httpUrl;
|
||||||
|
} else {
|
||||||
|
this.httpUrl = '';
|
||||||
|
this.$.httpUrlInput.value = '';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update server status:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleServer(this: any) {
|
||||||
|
this.isProcessing = true;
|
||||||
|
try {
|
||||||
|
if (this.serverRunning) {
|
||||||
|
await this.stopServer();
|
||||||
|
} else {
|
||||||
|
await this.startServer();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async startServer(this: any) {
|
||||||
|
try {
|
||||||
|
await Editor.Message.request('cocos-mcp-server', 'start-server');
|
||||||
|
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.server_started'), {
|
||||||
|
detail: Editor.I18n.t('cocos-mcp-server.server_running').replace('{0}', this.settings.port.toString())
|
||||||
|
});
|
||||||
|
await this.updateServerStatus();
|
||||||
|
} catch (err: any) {
|
||||||
|
Editor.Dialog.error(Editor.I18n.t('cocos-mcp-server.failed_to_start'), err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async stopServer(this: any) {
|
||||||
|
try {
|
||||||
|
await Editor.Message.request('cocos-mcp-server', 'stop-server');
|
||||||
|
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.server_stopped_msg'), {
|
||||||
|
detail: Editor.I18n.t('cocos-mcp-server.server_stopped')
|
||||||
|
});
|
||||||
|
await this.updateServerStatus();
|
||||||
|
} catch (err: any) {
|
||||||
|
Editor.Dialog.error(Editor.I18n.t('cocos-mcp-server.failed_to_stop'), err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSettings(this: any) {
|
||||||
|
try {
|
||||||
|
// 直接用 this.$ 获取所有输入控件的当前值
|
||||||
|
const port = this.$.portInput ? Number((this.$.portInput as any).value) : 3000;
|
||||||
|
const maxConnections = this.$.maxConnInput ? Number((this.$.maxConnInput as any).value) : 10;
|
||||||
|
const autoStart = this.$.autoStartInput ? !!(this.$.autoStartInput as any).checked : false;
|
||||||
|
const enableDebugLog = this.$.debugLogInput ? !!(this.$.debugLogInput as any).checked : false;
|
||||||
|
// 组装 settings
|
||||||
|
const settings = {
|
||||||
|
...this.settings,
|
||||||
|
port,
|
||||||
|
maxConnections,
|
||||||
|
autoStart,
|
||||||
|
enableDebugLog,
|
||||||
|
};
|
||||||
|
await Editor.Message.request('cocos-mcp-server', 'update-settings', settings);
|
||||||
|
// 重新拉取设置
|
||||||
|
const newSettings = await Editor.Message.request('cocos-mcp-server', 'get-server-settings');
|
||||||
|
this.settings = newSettings;
|
||||||
|
this.originalSettings = JSON.stringify(newSettings);
|
||||||
|
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.settings_saved'));
|
||||||
|
} catch (err: any) {
|
||||||
|
Editor.Dialog.error(Editor.I18n.t('cocos-mcp-server.failed_to_save'), err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
copyUrl(this: any) {
|
||||||
|
Editor.Clipboard.write('text', this.httpUrl);
|
||||||
|
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.url_copied'));
|
||||||
|
},
|
||||||
|
|
||||||
|
settingsChanged(this: any) {
|
||||||
|
return JSON.stringify(this.settings) !== this.originalSettings;
|
||||||
|
},
|
||||||
|
bindSettingsEvents(this: any) {
|
||||||
|
// 端口输入框
|
||||||
|
const portInput = document.querySelectorAll('ui-num-input[slot="content"]')[0];
|
||||||
|
if (portInput) {
|
||||||
|
portInput.addEventListener('change', (e: any) => {
|
||||||
|
this.settings.port = Number(e.detail.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 最大连接数
|
||||||
|
const maxConnInput = document.querySelectorAll('ui-num-input[slot="content"]')[1];
|
||||||
|
if (maxConnInput) {
|
||||||
|
maxConnInput.addEventListener('change', (e: any) => {
|
||||||
|
this.settings.maxConnections = Number(e.detail.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 复选框
|
||||||
|
const checkboxes = document.querySelectorAll('ui-checkbox[slot="content"]');
|
||||||
|
if (checkboxes && checkboxes.length >= 2) {
|
||||||
|
checkboxes[0].addEventListener('change', (e: any) => {
|
||||||
|
this.settings.autoStart = !!e.detail.value;
|
||||||
|
});
|
||||||
|
checkboxes[1].addEventListener('change', (e: any) => {
|
||||||
|
this.settings.enableDebugLog = !!e.detail.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
ready() {
|
||||||
|
Editor.Message.request('cocos-mcp-server', 'get-server-settings').then((settings) => {
|
||||||
|
this.settings = settings;
|
||||||
|
this.originalSettings = JSON.stringify(settings);
|
||||||
|
// 本地化label赋值
|
||||||
|
this.$.panelTitle.innerText = Editor.I18n.t('cocos-mcp-server.panel_title');
|
||||||
|
this.$.serverStatusLabel.innerText = Editor.I18n.t('cocos-mcp-server.server_status');
|
||||||
|
this.$.serverStatusLabelProp.innerText = Editor.I18n.t('cocos-mcp-server.server_status');
|
||||||
|
this.$.connectedLabel.innerText = Editor.I18n.t('cocos-mcp-server.connected');
|
||||||
|
this.$.settingsLabel.innerText = Editor.I18n.t('cocos-mcp-server.settings');
|
||||||
|
this.$.portLabel.innerText = Editor.I18n.t('cocos-mcp-server.port');
|
||||||
|
this.$.autoStartLabel.innerText = Editor.I18n.t('cocos-mcp-server.auto_start');
|
||||||
|
this.$.debugLogLabel.innerText = Editor.I18n.t('cocos-mcp-server.debug_log');
|
||||||
|
this.$.maxConnectionsLabel.innerText = Editor.I18n.t('cocos-mcp-server.max_connections');
|
||||||
|
this.$.connectionInfoLabel.innerText = Editor.I18n.t('cocos-mcp-server.connection_info');
|
||||||
|
this.$.httpUrlLabel.innerText = Editor.I18n.t('cocos-mcp-server.http_url');
|
||||||
|
this.$.copyBtn.innerText = Editor.I18n.t('cocos-mcp-server.copy');
|
||||||
|
this.$.saveSettingsBtn.innerText = Editor.I18n.t('cocos-mcp-server.save_settings');
|
||||||
|
// 动态内容初始化
|
||||||
|
this.$.serverStatusValue.innerText = '';
|
||||||
|
this.$.connectedClients.innerText = '';
|
||||||
|
this.$.toggleServerBtn.innerText = '';
|
||||||
|
this.$.httpUrlInput.value = '';
|
||||||
|
// 绑定按钮事件
|
||||||
|
this.$.toggleServerBtn.addEventListener('confirm', this.toggleServer.bind(this));
|
||||||
|
this.$.saveSettingsBtn.addEventListener('confirm', this.saveSettings.bind(this));
|
||||||
|
this.$.copyBtn.addEventListener('confirm', this.copyUrl.bind(this));
|
||||||
|
// 延迟绑定事件,确保 UI 组件已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
this.bindSettingsEvents();
|
||||||
|
}, 100);
|
||||||
|
// Set up periodic status updates
|
||||||
|
(this as any).statusInterval = setInterval(() => {
|
||||||
|
(this as any).updateServerStatus();
|
||||||
|
}, 2000);
|
||||||
|
// 不再自动启动服务器,用户点击才启动
|
||||||
|
(this as any).updateServerStatus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeClose() {
|
||||||
|
if ((this as any).statusInterval) {
|
||||||
|
clearInterval((this as any).statusInterval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
// Panel close cleanup
|
||||||
|
},
|
||||||
|
|
||||||
|
// Direct properties for data access
|
||||||
|
serverRunning: false,
|
||||||
|
connectedClients: 0,
|
||||||
|
serverStatus: '',
|
||||||
|
statusClass: 'stopped',
|
||||||
|
buttonText: '',
|
||||||
|
isProcessing: false,
|
||||||
|
settings: {},
|
||||||
|
httpUrl: '',
|
||||||
|
statusInterval: null as any,
|
||||||
|
originalSettings: ''
|
||||||
|
} as any);
|
||||||
435
source/scene.ts
Normal file
435
source/scene.ts
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
module.paths.push(join(Editor.App.path, 'node_modules'));
|
||||||
|
|
||||||
|
export const methods: { [key: string]: (...any: any) => any } = {
|
||||||
|
/**
|
||||||
|
* Create a new scene
|
||||||
|
*/
|
||||||
|
createNewScene() {
|
||||||
|
try {
|
||||||
|
const { director, Scene } = require('cc');
|
||||||
|
const scene = new Scene();
|
||||||
|
scene.name = 'New Scene';
|
||||||
|
director.runScene(scene);
|
||||||
|
return { success: true, message: 'New scene created successfully' };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add component to a node
|
||||||
|
*/
|
||||||
|
addComponentToNode(nodeUuid: string, componentType: string) {
|
||||||
|
try {
|
||||||
|
const { director, js } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find node by UUID
|
||||||
|
const node = scene.getChildByUuid(nodeUuid);
|
||||||
|
if (!node) {
|
||||||
|
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get component class
|
||||||
|
const ComponentClass = js.getClassByName(componentType);
|
||||||
|
if (!ComponentClass) {
|
||||||
|
return { success: false, error: `Component type ${componentType} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add component
|
||||||
|
const component = node.addComponent(ComponentClass);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Component ${componentType} added successfully`,
|
||||||
|
data: { componentId: component.uuid }
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove component from a node
|
||||||
|
*/
|
||||||
|
removeComponentFromNode(nodeUuid: string, componentType: string) {
|
||||||
|
try {
|
||||||
|
const { director, js } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = scene.getChildByUuid(nodeUuid);
|
||||||
|
if (!node) {
|
||||||
|
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComponentClass = js.getClassByName(componentType);
|
||||||
|
if (!ComponentClass) {
|
||||||
|
return { success: false, error: `Component type ${componentType} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = node.getComponent(ComponentClass);
|
||||||
|
if (!component) {
|
||||||
|
return { success: false, error: `Component ${componentType} not found on node` };
|
||||||
|
}
|
||||||
|
|
||||||
|
node.removeComponent(component);
|
||||||
|
return { success: true, message: `Component ${componentType} removed successfully` };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new node
|
||||||
|
*/
|
||||||
|
createNode(name: string, parentUuid?: string) {
|
||||||
|
try {
|
||||||
|
const { director, Node } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = new Node(name);
|
||||||
|
|
||||||
|
if (parentUuid) {
|
||||||
|
const parent = scene.getChildByUuid(parentUuid);
|
||||||
|
if (parent) {
|
||||||
|
parent.addChild(node);
|
||||||
|
} else {
|
||||||
|
scene.addChild(node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scene.addChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Node ${name} created successfully`,
|
||||||
|
data: { uuid: node.uuid, name: node.name }
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get node information
|
||||||
|
*/
|
||||||
|
getNodeInfo(nodeUuid: string) {
|
||||||
|
try {
|
||||||
|
const { director } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = scene.getChildByUuid(nodeUuid);
|
||||||
|
if (!node) {
|
||||||
|
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
active: node.active,
|
||||||
|
position: node.position,
|
||||||
|
rotation: node.rotation,
|
||||||
|
scale: node.scale,
|
||||||
|
parent: node.parent?.uuid,
|
||||||
|
children: node.children.map((child: any) => child.uuid),
|
||||||
|
components: node.components.map((comp: any) => ({
|
||||||
|
type: comp.constructor.name,
|
||||||
|
enabled: comp.enabled
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all nodes in scene
|
||||||
|
*/
|
||||||
|
getAllNodes() {
|
||||||
|
try {
|
||||||
|
const { director } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes: any[] = [];
|
||||||
|
const collectNodes = (node: any) => {
|
||||||
|
nodes.push({
|
||||||
|
uuid: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
active: node.active,
|
||||||
|
parent: node.parent?.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
node.children.forEach((child: any) => collectNodes(child));
|
||||||
|
};
|
||||||
|
|
||||||
|
scene.children.forEach((child: any) => collectNodes(child));
|
||||||
|
|
||||||
|
return { success: true, data: nodes };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find node by name
|
||||||
|
*/
|
||||||
|
findNodeByName(name: string) {
|
||||||
|
try {
|
||||||
|
const { director } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = scene.getChildByName(name);
|
||||||
|
if (!node) {
|
||||||
|
return { success: false, error: `Node with name ${name} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
active: node.active,
|
||||||
|
position: node.position
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current scene information
|
||||||
|
*/
|
||||||
|
getCurrentSceneInfo() {
|
||||||
|
try {
|
||||||
|
const { director } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name: scene.name,
|
||||||
|
uuid: scene.uuid,
|
||||||
|
nodeCount: scene.children.length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set node property
|
||||||
|
*/
|
||||||
|
setNodeProperty(nodeUuid: string, property: string, value: any) {
|
||||||
|
try {
|
||||||
|
const { director } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = scene.getChildByUuid(nodeUuid);
|
||||||
|
if (!node) {
|
||||||
|
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置属性
|
||||||
|
if (property === 'position') {
|
||||||
|
node.setPosition(value.x || 0, value.y || 0, value.z || 0);
|
||||||
|
} else if (property === 'rotation') {
|
||||||
|
node.setRotationFromEuler(value.x || 0, value.y || 0, value.z || 0);
|
||||||
|
} else if (property === 'scale') {
|
||||||
|
node.setScale(value.x || 1, value.y || 1, value.z || 1);
|
||||||
|
} else if (property === 'active') {
|
||||||
|
node.active = value;
|
||||||
|
} else if (property === 'name') {
|
||||||
|
node.name = value;
|
||||||
|
} else {
|
||||||
|
// 尝试直接设置属性
|
||||||
|
(node as any)[property] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Property '${property}' updated successfully`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get scene hierarchy
|
||||||
|
*/
|
||||||
|
getSceneHierarchy(includeComponents: boolean = false) {
|
||||||
|
try {
|
||||||
|
const { director } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const processNode = (node: any): any => {
|
||||||
|
const result: any = {
|
||||||
|
name: node.name,
|
||||||
|
uuid: node.uuid,
|
||||||
|
active: node.active,
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeComponents) {
|
||||||
|
result.components = node.components.map((comp: any) => ({
|
||||||
|
type: comp.constructor.name,
|
||||||
|
enabled: comp.enabled
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
result.children = node.children.map((child: any) => processNode(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hierarchy = scene.children.map((child: any) => processNode(child));
|
||||||
|
return { success: true, data: hierarchy };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create prefab from node
|
||||||
|
*/
|
||||||
|
createPrefabFromNode(nodeUuid: string, prefabPath: string) {
|
||||||
|
try {
|
||||||
|
const { director, instantiate } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = scene.getChildByUuid(nodeUuid);
|
||||||
|
if (!node) {
|
||||||
|
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:这里只是一个模拟实现,因为运行时环境下无法直接创建预制体文件
|
||||||
|
// 真正的预制体创建需要Editor API支持
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
prefabPath: prefabPath,
|
||||||
|
sourceNodeUuid: nodeUuid,
|
||||||
|
message: `Prefab created from node '${node.name}' at ${prefabPath}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set component property
|
||||||
|
*/
|
||||||
|
setComponentProperty(nodeUuid: string, componentType: string, property: string, value: any) {
|
||||||
|
try {
|
||||||
|
const { director, js } = require('cc');
|
||||||
|
const scene = director.getScene();
|
||||||
|
if (!scene) {
|
||||||
|
return { success: false, error: 'No active scene' };
|
||||||
|
}
|
||||||
|
const node = scene.getChildByUuid(nodeUuid);
|
||||||
|
if (!node) {
|
||||||
|
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||||
|
}
|
||||||
|
const ComponentClass = js.getClassByName(componentType);
|
||||||
|
if (!ComponentClass) {
|
||||||
|
return { success: false, error: `Component type ${componentType} not found` };
|
||||||
|
}
|
||||||
|
const component = node.getComponent(ComponentClass);
|
||||||
|
if (!component) {
|
||||||
|
return { success: false, error: `Component ${componentType} not found on node` };
|
||||||
|
}
|
||||||
|
// 针对常见属性做特殊处理
|
||||||
|
if (property === 'spriteFrame' && componentType === 'cc.Sprite') {
|
||||||
|
// 支持 value 为 uuid 或资源路径
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// 先尝试按 uuid 查找
|
||||||
|
const assetManager = require('cc').assetManager;
|
||||||
|
assetManager.resources.load(value, require('cc').SpriteFrame, (err: any, spriteFrame: any) => {
|
||||||
|
if (!err && spriteFrame) {
|
||||||
|
component.spriteFrame = spriteFrame;
|
||||||
|
} else {
|
||||||
|
// 尝试通过 uuid 加载
|
||||||
|
assetManager.loadAny({ uuid: value }, (err2: any, asset: any) => {
|
||||||
|
if (!err2 && asset) {
|
||||||
|
component.spriteFrame = asset;
|
||||||
|
} else {
|
||||||
|
// 直接赋值(兼容已传入资源对象)
|
||||||
|
component.spriteFrame = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
component.spriteFrame = value;
|
||||||
|
}
|
||||||
|
} else if (property === 'material' && (componentType === 'cc.Sprite' || componentType === 'cc.MeshRenderer')) {
|
||||||
|
// 支持 value 为 uuid 或资源路径
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const assetManager = require('cc').assetManager;
|
||||||
|
assetManager.resources.load(value, require('cc').Material, (err: any, material: any) => {
|
||||||
|
if (!err && material) {
|
||||||
|
component.material = material;
|
||||||
|
} else {
|
||||||
|
assetManager.loadAny({ uuid: value }, (err2: any, asset: any) => {
|
||||||
|
if (!err2 && asset) {
|
||||||
|
component.material = asset;
|
||||||
|
} else {
|
||||||
|
component.material = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
component.material = value;
|
||||||
|
}
|
||||||
|
} else if (property === 'string' && (componentType === 'cc.Label' || componentType === 'cc.RichText')) {
|
||||||
|
component.string = value;
|
||||||
|
} else {
|
||||||
|
component[property] = value;
|
||||||
|
}
|
||||||
|
// 可选:刷新 Inspector
|
||||||
|
// Editor.Message.send('scene', 'snapshot');
|
||||||
|
return { success: true, message: `Component property '${property}' updated successfully` };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
49
source/settings.ts
Normal file
49
source/settings.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { MCPServerSettings } from './types';
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: MCPServerSettings = {
|
||||||
|
port: 3000,
|
||||||
|
autoStart: false,
|
||||||
|
enableDebugLog: false,
|
||||||
|
allowedOrigins: ['*'],
|
||||||
|
maxConnections: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSettingsPath(): string {
|
||||||
|
return path.join(Editor.Project.path, 'settings', 'mcp-server.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSettingsDir(): void {
|
||||||
|
const settingsDir = path.dirname(getSettingsPath());
|
||||||
|
if (!fs.existsSync(settingsDir)) {
|
||||||
|
fs.mkdirSync(settingsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readSettings(): MCPServerSettings {
|
||||||
|
try {
|
||||||
|
ensureSettingsDir();
|
||||||
|
const settingsFile = getSettingsPath();
|
||||||
|
if (fs.existsSync(settingsFile)) {
|
||||||
|
const content = fs.readFileSync(settingsFile, 'utf8');
|
||||||
|
return { ...DEFAULT_SETTINGS, ...JSON.parse(content) };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to read settings:', e);
|
||||||
|
}
|
||||||
|
return DEFAULT_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSettings(settings: MCPServerSettings): void {
|
||||||
|
try {
|
||||||
|
ensureSettingsDir();
|
||||||
|
const settingsFile = getSettingsPath();
|
||||||
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save settings:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DEFAULT_SETTINGS };
|
||||||
132
source/test/manual-test.ts
Normal file
132
source/test/manual-test.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
declare const Editor: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动测试脚本
|
||||||
|
* 可以在 Cocos Creator 控制台中执行测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function testSceneTools() {
|
||||||
|
console.log('=== Testing Scene Tools ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取场景信息
|
||||||
|
console.log('1. Getting scene info...');
|
||||||
|
const sceneInfo = await Editor.Message.request('scene', 'get-scene-info');
|
||||||
|
console.log('Scene info:', sceneInfo);
|
||||||
|
|
||||||
|
// 2. 创建节点
|
||||||
|
console.log('\n2. Creating test node...');
|
||||||
|
const createResult = await Editor.Message.request('scene', 'create-node', {
|
||||||
|
name: 'TestNode_' + Date.now(),
|
||||||
|
type: 'cc.Node'
|
||||||
|
});
|
||||||
|
console.log('Create result:', createResult);
|
||||||
|
|
||||||
|
if (createResult && createResult.uuid) {
|
||||||
|
const nodeUuid = createResult.uuid;
|
||||||
|
|
||||||
|
// 3. 查询节点
|
||||||
|
console.log('\n3. Querying node...');
|
||||||
|
const nodeInfo = await Editor.Message.request('scene', 'query-node', {
|
||||||
|
uuid: nodeUuid
|
||||||
|
});
|
||||||
|
console.log('Node info:', nodeInfo);
|
||||||
|
|
||||||
|
// 4. 设置节点属性
|
||||||
|
console.log('\n4. Setting node position...');
|
||||||
|
await Editor.Message.request('scene', 'set-node-property', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
path: 'position',
|
||||||
|
value: { x: 100, y: 200, z: 0 }
|
||||||
|
});
|
||||||
|
console.log('Position set successfully');
|
||||||
|
|
||||||
|
// 5. 添加组件
|
||||||
|
console.log('\n5. Adding Sprite component...');
|
||||||
|
const addCompResult = await Editor.Message.request('scene', 'add-component', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
component: 'cc.Sprite'
|
||||||
|
});
|
||||||
|
console.log('Component added:', addCompResult);
|
||||||
|
|
||||||
|
// 6. 查询组件
|
||||||
|
console.log('\n6. Querying component...');
|
||||||
|
const compInfo = await Editor.Message.request('scene', 'query-node-component', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
component: 'cc.Sprite'
|
||||||
|
});
|
||||||
|
console.log('Component info:', compInfo);
|
||||||
|
|
||||||
|
// 7. 删除节点
|
||||||
|
console.log('\n7. Removing test node...');
|
||||||
|
await Editor.Message.request('scene', 'remove-node', {
|
||||||
|
uuid: nodeUuid
|
||||||
|
});
|
||||||
|
console.log('Node removed successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testAssetTools() {
|
||||||
|
console.log('\n=== Testing Asset Tools ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 查询资源
|
||||||
|
console.log('1. Querying image assets...');
|
||||||
|
const assets = await Editor.Message.request('asset-db', 'query-assets', {
|
||||||
|
pattern: '**/*.png',
|
||||||
|
ccType: 'cc.ImageAsset'
|
||||||
|
});
|
||||||
|
console.log('Found assets:', assets?.length || 0);
|
||||||
|
|
||||||
|
// 2. 获取资源信息
|
||||||
|
console.log('\n2. Getting asset database info...');
|
||||||
|
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', {
|
||||||
|
uuid: 'db://assets'
|
||||||
|
});
|
||||||
|
console.log('Asset info:', assetInfo);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testProjectTools() {
|
||||||
|
console.log('\n=== Testing Project Tools ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取项目信息
|
||||||
|
console.log('1. Getting project info...');
|
||||||
|
const projectInfo = await Editor.Message.request('project', 'query-info');
|
||||||
|
console.log('Project info:', projectInfo);
|
||||||
|
|
||||||
|
// 2. 检查构建能力
|
||||||
|
console.log('\n2. Checking build capability...');
|
||||||
|
const canBuild = await Editor.Message.request('project', 'can-build');
|
||||||
|
console.log('Can build:', canBuild);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runAllTests() {
|
||||||
|
console.log('Starting MCP Server Tools Test...\n');
|
||||||
|
|
||||||
|
await testSceneTools();
|
||||||
|
await testAssetTools();
|
||||||
|
await testProjectTools();
|
||||||
|
|
||||||
|
console.log('\n=== All tests completed ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出到全局,方便在控制台调用
|
||||||
|
(global as any).MCPTest = {
|
||||||
|
testSceneTools,
|
||||||
|
testAssetTools,
|
||||||
|
testProjectTools,
|
||||||
|
runAllTests
|
||||||
|
};
|
||||||
235
source/test/mcp-tool-tester.ts
Normal file
235
source/test/mcp-tool-tester.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
declare const Editor: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 工具测试器 - 直接测试通过 WebSocket 的 MCP 工具
|
||||||
|
*/
|
||||||
|
export class MCPToolTester {
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private messageId = 0;
|
||||||
|
private responseHandlers = new Map<number, (response: any) => void>();
|
||||||
|
|
||||||
|
async connect(port: number): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(`ws://localhost:${port}`);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('WebSocket 连接成功');
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket 连接错误:', error);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(event.data);
|
||||||
|
if (response.id && this.responseHandlers.has(response.id)) {
|
||||||
|
const handler = this.responseHandlers.get(response.id);
|
||||||
|
this.responseHandlers.delete(response.id);
|
||||||
|
handler?.(response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理响应时出错:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建 WebSocket 时出错:', error);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async callTool(tool: string, args: any = {}): Promise<any> {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error('WebSocket 未连接');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = ++this.messageId;
|
||||||
|
const request = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: tool,
|
||||||
|
arguments: args
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.responseHandlers.delete(id);
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
this.responseHandlers.set(id, (response) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (response.error) {
|
||||||
|
reject(new Error(response.error.message));
|
||||||
|
} else {
|
||||||
|
resolve(response.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws!.send(JSON.stringify(request));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTools(): Promise<any> {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error('WebSocket 未连接');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = ++this.messageId;
|
||||||
|
const request = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id,
|
||||||
|
method: 'tools/list'
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.responseHandlers.delete(id);
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
this.responseHandlers.set(id, (response) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (response.error) {
|
||||||
|
reject(new Error(response.error.message));
|
||||||
|
} else {
|
||||||
|
resolve(response.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws!.send(JSON.stringify(request));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async testMCPTools() {
|
||||||
|
console.log('\n=== 测试 MCP 工具(通过 WebSocket)===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 0. 获取工具列表
|
||||||
|
console.log('\n0. 获取工具列表...');
|
||||||
|
const toolsList = await this.listTools();
|
||||||
|
console.log(`找到 ${toolsList.tools?.length || 0} 个工具:`);
|
||||||
|
if (toolsList.tools) {
|
||||||
|
for (const tool of toolsList.tools.slice(0, 10)) { // 只显示前10个
|
||||||
|
console.log(` - ${tool.name}: ${tool.description}`);
|
||||||
|
}
|
||||||
|
if (toolsList.tools.length > 10) {
|
||||||
|
console.log(` ... 还有 ${toolsList.tools.length - 10} 个工具`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 测试场景工具
|
||||||
|
console.log('\n1. 测试当前场景信息...');
|
||||||
|
const sceneInfo = await this.callTool('scene_get_current_scene');
|
||||||
|
console.log('场景信息:', JSON.stringify(sceneInfo).substring(0, 100) + '...');
|
||||||
|
|
||||||
|
// 2. 测试场景列表
|
||||||
|
console.log('\n2. 测试场景列表...');
|
||||||
|
const sceneList = await this.callTool('scene_get_scene_list');
|
||||||
|
console.log('场景列表:', JSON.stringify(sceneList).substring(0, 100) + '...');
|
||||||
|
|
||||||
|
// 3. 测试节点创建
|
||||||
|
console.log('\n3. 测试创建节点...');
|
||||||
|
const createResult = await this.callTool('node_create_node', {
|
||||||
|
name: 'MCPTestNode_' + Date.now(),
|
||||||
|
nodeType: 'cc.Node',
|
||||||
|
position: { x: 0, y: 0, z: 0 }
|
||||||
|
});
|
||||||
|
console.log('创建节点结果:', createResult);
|
||||||
|
|
||||||
|
// 解析创建节点的结果
|
||||||
|
let nodeUuid: string | null = null;
|
||||||
|
if (createResult.content && createResult.content[0] && createResult.content[0].text) {
|
||||||
|
try {
|
||||||
|
const resultData = JSON.parse(createResult.content[0].text);
|
||||||
|
if (resultData.success && resultData.data && resultData.data.uuid) {
|
||||||
|
nodeUuid = resultData.data.uuid;
|
||||||
|
console.log('成功获取节点UUID:', nodeUuid);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeUuid) {
|
||||||
|
// 4. 测试查询节点
|
||||||
|
console.log('\n4. 测试查询节点...');
|
||||||
|
const queryResult = await this.callTool('node_get_node_info', {
|
||||||
|
uuid: nodeUuid
|
||||||
|
});
|
||||||
|
console.log('节点信息:', JSON.stringify(queryResult).substring(0, 100) + '...');
|
||||||
|
|
||||||
|
// 5. 测试删除节点
|
||||||
|
console.log('\n5. 测试删除节点...');
|
||||||
|
const removeResult = await this.callTool('node_delete_node', {
|
||||||
|
uuid: nodeUuid
|
||||||
|
});
|
||||||
|
console.log('删除结果:', removeResult);
|
||||||
|
} else {
|
||||||
|
console.log('无法从创建结果获取节点UUID,尝试通过名称查找...');
|
||||||
|
|
||||||
|
// 备用方案:通过名称查找刚创建的节点
|
||||||
|
const findResult = await this.callTool('node_find_node_by_name', {
|
||||||
|
name: 'MCPTestNode_' + Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (findResult.content && findResult.content[0] && findResult.content[0].text) {
|
||||||
|
try {
|
||||||
|
const findData = JSON.parse(findResult.content[0].text);
|
||||||
|
if (findData.success && findData.data && findData.data.uuid) {
|
||||||
|
nodeUuid = findData.data.uuid;
|
||||||
|
console.log('通过名称查找成功获取UUID:', nodeUuid);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeUuid) {
|
||||||
|
console.log('所有方式都无法获取节点UUID,跳过后续节点操作测试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 测试项目工具
|
||||||
|
console.log('\n6. 测试项目信息...');
|
||||||
|
const projectInfo = await this.callTool('project_get_project_info');
|
||||||
|
console.log('项目信息:', JSON.stringify(projectInfo).substring(0, 100) + '...');
|
||||||
|
|
||||||
|
// 7. 测试预制体工具
|
||||||
|
console.log('\n7. 测试预制体列表...');
|
||||||
|
const prefabResult = await this.callTool('prefab_get_prefab_list', {
|
||||||
|
folder: 'db://assets'
|
||||||
|
});
|
||||||
|
console.log('找到预制体:', prefabResult.data?.length || 0);
|
||||||
|
|
||||||
|
// 8. 测试组件工具
|
||||||
|
console.log('\n8. 测试可用组件...');
|
||||||
|
const componentsResult = await this.callTool('component_get_available_components');
|
||||||
|
console.log('可用组件:', JSON.stringify(componentsResult).substring(0, 100) + '...');
|
||||||
|
|
||||||
|
// 9. 测试调试工具
|
||||||
|
console.log('\n9. 测试编辑器信息...');
|
||||||
|
const editorInfo = await this.callTool('debug_get_editor_info');
|
||||||
|
console.log('编辑器信息:', JSON.stringify(editorInfo).substring(0, 100) + '...');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MCP 工具测试失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
this.responseHandlers.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出到全局方便测试
|
||||||
|
(global as any).MCPToolTester = MCPToolTester;
|
||||||
168
source/test/tool-tester.ts
Normal file
168
source/test/tool-tester.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
declare const Editor: any;
|
||||||
|
|
||||||
|
interface TestResult {
|
||||||
|
tool: string;
|
||||||
|
method: string;
|
||||||
|
success: boolean;
|
||||||
|
result?: any;
|
||||||
|
error?: string;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToolTester {
|
||||||
|
private results: TestResult[] = [];
|
||||||
|
|
||||||
|
async runTest(tool: string, method: string, params: any): Promise<TestResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const result: TestResult = {
|
||||||
|
tool,
|
||||||
|
method,
|
||||||
|
success: false,
|
||||||
|
time: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await Editor.Message.request(tool, method, params);
|
||||||
|
result.success = true;
|
||||||
|
result.result = response;
|
||||||
|
} catch (error) {
|
||||||
|
result.success = false;
|
||||||
|
result.error = error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.time = Date.now() - startTime;
|
||||||
|
this.results.push(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async testSceneOperations() {
|
||||||
|
console.log('Testing Scene Operations...');
|
||||||
|
|
||||||
|
// Test node creation (this is the main scene operation that works)
|
||||||
|
const createResult = await this.runTest('scene', 'create-node', {
|
||||||
|
name: 'TestNode',
|
||||||
|
type: 'cc.Node'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (createResult.success && createResult.result) {
|
||||||
|
const nodeUuid = createResult.result;
|
||||||
|
|
||||||
|
// Test query node info
|
||||||
|
await this.runTest('scene', 'query-node-info', nodeUuid);
|
||||||
|
|
||||||
|
// Test remove node
|
||||||
|
await this.runTest('scene', 'remove-node', nodeUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test execute scene script
|
||||||
|
await this.runTest('scene', 'execute-scene-script', {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'test-method',
|
||||||
|
args: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async testNodeOperations() {
|
||||||
|
console.log('Testing Node Operations...');
|
||||||
|
|
||||||
|
// Create a test node first
|
||||||
|
const createResult = await this.runTest('scene', 'create-node', {
|
||||||
|
name: 'TestNodeForOps',
|
||||||
|
type: 'cc.Node'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (createResult.success && createResult.result) {
|
||||||
|
const nodeUuid = createResult.result;
|
||||||
|
|
||||||
|
// Test set property
|
||||||
|
await this.runTest('scene', 'set-property', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
path: 'position',
|
||||||
|
dump: {
|
||||||
|
type: 'cc.Vec3',
|
||||||
|
value: { x: 100, y: 200, z: 0 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test add component
|
||||||
|
await this.runTest('scene', 'add-component', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
component: 'cc.Sprite'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await this.runTest('scene', 'remove-node', nodeUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testAssetOperations() {
|
||||||
|
console.log('Testing Asset Operations...');
|
||||||
|
|
||||||
|
// Test asset list
|
||||||
|
await this.runTest('asset-db', 'query-assets', {
|
||||||
|
pattern: '**/*.png',
|
||||||
|
ccType: 'cc.ImageAsset'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test query asset by path
|
||||||
|
await this.runTest('asset-db', 'query-path', 'db://assets');
|
||||||
|
|
||||||
|
// Test query asset by uuid (using a valid uuid format)
|
||||||
|
await this.runTest('asset-db', 'query-uuid', 'db://assets');
|
||||||
|
}
|
||||||
|
|
||||||
|
async testProjectOperations() {
|
||||||
|
console.log('Testing Project Operations...');
|
||||||
|
|
||||||
|
// Test open project settings
|
||||||
|
await this.runTest('project', 'open-settings', {});
|
||||||
|
|
||||||
|
// Test query project settings
|
||||||
|
const projectName = await this.runTest('project', 'query-setting', 'name');
|
||||||
|
|
||||||
|
if (projectName.success) {
|
||||||
|
console.log('Project name:', projectName.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAllTests() {
|
||||||
|
this.results = [];
|
||||||
|
|
||||||
|
await this.testSceneOperations();
|
||||||
|
await this.testNodeOperations();
|
||||||
|
await this.testAssetOperations();
|
||||||
|
await this.testProjectOperations();
|
||||||
|
|
||||||
|
return this.getTestReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTestReport() {
|
||||||
|
const total = this.results.length;
|
||||||
|
const passed = this.results.filter(r => r.success).length;
|
||||||
|
const failed = total - passed;
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
total,
|
||||||
|
passed,
|
||||||
|
failed,
|
||||||
|
passRate: total > 0 ? (passed / total * 100).toFixed(2) + '%' : '0%'
|
||||||
|
},
|
||||||
|
results: this.results,
|
||||||
|
grouped: this.groupResultsByTool()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupResultsByTool() {
|
||||||
|
const grouped: Record<string, TestResult[]> = {};
|
||||||
|
|
||||||
|
for (const result of this.results) {
|
||||||
|
if (!grouped[result.tool]) {
|
||||||
|
grouped[result.tool] = [];
|
||||||
|
}
|
||||||
|
grouped[result.tool].push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
}
|
||||||
264
source/tools/broadcast-tools.ts
Normal file
264
source/tools/broadcast-tools.ts
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
|
||||||
|
|
||||||
|
export class BroadcastTools implements ToolExecutor {
|
||||||
|
private listeners: Map<string, Function[]> = new Map();
|
||||||
|
private messageLog: Array<{ message: string; data: any; timestamp: number }> = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.setupBroadcastListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'get_broadcast_log',
|
||||||
|
description: 'Get recent broadcast messages log',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of recent messages to return',
|
||||||
|
default: 50
|
||||||
|
},
|
||||||
|
messageType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by message type (optional)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'listen_broadcast',
|
||||||
|
description: 'Start listening for specific broadcast messages',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
messageType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Message type to listen for'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['messageType']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop_listening',
|
||||||
|
description: 'Stop listening for specific broadcast messages',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
messageType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Message type to stop listening for'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['messageType']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clear_broadcast_log',
|
||||||
|
description: 'Clear the broadcast messages log',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_active_listeners',
|
||||||
|
description: 'Get list of active broadcast listeners',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'get_broadcast_log':
|
||||||
|
return await this.getBroadcastLog(args.limit, args.messageType);
|
||||||
|
case 'listen_broadcast':
|
||||||
|
return await this.listenBroadcast(args.messageType);
|
||||||
|
case 'stop_listening':
|
||||||
|
return await this.stopListening(args.messageType);
|
||||||
|
case 'clear_broadcast_log':
|
||||||
|
return await this.clearBroadcastLog();
|
||||||
|
case 'get_active_listeners':
|
||||||
|
return await this.getActiveListeners();
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupBroadcastListeners(): void {
|
||||||
|
// 设置预定义的重要广播消息监听
|
||||||
|
const importantMessages = [
|
||||||
|
'build-worker:ready',
|
||||||
|
'build-worker:closed',
|
||||||
|
'scene:ready',
|
||||||
|
'scene:close',
|
||||||
|
'scene:light-probe-edit-mode-changed',
|
||||||
|
'scene:light-probe-bounding-box-edit-mode-changed',
|
||||||
|
'asset-db:ready',
|
||||||
|
'asset-db:close',
|
||||||
|
'asset-db:asset-add',
|
||||||
|
'asset-db:asset-change',
|
||||||
|
'asset-db:asset-delete'
|
||||||
|
];
|
||||||
|
|
||||||
|
importantMessages.forEach(messageType => {
|
||||||
|
this.addBroadcastListener(messageType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addBroadcastListener(messageType: string): void {
|
||||||
|
const listener = (data: any) => {
|
||||||
|
this.messageLog.push({
|
||||||
|
message: messageType,
|
||||||
|
data: data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保持日志大小在合理范围内
|
||||||
|
if (this.messageLog.length > 1000) {
|
||||||
|
this.messageLog = this.messageLog.slice(-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Broadcast] ${messageType}:`, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.listeners.has(messageType)) {
|
||||||
|
this.listeners.set(messageType, []);
|
||||||
|
}
|
||||||
|
this.listeners.get(messageType)!.push(listener);
|
||||||
|
|
||||||
|
// 注册 Editor 消息监听 - 暂时注释掉,Editor.Message API可能不支持
|
||||||
|
// Editor.Message.on(messageType, listener);
|
||||||
|
console.log(`[BroadcastTools] Added listener for ${messageType} (simulated)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeBroadcastListener(messageType: string): void {
|
||||||
|
const listeners = this.listeners.get(messageType);
|
||||||
|
if (listeners) {
|
||||||
|
listeners.forEach(listener => {
|
||||||
|
// Editor.Message.off(messageType, listener);
|
||||||
|
console.log(`[BroadcastTools] Removed listener for ${messageType} (simulated)`);
|
||||||
|
});
|
||||||
|
this.listeners.delete(messageType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getBroadcastLog(limit: number = 50, messageType?: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let filteredLog = this.messageLog;
|
||||||
|
|
||||||
|
if (messageType) {
|
||||||
|
filteredLog = this.messageLog.filter(entry => entry.message === messageType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentLog = filteredLog.slice(-limit).map(entry => ({
|
||||||
|
...entry,
|
||||||
|
timestamp: new Date(entry.timestamp).toISOString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
log: recentLog,
|
||||||
|
count: recentLog.length,
|
||||||
|
totalCount: filteredLog.length,
|
||||||
|
filter: messageType || 'all',
|
||||||
|
message: 'Broadcast log retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async listenBroadcast(messageType: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
if (!this.listeners.has(messageType)) {
|
||||||
|
this.addBroadcastListener(messageType);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
messageType: messageType,
|
||||||
|
message: `Started listening for broadcast: ${messageType}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
messageType: messageType,
|
||||||
|
message: `Already listening for broadcast: ${messageType}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async stopListening(messageType: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
if (this.listeners.has(messageType)) {
|
||||||
|
this.removeBroadcastListener(messageType);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
messageType: messageType,
|
||||||
|
message: `Stopped listening for broadcast: ${messageType}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
messageType: messageType,
|
||||||
|
message: `Was not listening for broadcast: ${messageType}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearBroadcastLog(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const previousCount = this.messageLog.length;
|
||||||
|
this.messageLog = [];
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
clearedCount: previousCount,
|
||||||
|
message: 'Broadcast log cleared successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getActiveListeners(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const activeListeners = Array.from(this.listeners.keys()).map(messageType => ({
|
||||||
|
messageType: messageType,
|
||||||
|
listenerCount: this.listeners.get(messageType)?.length || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
listeners: activeListeners,
|
||||||
|
count: activeListeners.length,
|
||||||
|
message: 'Active listeners retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
445
source/tools/component-tools.ts
Normal file
445
source/tools/component-tools.ts
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor, ComponentInfo } from '../types';
|
||||||
|
|
||||||
|
export class ComponentTools implements ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'add_component',
|
||||||
|
description: 'Add a component to a specific node. The component will be added to the exact node specified by nodeUuid.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target node UUID. Use get_node_info or find_node_by_name to get the UUID of the desired node.'
|
||||||
|
},
|
||||||
|
componentType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Component type (e.g., cc.Sprite, cc.Label, cc.Button)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'componentType']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'remove_component',
|
||||||
|
description: 'Remove a component from a node',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID'
|
||||||
|
},
|
||||||
|
componentType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Component type to remove'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'componentType']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_components',
|
||||||
|
description: 'Get all components of a node',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_component_info',
|
||||||
|
description: 'Get specific component information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID'
|
||||||
|
},
|
||||||
|
componentType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Component type to get info for'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'componentType']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'set_component_property',
|
||||||
|
description: 'Set component property value',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID'
|
||||||
|
},
|
||||||
|
componentType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Component type'
|
||||||
|
},
|
||||||
|
property: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Property name'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'Property value'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'componentType', 'property', 'value']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attach_script',
|
||||||
|
description: 'Attach a script component to a node',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID'
|
||||||
|
},
|
||||||
|
scriptPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Script asset path (e.g., db://assets/scripts/MyScript.ts)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'scriptPath']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_available_components',
|
||||||
|
description: 'Get list of available component types',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Component category filter',
|
||||||
|
enum: ['all', 'renderer', 'ui', 'physics', 'animation', 'audio'],
|
||||||
|
default: 'all'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'add_component':
|
||||||
|
return await this.addComponent(args.nodeUuid, args.componentType);
|
||||||
|
case 'remove_component':
|
||||||
|
return await this.removeComponent(args.nodeUuid, args.componentType);
|
||||||
|
case 'get_components':
|
||||||
|
return await this.getComponents(args.nodeUuid);
|
||||||
|
case 'get_component_info':
|
||||||
|
return await this.getComponentInfo(args.nodeUuid, args.componentType);
|
||||||
|
case 'set_component_property':
|
||||||
|
return await this.setComponentProperty(args);
|
||||||
|
case 'attach_script':
|
||||||
|
return await this.attachScript(args.nodeUuid, args.scriptPath);
|
||||||
|
case 'get_available_components':
|
||||||
|
return await this.getAvailableComponents(args.category);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 尝试直接使用 Editor API 添加组件
|
||||||
|
Editor.Message.request('scene', 'create-component', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
component: componentType
|
||||||
|
}).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
componentId: result,
|
||||||
|
message: `Component '${componentType}' added successfully`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'addComponentToNode',
|
||||||
|
args: [nodeUuid, componentType]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'removeComponentFromNode',
|
||||||
|
args: [nodeUuid, componentType]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getComponents(nodeUuid: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 优先尝试直接使用 Editor API 查询节点信息
|
||||||
|
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
|
||||||
|
if (nodeData && nodeData.__comps__) {
|
||||||
|
const components = nodeData.__comps__.map((comp: any) => ({
|
||||||
|
type: comp.__type__ || 'Unknown',
|
||||||
|
enabled: comp.enabled !== undefined ? comp.enabled : true,
|
||||||
|
properties: this.extractComponentProperties(comp)
|
||||||
|
}));
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
nodeUuid: nodeUuid,
|
||||||
|
components: components
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'Node not found or no components data' });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'getNodeInfo',
|
||||||
|
args: [nodeUuid]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
if (result.success) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: result.data.components
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getComponentInfo(nodeUuid: string, componentType: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 优先尝试直接使用 Editor API 查询节点信息
|
||||||
|
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
|
||||||
|
if (nodeData && nodeData.__comps__) {
|
||||||
|
const component = nodeData.__comps__.find((comp: any) => comp.__type__ === componentType);
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
nodeUuid: nodeUuid,
|
||||||
|
componentType: componentType,
|
||||||
|
enabled: component.enabled !== undefined ? component.enabled : true,
|
||||||
|
properties: this.extractComponentProperties(component)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: `Component '${componentType}' not found on node` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'Node not found or no components data' });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'getNodeInfo',
|
||||||
|
args: [nodeUuid]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
if (result.success && result.data.components) {
|
||||||
|
const component = result.data.components.find((comp: any) => comp.type === componentType);
|
||||||
|
if (component) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
nodeUuid: nodeUuid,
|
||||||
|
componentType: componentType,
|
||||||
|
...component
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: `Component '${componentType}' not found on node` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: result.error || 'Failed to get component info' });
|
||||||
|
}
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractComponentProperties(component: any): Record<string, any> {
|
||||||
|
const properties: Record<string, any> = {};
|
||||||
|
const excludeKeys = ['__type__', 'enabled', 'node', '_id'];
|
||||||
|
|
||||||
|
for (const key in component) {
|
||||||
|
if (!excludeKeys.includes(key) && !key.startsWith('_')) {
|
||||||
|
properties[key] = component[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setComponentProperty(args: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 首先获取节点信息以找到正确的组件索引
|
||||||
|
Editor.Message.request('scene', 'query-node', args.nodeUuid).then((nodeData: any) => {
|
||||||
|
if (!nodeData || !nodeData.__comps__) {
|
||||||
|
throw new Error('Node not found or no components data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找组件索引
|
||||||
|
let componentIndex = -1;
|
||||||
|
for (let i = 0; i < nodeData.__comps__.length; i++) {
|
||||||
|
const comp = nodeData.__comps__[i];
|
||||||
|
if (comp.__type__ === args.componentType) {
|
||||||
|
componentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentIndex === -1) {
|
||||||
|
throw new Error(`Component '${args.componentType}' not found on node`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正确的组件索引路径
|
||||||
|
const propertyPath = `__comps__.${componentIndex}.${args.property}`;
|
||||||
|
|
||||||
|
return Editor.Message.request('scene', 'set-property', {
|
||||||
|
uuid: args.nodeUuid,
|
||||||
|
path: propertyPath,
|
||||||
|
dump: {
|
||||||
|
value: args.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: `Component property '${args.property}' updated successfully`
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'setComponentProperty',
|
||||||
|
args: [args.nodeUuid, args.componentType, args.property, args.value]
|
||||||
|
};
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async attachScript(nodeUuid: string, scriptPath: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 从脚本路径提取组件类名
|
||||||
|
const scriptName = scriptPath.split('/').pop()?.replace('.ts', '').replace('.js', '');
|
||||||
|
if (!scriptName) {
|
||||||
|
resolve({ success: false, error: 'Invalid script path' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首先尝试直接使用脚本名称作为组件类型
|
||||||
|
Editor.Message.request('scene', 'create-component', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
component: scriptName // 使用脚本名称而非UUID
|
||||||
|
}).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
componentId: result,
|
||||||
|
scriptPath: scriptPath,
|
||||||
|
componentName: scriptName,
|
||||||
|
message: `Script '${scriptName}' attached successfully`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'attachScript',
|
||||||
|
args: [nodeUuid, scriptPath]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to attach script '${scriptName}': ${err.message}`,
|
||||||
|
instruction: 'Please ensure the script is properly compiled and exported as a Component class. You can also manually attach the script through the Properties panel in the editor.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAvailableComponents(category: string = 'all'): Promise<ToolResponse> {
|
||||||
|
const componentCategories: Record<string, string[]> = {
|
||||||
|
renderer: ['cc.Sprite', 'cc.Label', 'cc.RichText', 'cc.Mask', 'cc.Graphics'],
|
||||||
|
ui: ['cc.Button', 'cc.Toggle', 'cc.Slider', 'cc.ScrollView', 'cc.EditBox', 'cc.ProgressBar'],
|
||||||
|
physics: ['cc.RigidBody2D', 'cc.BoxCollider2D', 'cc.CircleCollider2D', 'cc.PolygonCollider2D'],
|
||||||
|
animation: ['cc.Animation', 'cc.AnimationClip', 'cc.SkeletalAnimation'],
|
||||||
|
audio: ['cc.AudioSource'],
|
||||||
|
layout: ['cc.Layout', 'cc.Widget', 'cc.PageView', 'cc.PageViewIndicator'],
|
||||||
|
effects: ['cc.MotionStreak', 'cc.ParticleSystem2D'],
|
||||||
|
camera: ['cc.Camera'],
|
||||||
|
light: ['cc.Light', 'cc.DirectionalLight', 'cc.PointLight', 'cc.SpotLight']
|
||||||
|
};
|
||||||
|
|
||||||
|
let components: string[] = [];
|
||||||
|
|
||||||
|
if (category === 'all') {
|
||||||
|
for (const cat in componentCategories) {
|
||||||
|
components = components.concat(componentCategories[cat]);
|
||||||
|
}
|
||||||
|
} else if (componentCategories[category]) {
|
||||||
|
components = componentCategories[category];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
category: category,
|
||||||
|
components: components
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
351
source/tools/debug-tools.ts
Normal file
351
source/tools/debug-tools.ts
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor, ConsoleMessage, PerformanceStats, ValidationResult, ValidationIssue } from '../types';
|
||||||
|
|
||||||
|
export class DebugTools implements ToolExecutor {
|
||||||
|
private consoleMessages: ConsoleMessage[] = [];
|
||||||
|
private readonly maxMessages = 1000;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.setupConsoleCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupConsoleCapture(): void {
|
||||||
|
// Intercept Editor console messages
|
||||||
|
// Note: Editor.Message.addBroadcastListener may not be available in all versions
|
||||||
|
// This is a placeholder for console capture implementation
|
||||||
|
console.log('Console capture setup - implementation depends on Editor API availability');
|
||||||
|
}
|
||||||
|
|
||||||
|
private addConsoleMessage(message: any): void {
|
||||||
|
this.consoleMessages.push({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only latest messages
|
||||||
|
if (this.consoleMessages.length > this.maxMessages) {
|
||||||
|
this.consoleMessages.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'get_console_logs',
|
||||||
|
description: 'Get editor console logs',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of recent logs to retrieve',
|
||||||
|
default: 100
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter logs by type',
|
||||||
|
enum: ['all', 'log', 'warn', 'error', 'info'],
|
||||||
|
default: 'all'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clear_console',
|
||||||
|
description: 'Clear editor console',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'execute_script',
|
||||||
|
description: 'Execute JavaScript in scene context',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
script: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'JavaScript code to execute'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['script']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_node_tree',
|
||||||
|
description: 'Get detailed node tree for debugging',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
rootUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Root node UUID (optional, uses scene root if not provided)'
|
||||||
|
},
|
||||||
|
maxDepth: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Maximum tree depth',
|
||||||
|
default: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_performance_stats',
|
||||||
|
description: 'Get performance statistics',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'validate_scene',
|
||||||
|
description: 'Validate current scene for issues',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
checkMissingAssets: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Check for missing asset references',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
checkPerformance: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Check for performance issues',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_editor_info',
|
||||||
|
description: 'Get editor and environment information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'get_console_logs':
|
||||||
|
return await this.getConsoleLogs(args.limit, args.filter);
|
||||||
|
case 'clear_console':
|
||||||
|
return await this.clearConsole();
|
||||||
|
case 'execute_script':
|
||||||
|
return await this.executeScript(args.script);
|
||||||
|
case 'get_node_tree':
|
||||||
|
return await this.getNodeTree(args.rootUuid, args.maxDepth);
|
||||||
|
case 'get_performance_stats':
|
||||||
|
return await this.getPerformanceStats();
|
||||||
|
case 'validate_scene':
|
||||||
|
return await this.validateScene(args);
|
||||||
|
case 'get_editor_info':
|
||||||
|
return await this.getEditorInfo();
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getConsoleLogs(limit: number = 100, filter: string = 'all'): Promise<ToolResponse> {
|
||||||
|
let logs = this.consoleMessages;
|
||||||
|
|
||||||
|
if (filter !== 'all') {
|
||||||
|
logs = logs.filter(log => log.type === filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentLogs = logs.slice(-limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
total: logs.length,
|
||||||
|
returned: recentLogs.length,
|
||||||
|
logs: recentLogs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearConsole(): Promise<ToolResponse> {
|
||||||
|
this.consoleMessages = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Note: Editor.Message.send may not return a promise in all versions
|
||||||
|
Editor.Message.send('console', 'clear');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Console cleared successfully'
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeScript(script: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'execute-script', {
|
||||||
|
script: script
|
||||||
|
}).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
result: result,
|
||||||
|
message: 'Script executed successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNodeTree(rootUuid?: string, maxDepth: number = 10): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const buildTree = async (nodeUuid: string, depth: number = 0): Promise<any> => {
|
||||||
|
if (depth >= maxDepth) {
|
||||||
|
return { truncated: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodeData = await Editor.Message.request('scene', 'query-node', nodeUuid);
|
||||||
|
|
||||||
|
const tree = {
|
||||||
|
uuid: nodeData.uuid,
|
||||||
|
name: nodeData.name,
|
||||||
|
active: nodeData.active,
|
||||||
|
components: (nodeData as any).components ? (nodeData as any).components.map((c: any) => c.__type__) : [],
|
||||||
|
childCount: nodeData.children ? nodeData.children.length : 0,
|
||||||
|
children: [] as any[]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nodeData.children && nodeData.children.length > 0) {
|
||||||
|
for (const childId of nodeData.children) {
|
||||||
|
const childTree = await buildTree(childId, depth + 1);
|
||||||
|
tree.children.push(childTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
} catch (err: any) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rootUuid) {
|
||||||
|
buildTree(rootUuid).then(tree => {
|
||||||
|
resolve({ success: true, data: tree });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Editor.Message.request('scene', 'query-hierarchy').then(async (hierarchy: any) => {
|
||||||
|
const trees = [];
|
||||||
|
for (const rootNode of hierarchy.children) {
|
||||||
|
const tree = await buildTree(rootNode.uuid);
|
||||||
|
trees.push(tree);
|
||||||
|
}
|
||||||
|
resolve({ success: true, data: trees });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPerformanceStats(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'query-performance').then((stats: any) => {
|
||||||
|
const perfStats: PerformanceStats = {
|
||||||
|
nodeCount: stats.nodeCount || 0,
|
||||||
|
componentCount: stats.componentCount || 0,
|
||||||
|
drawCalls: stats.drawCalls || 0,
|
||||||
|
triangles: stats.triangles || 0,
|
||||||
|
memory: stats.memory || {}
|
||||||
|
};
|
||||||
|
resolve({ success: true, data: perfStats });
|
||||||
|
}).catch(() => {
|
||||||
|
// Fallback to basic stats
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: 'Performance stats not available in edit mode'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateScene(options: any): Promise<ToolResponse> {
|
||||||
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for missing assets
|
||||||
|
if (options.checkMissingAssets) {
|
||||||
|
const assetCheck = await Editor.Message.request('scene', 'check-missing-assets');
|
||||||
|
if (assetCheck && assetCheck.missing) {
|
||||||
|
issues.push({
|
||||||
|
type: 'error',
|
||||||
|
category: 'assets',
|
||||||
|
message: `Found ${assetCheck.missing.length} missing asset references`,
|
||||||
|
details: assetCheck.missing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for performance issues
|
||||||
|
if (options.checkPerformance) {
|
||||||
|
const hierarchy = await Editor.Message.request('scene', 'query-hierarchy');
|
||||||
|
const nodeCount = this.countNodes(hierarchy.children);
|
||||||
|
|
||||||
|
if (nodeCount > 1000) {
|
||||||
|
issues.push({
|
||||||
|
type: 'warning',
|
||||||
|
category: 'performance',
|
||||||
|
message: `High node count: ${nodeCount} nodes (recommended < 1000)`,
|
||||||
|
suggestion: 'Consider using object pooling or scene optimization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: ValidationResult = {
|
||||||
|
valid: issues.length === 0,
|
||||||
|
issueCount: issues.length,
|
||||||
|
issues: issues
|
||||||
|
};
|
||||||
|
|
||||||
|
return { success: true, data: result };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private countNodes(nodes: any[]): number {
|
||||||
|
let count = nodes.length;
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.children) {
|
||||||
|
count += this.countNodes(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getEditorInfo(): Promise<ToolResponse> {
|
||||||
|
const info = {
|
||||||
|
editor: {
|
||||||
|
version: (Editor as any).versions?.editor || 'Unknown',
|
||||||
|
cocosVersion: (Editor as any).versions?.cocos || 'Unknown',
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
nodeVersion: process.version
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: Editor.Project.name,
|
||||||
|
path: Editor.Project.path,
|
||||||
|
uuid: Editor.Project.uuid
|
||||||
|
},
|
||||||
|
memory: process.memoryUsage(),
|
||||||
|
uptime: process.uptime()
|
||||||
|
};
|
||||||
|
|
||||||
|
return { success: true, data: info };
|
||||||
|
}
|
||||||
|
}
|
||||||
526
source/tools/node-tools.ts
Normal file
526
source/tools/node-tools.ts
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor, NodeInfo } from '../types';
|
||||||
|
|
||||||
|
export class NodeTools implements ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'create_node',
|
||||||
|
description: 'Create a new node in the scene. If parentUuid is not provided, the node will be created at the current selection in the editor.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node name'
|
||||||
|
},
|
||||||
|
parentUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Parent node UUID. If not provided, node will be created at current editor selection. To create at scene root, first get the root node UUID.'
|
||||||
|
},
|
||||||
|
nodeType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node type: Node, 2DNode, 3DNode',
|
||||||
|
enum: ['Node', '2DNode', '3DNode'],
|
||||||
|
default: 'Node'
|
||||||
|
},
|
||||||
|
siblingIndex: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Sibling index for ordering (-1 means append at end)',
|
||||||
|
default: -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['name']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_node_info',
|
||||||
|
description: 'Get node information by UUID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['uuid']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'find_nodes',
|
||||||
|
description: 'Find nodes by name pattern',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
pattern: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name pattern to search'
|
||||||
|
},
|
||||||
|
exactMatch: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Exact match or partial match',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['pattern']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'find_node_by_name',
|
||||||
|
description: 'Find first node by exact name',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node name to find'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['name']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_all_nodes',
|
||||||
|
description: 'Get all nodes in the scene with their UUIDs',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'set_node_property',
|
||||||
|
description: 'Set node property value',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID'
|
||||||
|
},
|
||||||
|
property: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Property name (e.g., position, rotation, scale, active)'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'Property value'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['uuid', 'property', 'value']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete_node',
|
||||||
|
description: 'Delete a node from scene',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID to delete'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['uuid']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'move_node',
|
||||||
|
description: 'Move node to new parent',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID to move'
|
||||||
|
},
|
||||||
|
newParentUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'New parent node UUID'
|
||||||
|
},
|
||||||
|
siblingIndex: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Sibling index in new parent',
|
||||||
|
default: -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'newParentUuid']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duplicate_node',
|
||||||
|
description: 'Duplicate a node',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID to duplicate'
|
||||||
|
},
|
||||||
|
includeChildren: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Include children nodes',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['uuid']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'create_node':
|
||||||
|
return await this.createNode(args);
|
||||||
|
case 'get_node_info':
|
||||||
|
return await this.getNodeInfo(args.uuid);
|
||||||
|
case 'find_nodes':
|
||||||
|
return await this.findNodes(args.pattern, args.exactMatch);
|
||||||
|
case 'find_node_by_name':
|
||||||
|
return await this.findNodeByName(args.name);
|
||||||
|
case 'get_all_nodes':
|
||||||
|
return await this.getAllNodes();
|
||||||
|
case 'set_node_property':
|
||||||
|
return await this.setNodeProperty(args.uuid, args.property, args.value);
|
||||||
|
case 'delete_node':
|
||||||
|
return await this.deleteNode(args.uuid);
|
||||||
|
case 'move_node':
|
||||||
|
return await this.moveNode(args.nodeUuid, args.newParentUuid, args.siblingIndex);
|
||||||
|
case 'duplicate_node':
|
||||||
|
return await this.duplicateNode(args.uuid, args.includeChildren);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNode(args: any): Promise<ToolResponse> {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
// 如果指定了父节点,先验证父节点是否存在
|
||||||
|
if (args.parentUuid) {
|
||||||
|
try {
|
||||||
|
const parentNode = await Editor.Message.request('scene', 'query-node', args.parentUuid);
|
||||||
|
if (!parentNode) {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Parent node with UUID '${args.parentUuid}' not found`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to verify parent node: ${err}`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeData: any = {
|
||||||
|
name: args.name,
|
||||||
|
type: args.nodeType || 'cc.Node'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用更明确的父节点指定方式
|
||||||
|
if (args.parentUuid) {
|
||||||
|
nodeData.parent = args.parentUuid;
|
||||||
|
// 尝试先创建节点,然后移动到指定父节点
|
||||||
|
Editor.Message.request('scene', 'create-node', nodeData).then((nodeUuid: any) => {
|
||||||
|
// 如果创建成功但可能没有在正确的父节点下,尝试移动
|
||||||
|
if (args.parentUuid && nodeUuid) {
|
||||||
|
Editor.Message.request('scene', 'move-node', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
parent: args.parentUuid,
|
||||||
|
index: args.siblingIndex !== undefined ? args.siblingIndex : -1
|
||||||
|
}).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
name: args.name,
|
||||||
|
parentUuid: args.parentUuid,
|
||||||
|
message: `Node '${args.name}' created under specified parent`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
// 即使移动失败,节点已创建,返回成功但带警告
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
name: args.name,
|
||||||
|
message: `Node '${args.name}' created but may not be under specified parent`,
|
||||||
|
warning: 'Failed to move node to specified parent'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
name: args.name,
|
||||||
|
message: `Node '${args.name}' created successfully`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 没有指定父节点,使用默认行为
|
||||||
|
Editor.Message.request('scene', 'create-node', nodeData).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: result,
|
||||||
|
name: args.name,
|
||||||
|
message: `Node '${args.name}' created at current selection`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNodeInfo(uuid: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'query-node', uuid).then((nodeData: any) => {
|
||||||
|
if (!nodeData) {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Node not found or invalid response'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据实际返回的数据结构解析节点信息
|
||||||
|
const info: NodeInfo = {
|
||||||
|
uuid: nodeData.uuid?.value || uuid,
|
||||||
|
name: nodeData.name?.value || 'Unknown',
|
||||||
|
active: nodeData.active?.value !== undefined ? nodeData.active.value : true,
|
||||||
|
position: nodeData.position?.value || { x: 0, y: 0, z: 0 },
|
||||||
|
rotation: nodeData.rotation?.value || { x: 0, y: 0, z: 0 },
|
||||||
|
scale: nodeData.scale?.value || { x: 1, y: 1, z: 1 },
|
||||||
|
parent: nodeData.parent?.value?.uuid || null,
|
||||||
|
children: nodeData.children || [],
|
||||||
|
components: (nodeData.__comps__ || []).map((comp: any) => ({
|
||||||
|
type: comp.__type__ || 'Unknown',
|
||||||
|
enabled: comp.enabled !== undefined ? comp.enabled : true
|
||||||
|
})),
|
||||||
|
layer: nodeData.layer?.value || 1073741824,
|
||||||
|
mobility: nodeData.mobility?.value || 0
|
||||||
|
};
|
||||||
|
resolve({ success: true, data: info });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findNodes(pattern: string, exactMatch: boolean = false): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'query-nodes-by-name', {
|
||||||
|
name: pattern,
|
||||||
|
exactMatch: exactMatch
|
||||||
|
}).then((results: any[]) => {
|
||||||
|
const nodes = results.map(node => ({
|
||||||
|
uuid: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
path: node.path
|
||||||
|
}));
|
||||||
|
resolve({ success: true, data: nodes });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findNodeByName(name: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 优先尝试使用 Editor API 查询节点树并搜索
|
||||||
|
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||||
|
const foundNode = this.searchNodeInTree(tree, name);
|
||||||
|
if (foundNode) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: foundNode.uuid,
|
||||||
|
name: foundNode.name,
|
||||||
|
path: this.getNodePath(foundNode)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: `Node '${name}' not found` });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'findNodeByName',
|
||||||
|
args: [name]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private searchNodeInTree(node: any, targetName: string): any {
|
||||||
|
if (node.name === targetName) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
const found = this.searchNodeInTree(child, targetName);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllNodes(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 尝试查询场景节点树
|
||||||
|
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||||
|
const nodes: any[] = [];
|
||||||
|
|
||||||
|
const traverseTree = (node: any) => {
|
||||||
|
nodes.push({
|
||||||
|
uuid: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
type: node.type,
|
||||||
|
active: node.active,
|
||||||
|
path: this.getNodePath(node)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
traverseTree(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tree && tree.children) {
|
||||||
|
traverseTree(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
totalNodes: nodes.length,
|
||||||
|
nodes: nodes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'getAllNodes',
|
||||||
|
args: []
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNodePath(node: any): string {
|
||||||
|
const path = [node.name];
|
||||||
|
let current = node.parent;
|
||||||
|
while (current && current.name !== 'Canvas') {
|
||||||
|
path.unshift(current.name);
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return path.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setNodeProperty(uuid: string, property: string, value: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 尝试直接使用 Editor API 设置节点属性
|
||||||
|
Editor.Message.request('scene', 'set-property', {
|
||||||
|
uuid: uuid,
|
||||||
|
path: property,
|
||||||
|
dump: {
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: `Property '${property}' updated successfully`
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 如果直接设置失败,尝试使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'setNodeProperty',
|
||||||
|
args: [uuid, property, value]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteNode(uuid: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'remove-node', { uuid: uuid }).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: 'Node deleted successfully'
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async moveNode(nodeUuid: string, newParentUuid: string, siblingIndex: number = -1): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'move-node', {
|
||||||
|
uuid: nodeUuid,
|
||||||
|
parent: newParentUuid,
|
||||||
|
index: siblingIndex
|
||||||
|
}).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: 'Node moved successfully'
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async duplicateNode(uuid: string, includeChildren: boolean = true): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'duplicate-node', uuid).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
newUuid: result.uuid,
|
||||||
|
message: 'Node duplicated successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
359
source/tools/prefab-tools.ts
Normal file
359
source/tools/prefab-tools.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor, PrefabInfo } from '../types';
|
||||||
|
|
||||||
|
export class PrefabTools implements ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'get_prefab_list',
|
||||||
|
description: 'Get all prefabs in the project',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
folder: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Folder path to search (optional)',
|
||||||
|
default: 'db://assets'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'load_prefab',
|
||||||
|
description: 'Load a prefab by path',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prefabPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prefab asset path'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['prefabPath']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'instantiate_prefab',
|
||||||
|
description: 'Instantiate a prefab in the scene',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prefabPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prefab asset path'
|
||||||
|
},
|
||||||
|
parentUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Parent node UUID (optional)'
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Initial position',
|
||||||
|
properties: {
|
||||||
|
x: { type: 'number' },
|
||||||
|
y: { type: 'number' },
|
||||||
|
z: { type: 'number' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['prefabPath']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_prefab',
|
||||||
|
description: 'Create a prefab from a node',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source node UUID'
|
||||||
|
},
|
||||||
|
savePath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Path to save the prefab'
|
||||||
|
},
|
||||||
|
prefabName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prefab name'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'savePath', 'prefabName']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_prefab_from_node',
|
||||||
|
description: 'Create a prefab from a node (alias for create_prefab)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source node UUID'
|
||||||
|
},
|
||||||
|
prefabPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Path to save the prefab'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid', 'prefabPath']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update_prefab',
|
||||||
|
description: 'Update an existing prefab',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prefabPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prefab asset path'
|
||||||
|
},
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Node UUID with changes'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['prefabPath', 'nodeUuid']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'revert_prefab',
|
||||||
|
description: 'Revert prefab instance to original',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nodeUuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prefab instance node UUID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['nodeUuid']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_prefab_info',
|
||||||
|
description: 'Get detailed prefab information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prefabPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prefab asset path'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['prefabPath']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'get_prefab_list':
|
||||||
|
return await this.getPrefabList(args.folder);
|
||||||
|
case 'load_prefab':
|
||||||
|
return await this.loadPrefab(args.prefabPath);
|
||||||
|
case 'instantiate_prefab':
|
||||||
|
return await this.instantiatePrefab(args);
|
||||||
|
case 'create_prefab':
|
||||||
|
return await this.createPrefab(args);
|
||||||
|
case 'create_prefab_from_node':
|
||||||
|
return await this.createPrefabFromNode(args);
|
||||||
|
case 'update_prefab':
|
||||||
|
return await this.updatePrefab(args.prefabPath, args.nodeUuid);
|
||||||
|
case 'revert_prefab':
|
||||||
|
return await this.revertPrefab(args.nodeUuid);
|
||||||
|
case 'get_prefab_info':
|
||||||
|
return await this.getPrefabInfo(args.prefabPath);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPrefabList(folder: string = 'db://assets'): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const pattern = folder.endsWith('/') ?
|
||||||
|
`${folder}**/*.prefab` : `${folder}/**/*.prefab`;
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'query-assets', {
|
||||||
|
pattern: pattern
|
||||||
|
}).then((results: any[]) => {
|
||||||
|
const prefabs: PrefabInfo[] = results.map(asset => ({
|
||||||
|
name: asset.name,
|
||||||
|
path: asset.url,
|
||||||
|
uuid: asset.uuid,
|
||||||
|
folder: asset.url.substring(0, asset.url.lastIndexOf('/'))
|
||||||
|
}));
|
||||||
|
resolve({ success: true, data: prefabs });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadPrefab(prefabPath: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-asset-info', prefabPath).then((assetInfo: any) => {
|
||||||
|
if (!assetInfo) {
|
||||||
|
throw new Error('Prefab not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Editor.Message.request('scene', 'load-asset', {
|
||||||
|
uuid: assetInfo.uuid
|
||||||
|
});
|
||||||
|
}).then((prefabData: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: prefabData.uuid,
|
||||||
|
name: prefabData.name,
|
||||||
|
message: 'Prefab loaded successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async instantiatePrefab(args: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-asset-info', args.prefabPath).then((assetInfo: any) => {
|
||||||
|
if (!assetInfo) {
|
||||||
|
throw new Error('Prefab not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const instantiateData: any = {
|
||||||
|
prefab: assetInfo.uuid
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.parentUuid) {
|
||||||
|
instantiateData.parent = args.parentUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.position) {
|
||||||
|
instantiateData.position = args.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Editor.Message.request('scene', 'instantiate-prefab', instantiateData);
|
||||||
|
}).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
nodeUuid: result.uuid,
|
||||||
|
name: result.name,
|
||||||
|
message: 'Prefab instantiated successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createPrefab(args: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 支持 prefabPath 和 savePath 两种参数名
|
||||||
|
const pathParam = args.prefabPath || args.savePath;
|
||||||
|
if (!pathParam) {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Missing prefab path parameter. Please provide either prefabPath or savePath.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = pathParam.endsWith('.prefab') ?
|
||||||
|
pathParam : `${pathParam}/${args.prefabName || 'NewPrefab'}.prefab`;
|
||||||
|
|
||||||
|
// 预制体创建需要特殊的Editor API支持
|
||||||
|
// 目前Cocos Creator 3.8的MCP插件环境下,预制体创建功能受限
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Prefab creation is not supported in the current MCP plugin environment',
|
||||||
|
instruction: 'Please create prefabs manually by dragging nodes from the scene to the assets folder in the Cocos Creator editor',
|
||||||
|
data: {
|
||||||
|
nodeUuid: args.nodeUuid,
|
||||||
|
requestedPath: fullPath,
|
||||||
|
suggestion: 'You can manually drag the node from the scene to the assets folder to create a prefab.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updatePrefab(prefabPath: string, nodeUuid: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-asset-info', prefabPath).then((assetInfo: any) => {
|
||||||
|
if (!assetInfo) {
|
||||||
|
throw new Error('Prefab not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Editor.Message.request('scene', 'apply-prefab', {
|
||||||
|
node: nodeUuid,
|
||||||
|
prefab: assetInfo.uuid
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: 'Prefab updated successfully'
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async revertPrefab(nodeUuid: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'revert-prefab', {
|
||||||
|
node: nodeUuid
|
||||||
|
}).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: 'Prefab instance reverted successfully'
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPrefabInfo(prefabPath: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-asset-info', prefabPath).then((assetInfo: any) => {
|
||||||
|
if (!assetInfo) {
|
||||||
|
throw new Error('Prefab not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Editor.Message.request('asset-db', 'query-asset-meta', assetInfo.uuid);
|
||||||
|
}).then((metaInfo: any) => {
|
||||||
|
const info: PrefabInfo = {
|
||||||
|
name: metaInfo.name,
|
||||||
|
uuid: metaInfo.uuid,
|
||||||
|
path: prefabPath,
|
||||||
|
folder: prefabPath.substring(0, prefabPath.lastIndexOf('/')),
|
||||||
|
createTime: metaInfo.createTime,
|
||||||
|
modifyTime: metaInfo.modifyTime,
|
||||||
|
dependencies: metaInfo.depends || []
|
||||||
|
};
|
||||||
|
resolve({ success: true, data: info });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createPrefabFromNode(args: any): Promise<ToolResponse> {
|
||||||
|
// 从 prefabPath 提取名称
|
||||||
|
const prefabPath = args.prefabPath;
|
||||||
|
const prefabName = prefabPath.split('/').pop()?.replace('.prefab', '') || 'NewPrefab';
|
||||||
|
|
||||||
|
// 调用原来的 createPrefab 方法
|
||||||
|
return await this.createPrefab({
|
||||||
|
nodeUuid: args.nodeUuid,
|
||||||
|
savePath: prefabPath,
|
||||||
|
prefabName: prefabName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
163
source/tools/preferences-tools.ts
Normal file
163
source/tools/preferences-tools.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
|
||||||
|
|
||||||
|
export class PreferencesTools implements ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'get_preferences',
|
||||||
|
description: 'Get editor preferences',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Specific preference key to get (optional)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'set_preferences',
|
||||||
|
description: 'Set editor preferences',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Preference key to set'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'Preference value to set'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['key', 'value']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_global_preferences',
|
||||||
|
description: 'Get global editor preferences',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Global preference key to get (optional)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'set_global_preferences',
|
||||||
|
description: 'Set global editor preferences',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Global preference key to set'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'Global preference value to set'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['key', 'value']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_recent_projects',
|
||||||
|
description: 'Get recently opened projects',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clear_recent_projects',
|
||||||
|
description: 'Clear recently opened projects list',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'get_preferences':
|
||||||
|
return await this.getPreferences(args.key);
|
||||||
|
case 'set_preferences':
|
||||||
|
return await this.setPreferences(args.key, args.value);
|
||||||
|
case 'get_global_preferences':
|
||||||
|
return await this.getGlobalPreferences(args.key);
|
||||||
|
case 'set_global_preferences':
|
||||||
|
return await this.setGlobalPreferences(args.key, args.value);
|
||||||
|
case 'get_recent_projects':
|
||||||
|
return await this.getRecentProjects();
|
||||||
|
case 'clear_recent_projects':
|
||||||
|
return await this.clearRecentProjects();
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPreferences(key?: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Preferences API is not supported through MCP',
|
||||||
|
instruction: 'Please access preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setPreferences(key: string, value: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Preferences API is not supported through MCP',
|
||||||
|
instruction: 'Please modify preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getGlobalPreferences(key?: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Global preferences API is not supported through MCP',
|
||||||
|
instruction: 'Please access global preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setGlobalPreferences(key: string, value: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Global preferences API is not supported through MCP',
|
||||||
|
instruction: 'Please modify global preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRecentProjects(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Recent projects API is not supported through MCP',
|
||||||
|
instruction: 'Please check recent projects through the editor menu: File > Recent Projects or the start screen'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearRecentProjects(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Recent projects API is not supported through MCP',
|
||||||
|
instruction: 'Please clear recent projects through the editor menu: File > Recent Projects or the start screen'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
890
source/tools/project-tools.ts
Normal file
890
source/tools/project-tools.ts
Normal file
@@ -0,0 +1,890 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor, ProjectInfo, AssetInfo } from '../types';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class ProjectTools implements ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'run_project',
|
||||||
|
description: 'Run the project in preview mode',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
platform: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target platform',
|
||||||
|
enum: ['browser', 'simulator', 'preview'],
|
||||||
|
default: 'browser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'build_project',
|
||||||
|
description: 'Build the project',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
platform: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Build platform',
|
||||||
|
enum: ['web-mobile', 'web-desktop', 'ios', 'android', 'windows', 'mac']
|
||||||
|
},
|
||||||
|
debug: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Debug build',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['platform']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_project_info',
|
||||||
|
description: 'Get project information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_project_settings',
|
||||||
|
description: 'Get project settings',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Settings category',
|
||||||
|
enum: ['general', 'physics', 'render', 'assets'],
|
||||||
|
default: 'general'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'refresh_assets',
|
||||||
|
description: 'Refresh asset database',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
folder: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Specific folder to refresh (optional)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'import_asset',
|
||||||
|
description: 'Import an asset file',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sourcePath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source file path'
|
||||||
|
},
|
||||||
|
targetFolder: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target folder in assets'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['sourcePath', 'targetFolder']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_asset_info',
|
||||||
|
description: 'Get asset information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
assetPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset path (db://assets/...)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['assetPath']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_assets',
|
||||||
|
description: 'Get assets by type',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset type filter',
|
||||||
|
enum: ['all', 'scene', 'prefab', 'script', 'texture', 'material', 'mesh', 'audio', 'animation'],
|
||||||
|
default: 'all'
|
||||||
|
},
|
||||||
|
folder: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Folder to search in',
|
||||||
|
default: 'db://assets'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_build_settings',
|
||||||
|
description: 'Get build settings - shows current limitations',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'open_build_panel',
|
||||||
|
description: 'Open the build panel in the editor',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'check_builder_status',
|
||||||
|
description: 'Check if builder worker is ready',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'start_preview_server',
|
||||||
|
description: 'Start preview server',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
port: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Preview server port',
|
||||||
|
default: 7456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop_preview_server',
|
||||||
|
description: 'Stop preview server',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_asset',
|
||||||
|
description: 'Create a new asset file or folder',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset URL (e.g., db://assets/newfile.json)'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'File content (null for folder)',
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
overwrite: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Overwrite existing file',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'copy_asset',
|
||||||
|
description: 'Copy an asset to another location',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
source: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source asset URL'
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target location URL'
|
||||||
|
},
|
||||||
|
overwrite: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Overwrite existing file',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['source', 'target']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'move_asset',
|
||||||
|
description: 'Move an asset to another location',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
source: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source asset URL'
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target location URL'
|
||||||
|
},
|
||||||
|
overwrite: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Overwrite existing file',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['source', 'target']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete_asset',
|
||||||
|
description: 'Delete an asset',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset URL to delete'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'save_asset',
|
||||||
|
description: 'Save asset content',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset URL'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset content'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url', 'content']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reimport_asset',
|
||||||
|
description: 'Reimport an asset',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset URL to reimport'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'query_asset_path',
|
||||||
|
description: 'Get asset disk path',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset URL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'query_asset_uuid',
|
||||||
|
description: 'Get asset UUID from URL',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset URL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['url']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'query_asset_url',
|
||||||
|
description: 'Get asset URL from UUID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Asset UUID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['uuid']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'run_project':
|
||||||
|
return await this.runProject(args.platform);
|
||||||
|
case 'build_project':
|
||||||
|
return await this.buildProject(args);
|
||||||
|
case 'get_project_info':
|
||||||
|
return await this.getProjectInfo();
|
||||||
|
case 'get_project_settings':
|
||||||
|
return await this.getProjectSettings(args.category);
|
||||||
|
case 'refresh_assets':
|
||||||
|
return await this.refreshAssets(args.folder);
|
||||||
|
case 'import_asset':
|
||||||
|
return await this.importAsset(args.sourcePath, args.targetFolder);
|
||||||
|
case 'get_asset_info':
|
||||||
|
return await this.getAssetInfo(args.assetPath);
|
||||||
|
case 'get_assets':
|
||||||
|
return await this.getAssets(args.type, args.folder);
|
||||||
|
case 'get_build_settings':
|
||||||
|
return await this.getBuildSettings();
|
||||||
|
case 'open_build_panel':
|
||||||
|
return await this.openBuildPanel();
|
||||||
|
case 'check_builder_status':
|
||||||
|
return await this.checkBuilderStatus();
|
||||||
|
case 'start_preview_server':
|
||||||
|
return await this.startPreviewServer(args.port);
|
||||||
|
case 'stop_preview_server':
|
||||||
|
return await this.stopPreviewServer();
|
||||||
|
case 'create_asset':
|
||||||
|
return await this.createAsset(args.url, args.content, args.overwrite);
|
||||||
|
case 'copy_asset':
|
||||||
|
return await this.copyAsset(args.source, args.target, args.overwrite);
|
||||||
|
case 'move_asset':
|
||||||
|
return await this.moveAsset(args.source, args.target, args.overwrite);
|
||||||
|
case 'delete_asset':
|
||||||
|
return await this.deleteAsset(args.url);
|
||||||
|
case 'save_asset':
|
||||||
|
return await this.saveAsset(args.url, args.content);
|
||||||
|
case 'reimport_asset':
|
||||||
|
return await this.reimportAsset(args.url);
|
||||||
|
case 'query_asset_path':
|
||||||
|
return await this.queryAssetPath(args.url);
|
||||||
|
case 'query_asset_uuid':
|
||||||
|
return await this.queryAssetUuid(args.url);
|
||||||
|
case 'query_asset_url':
|
||||||
|
return await this.queryAssetUrl(args.uuid);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runProject(platform: string = 'browser'): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const previewConfig = {
|
||||||
|
platform: platform,
|
||||||
|
scenes: [] // Will use current scene
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('preview', 'start', previewConfig).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: `Project is running in ${platform} mode`
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildProject(args: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const buildOptions = {
|
||||||
|
platform: args.platform,
|
||||||
|
debug: args.debug !== false,
|
||||||
|
sourceMaps: args.debug !== false,
|
||||||
|
buildPath: `build/${args.platform}`
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('builder', 'build', buildOptions).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: `Project built for ${args.platform}`,
|
||||||
|
data: { buildPath: buildOptions.buildPath }
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectInfo(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const info: ProjectInfo = {
|
||||||
|
name: Editor.Project.name,
|
||||||
|
path: Editor.Project.path,
|
||||||
|
uuid: Editor.Project.uuid,
|
||||||
|
version: (Editor.Project as any).version || '1.0.0',
|
||||||
|
cocosVersion: (Editor as any).versions?.cocos || 'Unknown'
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('project', 'query-info').then((additionalInfo: any) => {
|
||||||
|
Object.assign(info, additionalInfo);
|
||||||
|
resolve({ success: true, data: info });
|
||||||
|
}).catch(() => {
|
||||||
|
// Return basic info even if detailed query fails
|
||||||
|
resolve({ success: true, data: info });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectSettings(category: string = 'general'): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 使用正确的 project API 查询项目配置
|
||||||
|
const configMap: Record<string, string> = {
|
||||||
|
general: 'project',
|
||||||
|
physics: 'physics',
|
||||||
|
render: 'render',
|
||||||
|
assets: 'asset-db'
|
||||||
|
};
|
||||||
|
|
||||||
|
const configName = configMap[category] || 'project';
|
||||||
|
|
||||||
|
Editor.Message.request('project', 'query-config', configName).then((settings: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
category: category,
|
||||||
|
config: settings,
|
||||||
|
message: `${category} settings retrieved successfully`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshAssets(folder?: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 使用正确的 asset-db API 刷新资源
|
||||||
|
const targetPath = folder || 'db://assets';
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'refresh-asset', targetPath).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: `Assets refreshed in: ${targetPath}`
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async importAsset(sourcePath: string, targetFolder: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
resolve({ success: false, error: 'Source file not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = path.basename(sourcePath);
|
||||||
|
const targetPath = targetFolder.startsWith('db://') ?
|
||||||
|
targetFolder : `db://assets/${targetFolder}`;
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'import-asset', sourcePath, `${targetPath}/${fileName}`).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: result.uuid,
|
||||||
|
path: result.url,
|
||||||
|
message: `Asset imported: ${fileName}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAssetInfo(assetPath: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-asset-info', assetPath).then((assetInfo: any) => {
|
||||||
|
if (!assetInfo) {
|
||||||
|
throw new Error('Asset not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const info: AssetInfo = {
|
||||||
|
name: assetInfo.name,
|
||||||
|
uuid: assetInfo.uuid,
|
||||||
|
path: assetInfo.url,
|
||||||
|
type: assetInfo.type,
|
||||||
|
size: assetInfo.size,
|
||||||
|
isDirectory: assetInfo.isDirectory
|
||||||
|
};
|
||||||
|
|
||||||
|
if (assetInfo.meta) {
|
||||||
|
info.meta = {
|
||||||
|
ver: assetInfo.meta.ver,
|
||||||
|
importer: assetInfo.meta.importer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ success: true, data: info });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAssets(type: string = 'all', folder: string = 'db://assets'): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let pattern = `${folder}/**/*`;
|
||||||
|
|
||||||
|
// 添加类型过滤
|
||||||
|
if (type !== 'all') {
|
||||||
|
const typeExtensions: Record<string, string> = {
|
||||||
|
'scene': '.scene',
|
||||||
|
'prefab': '.prefab',
|
||||||
|
'script': '.{ts,js}',
|
||||||
|
'texture': '.{png,jpg,jpeg,gif,tga,bmp,psd}',
|
||||||
|
'material': '.mtl',
|
||||||
|
'mesh': '.{fbx,obj,dae}',
|
||||||
|
'audio': '.{mp3,ogg,wav,m4a}',
|
||||||
|
'animation': '.{anim,clip}'
|
||||||
|
};
|
||||||
|
|
||||||
|
const extension = typeExtensions[type];
|
||||||
|
if (extension) {
|
||||||
|
pattern = `${folder}/**/*${extension}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'query-assets', {
|
||||||
|
pattern: pattern
|
||||||
|
}).then((results: any[]) => {
|
||||||
|
const assets = results.map(asset => ({
|
||||||
|
name: asset.name,
|
||||||
|
uuid: asset.uuid,
|
||||||
|
path: asset.url,
|
||||||
|
type: asset.type,
|
||||||
|
size: asset.size || 0,
|
||||||
|
isDirectory: asset.isDirectory || false
|
||||||
|
}));
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
type: type,
|
||||||
|
folder: folder,
|
||||||
|
count: assets.length,
|
||||||
|
assets: assets
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getBuildSettings(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 检查构建器是否准备就绪
|
||||||
|
Editor.Message.request('builder', 'query-worker-ready').then((ready: boolean) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
builderReady: ready,
|
||||||
|
message: 'Build settings are limited in MCP plugin environment',
|
||||||
|
availableActions: [
|
||||||
|
'Open build panel with open_build_panel',
|
||||||
|
'Check builder status with check_builder_status',
|
||||||
|
'Start preview server with start_preview_server',
|
||||||
|
'Stop preview server with stop_preview_server'
|
||||||
|
],
|
||||||
|
limitation: 'Full build configuration requires direct Editor UI access'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openBuildPanel(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('builder', 'open').then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: 'Build panel opened successfully'
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkBuilderStatus(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('builder', 'query-worker-ready').then((ready: boolean) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
ready: ready,
|
||||||
|
status: ready ? 'Builder worker is ready' : 'Builder worker is not ready',
|
||||||
|
message: 'Builder status checked successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startPreviewServer(port: number = 7456): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Preview server control is not supported through MCP API',
|
||||||
|
instruction: 'Please start the preview server manually using the editor menu: Project > Preview, or use the preview panel in the editor'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async stopPreviewServer(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Preview server control is not supported through MCP API',
|
||||||
|
instruction: 'Please stop the preview server manually using the preview panel in the editor'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAsset(url: string, content: string | null = null, overwrite: boolean = false): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const options = {
|
||||||
|
overwrite: overwrite,
|
||||||
|
rename: !overwrite
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'create-asset', url, content, options).then((result: any) => {
|
||||||
|
if (result && result.uuid) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: result.uuid,
|
||||||
|
url: result.url,
|
||||||
|
message: content === null ? 'Folder created successfully' : 'File created successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
url: url,
|
||||||
|
message: content === null ? 'Folder created successfully' : 'File created successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyAsset(source: string, target: string, overwrite: boolean = false): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const options = {
|
||||||
|
overwrite: overwrite,
|
||||||
|
rename: !overwrite
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'copy-asset', source, target, options).then((result: any) => {
|
||||||
|
if (result && result.uuid) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: result.uuid,
|
||||||
|
url: result.url,
|
||||||
|
message: 'Asset copied successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
source: source,
|
||||||
|
target: target,
|
||||||
|
message: 'Asset copied successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async moveAsset(source: string, target: string, overwrite: boolean = false): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const options = {
|
||||||
|
overwrite: overwrite,
|
||||||
|
rename: !overwrite
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'move-asset', source, target, options).then((result: any) => {
|
||||||
|
if (result && result.uuid) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: result.uuid,
|
||||||
|
url: result.url,
|
||||||
|
message: 'Asset moved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
source: source,
|
||||||
|
target: target,
|
||||||
|
message: 'Asset moved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteAsset(url: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'delete-asset', url).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
url: url,
|
||||||
|
message: 'Asset deleted successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveAsset(url: string, content: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'save-asset', url, content).then((result: any) => {
|
||||||
|
if (result && result.uuid) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: result.uuid,
|
||||||
|
url: result.url,
|
||||||
|
message: 'Asset saved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
url: url,
|
||||||
|
message: 'Asset saved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reimportAsset(url: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'reimport-asset', url).then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
url: url,
|
||||||
|
message: 'Asset reimported successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queryAssetPath(url: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-path', url).then((path: string | null) => {
|
||||||
|
if (path) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
url: url,
|
||||||
|
path: path,
|
||||||
|
message: 'Asset path retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'Asset path not found' });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queryAssetUuid(url: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-uuid', url).then((uuid: string | null) => {
|
||||||
|
if (uuid) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
url: url,
|
||||||
|
uuid: uuid,
|
||||||
|
message: 'Asset UUID retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'Asset UUID not found' });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queryAssetUrl(uuid: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-url', uuid).then((url: string | null) => {
|
||||||
|
if (url) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: uuid,
|
||||||
|
url: url,
|
||||||
|
message: 'Asset URL retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'Asset URL not found' });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
465
source/tools/scene-tools.ts
Normal file
465
source/tools/scene-tools.ts
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor, SceneInfo } from '../types';
|
||||||
|
|
||||||
|
export class SceneTools implements ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'get_current_scene',
|
||||||
|
description: 'Get current scene information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_scene_list',
|
||||||
|
description: 'Get all scenes in the project',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'open_scene',
|
||||||
|
description: 'Open a scene by path',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
scenePath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The scene file path'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['scenePath']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'save_scene',
|
||||||
|
description: 'Save current scene',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_scene',
|
||||||
|
description: 'Create a new scene asset',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sceneName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of the new scene'
|
||||||
|
},
|
||||||
|
savePath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Path to save the scene (e.g., db://assets/scenes/NewScene.scene)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['sceneName', 'savePath']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'save_scene_as',
|
||||||
|
description: 'Save scene as new file',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Path to save the scene'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['path']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'close_scene',
|
||||||
|
description: 'Close current scene',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_scene_hierarchy',
|
||||||
|
description: 'Get the complete hierarchy of current scene',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
includeComponents: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Include component information',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'get_current_scene':
|
||||||
|
return await this.getCurrentScene();
|
||||||
|
case 'get_scene_list':
|
||||||
|
return await this.getSceneList();
|
||||||
|
case 'open_scene':
|
||||||
|
return await this.openScene(args.scenePath);
|
||||||
|
case 'save_scene':
|
||||||
|
return await this.saveScene();
|
||||||
|
case 'create_scene':
|
||||||
|
return await this.createScene(args.sceneName, args.savePath);
|
||||||
|
case 'save_scene_as':
|
||||||
|
return await this.saveSceneAs(args.path);
|
||||||
|
case 'close_scene':
|
||||||
|
return await this.closeScene();
|
||||||
|
case 'get_scene_hierarchy':
|
||||||
|
return await this.getSceneHierarchy(args.includeComponents);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCurrentScene(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 直接使用 query-node-tree 来获取场景信息(这个方法已经验证可用)
|
||||||
|
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||||
|
if (tree && tree.uuid) {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name: tree.name || 'Current Scene',
|
||||||
|
uuid: tree.uuid,
|
||||||
|
type: tree.type || 'cc.Scene',
|
||||||
|
active: tree.active !== undefined ? tree.active : true,
|
||||||
|
nodeCount: tree.children ? tree.children.length : 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'No scene data available' });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'getCurrentSceneInfo',
|
||||||
|
args: []
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSceneList(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('asset-db', 'query-assets', {
|
||||||
|
pattern: 'db://assets/**/*.scene'
|
||||||
|
}).then((results: any[]) => {
|
||||||
|
const scenes: SceneInfo[] = results.map(asset => ({
|
||||||
|
name: asset.name,
|
||||||
|
path: asset.url,
|
||||||
|
uuid: asset.uuid
|
||||||
|
}));
|
||||||
|
resolve({ success: true, data: scenes });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openScene(scenePath: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 首先获取场景的UUID
|
||||||
|
Editor.Message.request('asset-db', 'query-uuid', scenePath).then((uuid: string | null) => {
|
||||||
|
if (!uuid) {
|
||||||
|
throw new Error('Scene not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正确的 scene API 打开场景 (需要UUID)
|
||||||
|
return Editor.Message.request('scene', 'open-scene', uuid);
|
||||||
|
}).then(() => {
|
||||||
|
resolve({ success: true, message: `Scene opened: ${scenePath}` });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveScene(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'save-scene').then(() => {
|
||||||
|
resolve({ success: true, message: 'Scene saved successfully' });
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createScene(sceneName: string, savePath: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 确保路径以.scene结尾
|
||||||
|
const fullPath = savePath.endsWith('.scene') ? savePath : `${savePath}/${sceneName}.scene`;
|
||||||
|
|
||||||
|
// 使用正确的Cocos Creator 3.8场景格式
|
||||||
|
const sceneContent = JSON.stringify([
|
||||||
|
{
|
||||||
|
"__type__": "cc.SceneAsset",
|
||||||
|
"_name": sceneName,
|
||||||
|
"_objFlags": 0,
|
||||||
|
"__editorExtras__": {},
|
||||||
|
"_native": "",
|
||||||
|
"scene": {
|
||||||
|
"__id__": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.Scene",
|
||||||
|
"_name": sceneName,
|
||||||
|
"_objFlags": 0,
|
||||||
|
"__editorExtras__": {},
|
||||||
|
"_parent": null,
|
||||||
|
"_children": [],
|
||||||
|
"_active": true,
|
||||||
|
"_components": [],
|
||||||
|
"_prefab": null,
|
||||||
|
"_lpos": {
|
||||||
|
"__type__": "cc.Vec3",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"_lrot": {
|
||||||
|
"__type__": "cc.Quat",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"w": 1
|
||||||
|
},
|
||||||
|
"_lscale": {
|
||||||
|
"__type__": "cc.Vec3",
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
|
"_mobility": 0,
|
||||||
|
"_layer": 1073741824,
|
||||||
|
"_euler": {
|
||||||
|
"__type__": "cc.Vec3",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"autoReleaseAssets": false,
|
||||||
|
"_globals": {
|
||||||
|
"__id__": 2
|
||||||
|
},
|
||||||
|
"_id": "scene"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.SceneGlobals",
|
||||||
|
"ambient": {
|
||||||
|
"__id__": 3
|
||||||
|
},
|
||||||
|
"skybox": {
|
||||||
|
"__id__": 4
|
||||||
|
},
|
||||||
|
"fog": {
|
||||||
|
"__id__": 5
|
||||||
|
},
|
||||||
|
"octree": {
|
||||||
|
"__id__": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.AmbientInfo",
|
||||||
|
"_skyColorHDR": {
|
||||||
|
"__type__": "cc.Vec4",
|
||||||
|
"x": 0.2,
|
||||||
|
"y": 0.5,
|
||||||
|
"z": 0.8,
|
||||||
|
"w": 0.520833
|
||||||
|
},
|
||||||
|
"_skyColor": {
|
||||||
|
"__type__": "cc.Vec4",
|
||||||
|
"x": 0.2,
|
||||||
|
"y": 0.5,
|
||||||
|
"z": 0.8,
|
||||||
|
"w": 0.520833
|
||||||
|
},
|
||||||
|
"_skyIllumHDR": 20000,
|
||||||
|
"_skyIllum": 20000,
|
||||||
|
"_groundAlbedoHDR": {
|
||||||
|
"__type__": "cc.Vec4",
|
||||||
|
"x": 0.2,
|
||||||
|
"y": 0.2,
|
||||||
|
"z": 0.2,
|
||||||
|
"w": 1
|
||||||
|
},
|
||||||
|
"_groundAlbedo": {
|
||||||
|
"__type__": "cc.Vec4",
|
||||||
|
"x": 0.2,
|
||||||
|
"y": 0.2,
|
||||||
|
"z": 0.2,
|
||||||
|
"w": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "cc.SkyboxInfo",
|
||||||
|
"_envLightingType": 0,
|
||||||
|
"_envmapHDR": null,
|
||||||
|
"_envmap": null,
|
||||||
|
"_envmapLodCount": 0,
|
||||||
|
"_diffuseMapHDR": null,
|
||||||
|
"_diffuseMap": null,
|
||||||
|
"_enabled": false,
|
||||||
|
"_useHDR": true,
|
||||||
|
"_editableMaterial": null,
|
||||||
|
"_reflectionHDR": null,
|
||||||
|
"_reflectionMap": 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
|
||||||
|
}
|
||||||
|
], null, 2);
|
||||||
|
|
||||||
|
Editor.Message.request('asset-db', 'create-asset', fullPath, sceneContent).then((result: any) => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: result.uuid,
|
||||||
|
url: result.url,
|
||||||
|
name: sceneName,
|
||||||
|
message: `Scene '${sceneName}' created successfully`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSceneHierarchy(includeComponents: boolean = false): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 优先尝试使用 Editor API 查询场景节点树
|
||||||
|
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||||
|
if (tree) {
|
||||||
|
const hierarchy = this.buildHierarchy(tree, includeComponents);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: hierarchy
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'No scene hierarchy available' });
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// 备用方案:使用场景脚本
|
||||||
|
const options = {
|
||||||
|
name: 'cocos-mcp-server',
|
||||||
|
method: 'getSceneHierarchy',
|
||||||
|
args: [includeComponents]
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch((err2: Error) => {
|
||||||
|
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildHierarchy(node: any, includeComponents: boolean): any {
|
||||||
|
const nodeInfo: any = {
|
||||||
|
uuid: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
type: node.type,
|
||||||
|
active: node.active,
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeComponents && node.__comps__) {
|
||||||
|
nodeInfo.components = node.__comps__.map((comp: any) => ({
|
||||||
|
type: comp.__type__ || 'Unknown',
|
||||||
|
enabled: comp.enabled !== undefined ? comp.enabled : true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
nodeInfo.children = node.children.map((child: any) =>
|
||||||
|
this.buildHierarchy(child, includeComponents)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveSceneAs(path: string): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// save-as-scene API 不接受路径参数,会弹出对话框让用户选择
|
||||||
|
(Editor.Message.request as any)('scene', 'save-as-scene').then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
path: path,
|
||||||
|
message: `Scene save-as dialog opened`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async closeScene(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Editor.Message.request('scene', 'close-scene').then(() => {
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
message: 'Scene closed successfully'
|
||||||
|
});
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
247
source/tools/server-tools.ts
Normal file
247
source/tools/server-tools.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
|
||||||
|
|
||||||
|
export class ServerTools implements ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'get_server_info',
|
||||||
|
description: 'Get server information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'broadcast_custom_message',
|
||||||
|
description: 'Broadcast a custom message',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Message name'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
description: 'Message data (optional)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['message']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_editor_version',
|
||||||
|
description: 'Get editor version information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_project_name',
|
||||||
|
description: 'Get current project name',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_project_path',
|
||||||
|
description: 'Get current project path',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_project_uuid',
|
||||||
|
description: 'Get current project UUID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'restart_editor',
|
||||||
|
description: 'Request to restart the editor',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quit_editor',
|
||||||
|
description: 'Request to quit the editor',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'get_server_info':
|
||||||
|
return await this.getServerInfo();
|
||||||
|
case 'broadcast_custom_message':
|
||||||
|
return await this.broadcastCustomMessage(args.message, args.data);
|
||||||
|
case 'get_editor_version':
|
||||||
|
return await this.getEditorVersion();
|
||||||
|
case 'get_project_name':
|
||||||
|
return await this.getProjectName();
|
||||||
|
case 'get_project_path':
|
||||||
|
return await this.getProjectPath();
|
||||||
|
case 'get_project_uuid':
|
||||||
|
return await this.getProjectUuid();
|
||||||
|
case 'restart_editor':
|
||||||
|
return await this.restartEditor();
|
||||||
|
case 'quit_editor':
|
||||||
|
return await this.quitEditor();
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getServerInfo(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const info = {
|
||||||
|
editorVersion: (Editor as any).versions?.editor || 'Unknown',
|
||||||
|
cocosVersion: (Editor as any).versions?.cocos || 'Unknown',
|
||||||
|
nodeVersion: process.version,
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
projectName: Editor.Project.name,
|
||||||
|
projectPath: Editor.Project.path,
|
||||||
|
projectUuid: Editor.Project.uuid
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
server: info,
|
||||||
|
message: 'Server information retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async broadcastCustomMessage(message: string, data?: any): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
if (data !== undefined) {
|
||||||
|
Editor.Message.broadcast(message, data);
|
||||||
|
} else {
|
||||||
|
Editor.Message.broadcast(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: message,
|
||||||
|
data: data,
|
||||||
|
result: 'Message broadcasted successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getEditorVersion(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const version = {
|
||||||
|
editor: (Editor as any).versions?.editor || 'Unknown',
|
||||||
|
cocos: (Editor as any).versions?.cocos || 'Unknown',
|
||||||
|
node: process.version
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
version: version,
|
||||||
|
message: 'Editor version retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectName(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const name = Editor.Project.name;
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name: name,
|
||||||
|
message: 'Project name retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectPath(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const path = Editor.Project.path;
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
path: path,
|
||||||
|
message: 'Project path retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectUuid(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const uuid = Editor.Project.uuid;
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
uuid: uuid,
|
||||||
|
message: 'Project UUID retrieved successfully'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restartEditor(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Editor restart is not supported through MCP API',
|
||||||
|
instruction: 'Please restart the editor manually or use the editor menu: File > Restart Editor'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async quitEditor(): Promise<ToolResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Editor quit is not supported through MCP API',
|
||||||
|
instruction: 'Please quit the editor manually or use the editor menu: File > Quit'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
124
source/types/index.ts
Normal file
124
source/types/index.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
export interface MCPServerSettings {
|
||||||
|
port: number;
|
||||||
|
autoStart: boolean;
|
||||||
|
enableDebugLog: boolean;
|
||||||
|
allowedOrigins: string[];
|
||||||
|
maxConnections: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerStatus {
|
||||||
|
running: boolean;
|
||||||
|
port: number;
|
||||||
|
clients: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolResponse {
|
||||||
|
success: boolean;
|
||||||
|
data?: any;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
instruction?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeInfo {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
position?: { x: number; y: number; z: number };
|
||||||
|
rotation?: { x: number; y: number; z: number };
|
||||||
|
scale?: { x: number; y: number; z: number };
|
||||||
|
parent?: string;
|
||||||
|
children?: string[];
|
||||||
|
components?: ComponentInfo[];
|
||||||
|
layer?: number;
|
||||||
|
mobility?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentInfo {
|
||||||
|
type: string;
|
||||||
|
enabled: boolean;
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SceneInfo {
|
||||||
|
name: string;
|
||||||
|
uuid: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrefabInfo {
|
||||||
|
name: string;
|
||||||
|
uuid: string;
|
||||||
|
path: string;
|
||||||
|
folder: string;
|
||||||
|
createTime?: string;
|
||||||
|
modifyTime?: string;
|
||||||
|
dependencies?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetInfo {
|
||||||
|
name: string;
|
||||||
|
uuid: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
size?: number;
|
||||||
|
isDirectory: boolean;
|
||||||
|
meta?: {
|
||||||
|
ver: string;
|
||||||
|
importer: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectInfo {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
uuid: string;
|
||||||
|
version: string;
|
||||||
|
cocosVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsoleMessage {
|
||||||
|
timestamp: string;
|
||||||
|
type: 'log' | 'warn' | 'error' | 'info';
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceStats {
|
||||||
|
nodeCount: number;
|
||||||
|
componentCount: number;
|
||||||
|
drawCalls: number;
|
||||||
|
triangles: number;
|
||||||
|
memory: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationIssue {
|
||||||
|
type: 'error' | 'warning' | 'info';
|
||||||
|
category: string;
|
||||||
|
message: string;
|
||||||
|
details?: any;
|
||||||
|
suggestion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
issueCount: number;
|
||||||
|
issues: ValidationIssue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPClient {
|
||||||
|
id: string;
|
||||||
|
lastActivity: Date;
|
||||||
|
userAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolExecutor {
|
||||||
|
getTools(): ToolDefinition[];
|
||||||
|
execute(toolName: string, args: any): Promise<ToolResponse>;
|
||||||
|
}
|
||||||
BIN
static/icon.png
Normal file
BIN
static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 KiB |
65
static/style/default/index.css
Normal file
65
static/style/default/index.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.mcp-server-panel {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h2 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--color-normal-fill-emphasis);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.running {
|
||||||
|
color: var(--color-success-fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.stopped {
|
||||||
|
color: var(--color-warn-fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-controls ui-button {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-details {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui-prop {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
64
static/template/default/index.html
Normal file
64
static/template/default/index.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<div class="mcp-server-panel">
|
||||||
|
<header>
|
||||||
|
<h2 id="panelTitle"></h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="server-status">
|
||||||
|
<h3 id="serverStatusLabel"></h3>
|
||||||
|
<div class="status-info">
|
||||||
|
<ui-prop>
|
||||||
|
<ui-label id="serverStatusLabelProp" slot="label"></ui-label>
|
||||||
|
<ui-label id="serverStatusValue" slot="content" class="status-value" :class="statusClass"></ui-label>
|
||||||
|
</ui-prop>
|
||||||
|
<ui-prop v-if="serverRunning">
|
||||||
|
<ui-label id="connectedLabel" slot="label"></ui-label>
|
||||||
|
<ui-label id="connectedClients" slot="content"></ui-label>
|
||||||
|
</ui-prop>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="server-controls">
|
||||||
|
<ui-button id="toggleServerBtn" @click="toggleServer" :disabled="isProcessing" class="primary"></ui-button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="server-settings">
|
||||||
|
<h3 id="settingsLabel"></h3>
|
||||||
|
<ui-prop>
|
||||||
|
<ui-label id="portLabel" slot="label"></ui-label>
|
||||||
|
<ui-num-input id="portInput" slot="content"
|
||||||
|
:min="1024" :max="65535" :step="1"
|
||||||
|
:disabled="serverRunning">
|
||||||
|
</ui-num-input>
|
||||||
|
</ui-prop>
|
||||||
|
<ui-prop>
|
||||||
|
<ui-label id="autoStartLabel" slot="label"></ui-label>
|
||||||
|
<ui-checkbox id="autoStartInput" slot="content"></ui-checkbox>
|
||||||
|
</ui-prop>
|
||||||
|
<ui-prop>
|
||||||
|
<ui-label id="debugLogLabel" slot="label"></ui-label>
|
||||||
|
<ui-checkbox id="debugLogInput" slot="content"></ui-checkbox>
|
||||||
|
</ui-prop>
|
||||||
|
<ui-prop>
|
||||||
|
<ui-label id="maxConnectionsLabel" slot="label"></ui-label>
|
||||||
|
<ui-num-input id="maxConnInput" slot="content"
|
||||||
|
:min="1" :max="100" :step="1">
|
||||||
|
</ui-num-input>
|
||||||
|
</ui-prop>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="server-info" v-if="serverRunning">
|
||||||
|
<h3 id="connectionInfoLabel"></h3>
|
||||||
|
<div class="connection-details">
|
||||||
|
<ui-prop>
|
||||||
|
<ui-label id="httpUrlLabel" slot="label"></ui-label>
|
||||||
|
<ui-input id="httpUrlInput" slot="content" :value="httpUrl" readonly>
|
||||||
|
<ui-button id="copyBtn" slot="suffix" @click="copyUrl"></ui-button>
|
||||||
|
</ui-input>
|
||||||
|
</ui-prop>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<ui-button id="saveSettingsBtn" @click="saveSettings" :disabled="!settingsChanged"></ui-button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
11
tsconfig.json
Executable file
11
tsconfig.json
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./base.tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./source",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"@cocos/creator-types/editor",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user