This is page 1 of 5. Use http://codebase.md/daxianlee/cocos-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── @types
│ └── schema
│ └── package
│ ├── base
│ │ └── panels.json
│ ├── contributions
│ │ └── index.json
│ └── index.json
├── base.tsconfig.json
├── dist
│ ├── examples
│ │ └── prefab-instantiation-example.js
│ ├── main.js
│ ├── mcp-server.js
│ ├── panels
│ │ ├── default
│ │ │ └── index.js
│ │ └── tool-manager
│ │ └── index.js
│ ├── scene.js
│ ├── settings.js
│ ├── test
│ │ ├── manual-test.js
│ │ ├── mcp-tool-tester.js
│ │ ├── prefab-tools-test.js
│ │ └── tool-tester.js
│ ├── tools
│ │ ├── asset-advanced-tools.js
│ │ ├── broadcast-tools.js
│ │ ├── component-tools.js
│ │ ├── debug-tools.js
│ │ ├── node-tools.js
│ │ ├── prefab-tools.js
│ │ ├── preferences-tools.js
│ │ ├── project-tools.js
│ │ ├── reference-image-tools.js
│ │ ├── scene-advanced-tools.js
│ │ ├── scene-tools.js
│ │ ├── scene-view-tools.js
│ │ ├── server-tools.js
│ │ ├── tool-manager.js
│ │ └── validation-tools.js
│ └── types
│ └── index.js
├── FEATURE_GUIDE_CN.md
├── FEATURE_GUIDE_EN.md
├── i18n
│ ├── en.js
│ └── zh.js
├── image
│ ├── iamge2.png
│ └── image-20250717174157957.png
├── package-lock.json
├── package.json
├── README.EN.md
├── README.md
├── scripts
│ └── preinstall.js
├── source
│ ├── main.ts
│ ├── mcp-server.ts
│ ├── panels
│ │ ├── default
│ │ │ └── index.ts
│ │ └── tool-manager
│ │ └── index.ts
│ ├── scene.ts
│ ├── settings.ts
│ ├── test
│ │ ├── manual-test.ts
│ │ ├── mcp-tool-tester.ts
│ │ ├── prefab-tools-test.ts
│ │ └── tool-tester.ts
│ ├── tools
│ │ ├── asset-advanced-tools.ts
│ │ ├── broadcast-tools.ts
│ │ ├── component-tools.ts
│ │ ├── debug-tools.ts
│ │ ├── node-tools.ts
│ │ ├── prefab-tools.ts
│ │ ├── preferences-tools.ts
│ │ ├── project-tools.ts
│ │ ├── reference-image-tools.ts
│ │ ├── scene-advanced-tools.ts
│ │ ├── scene-tools.ts
│ │ ├── scene-view-tools.ts
│ │ ├── server-tools.ts
│ │ ├── tool-manager.ts
│ │ └── validation-tools.ts
│ └── types
│ └── index.ts
├── static
│ ├── icon.png
│ ├── style
│ │ └── default
│ │ └── index.css
│ └── template
│ ├── default
│ │ ├── index.html
│ │ └── tool-manager.html
│ └── vue
│ └── mcp-server-app.html
├── TestScript.js
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
```
--------------------------------------------------------------------------------
/README.EN.md:
--------------------------------------------------------------------------------
```markdown
1 | # Cocos Creator MCP Server Plugin
2 |
3 | **[📖 English](README.EN.md)** **[📖 中文](README.md)**
4 |
5 | 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.
6 |
7 | **🚀 Now provides 50 powerful integrated tools, achieving 99% editor control!**
8 |
9 | ## Video Demonstrations and Tutorials
10 |
11 | [<img width="503" height="351" alt="image" src="https://github.com/user-attachments/assets/f186ce14-9ffc-4a29-8761-48bdd7c1ea16" />](https://www.bilibili.com/video/BV1mB8dzfEw8?spm_id_from=333.788.recommend_more_video.0&vd_source=6b1ff659dd5f04a92cc6d14061e8bb92)
12 |
13 |
14 |
15 | ## Quick Links
16 |
17 | - **[📖 Complete Feature Guide (English)](FEATURE_GUIDE_EN.md)** - Detailed documentation for all 50 tools (to be completed)
18 | - **[📖 完整功能指南 (中文)](FEATURE_GUIDE_CN.md)** - All 50 tools detailed documentation (to be completed)
19 |
20 |
21 | ## Changelog
22 |
23 | ## 🚀 Major Update v1.5.0 (July 29, 2024) (Already updated in Cocos Store, GitHub version will be synchronized in next version)
24 |
25 | Cocos store: https://store.cocos.com/app/detail/7941
26 | If you don't want to purchase, join the communication group and I can send you the latest version directly!
27 |
28 | - **Tool Streamlining and Refactoring**: Condensed the original 150+ tools into 50 high-reuse, high-coverage core tools, removing all invalid redundant code, greatly improving usability and maintainability.
29 | - **Unified Operation Codes**: All tools adopt "operation code + parameters" mode, greatly simplifying AI calling process, improving AI calling success rate, reducing AI calling times, and lowering 50% token consumption.
30 | - **Comprehensive Prefab Function Upgrade**: Completely fixed and perfected all core prefab functions including creation, instantiation, synchronization, references, etc., supporting complex reference relationships, 100% aligned with official format.
31 | - **Event Binding and Legacy Function Completion**: Added and implemented event binding, node/component/asset legacy functions, all methods completely aligned with official implementation.
32 | - **Interface Optimization**: All interface parameters are clearer, documentation is more complete, AI can understand and call more easily.
33 | - **Plugin Panel Optimization**: Panel UI is more concise, operations are more intuitive.
34 | - **Performance and Compatibility Improvements**: Overall architecture is more efficient, compatible with Cocos Creator 3.8.6 and all versions above.
35 |
36 |
37 | ## Tool System and Operation Codes
38 |
39 | - All tools are named with "category_operation", parameters use unified Schema, support multiple operation code (action) switching, greatly improving flexibility and extensibility.
40 | - 50 core tools cover scene, node, component, prefab, asset, project, debugging, preferences, server, message broadcasting and all other editor operations.
41 | - Tool calling example:
42 |
43 | ```json
44 | {
45 | "tool": "node_lifecycle",
46 | "arguments": {
47 | "action": "create",
48 | "name": "MyNode",
49 | "parentUuid": "parent-uuid",
50 | "nodeType": "2DNode"
51 | }
52 | }
53 | ```
54 |
55 | ---
56 |
57 | ## Main Function Categories (Partial Examples)
58 |
59 | - **scene_management**: Scene management (get/open/save/create/close scenes)
60 | - **node_query / node_lifecycle / node_transform**: Node query, creation, deletion, property changes
61 | - **component_manage / component_script / component_query**: Component add/remove, script mounting, component information
62 | - **prefab_browse / prefab_lifecycle / prefab_instance**: Prefab browsing, creation, instantiation, synchronization
63 | - **asset_manage / asset_analyze**: Asset import, deletion, dependency analysis
64 | - **project_manage / project_build_system**: Project running, building, configuration information
65 | - **debug_console / debug_logs**: Console and log management
66 | - **preferences_manage**: Preferences settings
67 | - **server_info**: Server information
68 | - **broadcast_message**: Message broadcasting
69 |
70 |
71 | ### v1.4.0 - July 26, 2025 (Current github version)
72 |
73 | #### 🎯 Major Functionality Fixes
74 | - **Complete Prefab Creation Fix**: Thoroughly resolved the issue of component/node/resource type reference loss during prefab creation
75 | - **Proper Reference Handling**: Implemented reference formats completely consistent with manually created prefabs
76 | - **Internal References**: Node and component references within prefabs correctly converted to `{"__id__": x}` format
77 | - **External References**: Node and component references outside prefabs correctly set to `null`
78 | - **Resource References**: Prefab, texture, sprite frame and other resource references fully preserved in UUID format
79 | - **Component/Script Removal API Standardization**: Now, when removing a component or script, you must provide the component's cid (type field), not the script name or class name. AI and users should first use getComponents to get the type field (cid), then pass it to removeComponent. This ensures 100% accurate removal of all component and script types, compatible with all Cocos Creator versions.
80 |
81 | #### 🔧 Core Improvements
82 | - **Index Order Optimization**: Adjusted prefab object creation order to ensure consistency with Cocos Creator standard format
83 | - **Component Type Support**: Extended component reference detection to support all cc. prefixed component types (Label, Button, Sprite, etc.)
84 | - **UUID Mapping Mechanism**: Perfected internal UUID to index mapping system, ensuring correct reference relationships
85 | - **Property Format Standardization**: Fixed component property order and format, eliminating engine parsing errors
86 |
87 | #### 🐛 Bug Fixes
88 | - **Fixed Prefab Import Errors**: Resolved `Cannot read properties of undefined (reading '_name')` error
89 | - **Fixed Engine Compatibility**: Resolved `placeHolder.initDefault is not a function` error
90 | - **Fixed Property Overwriting**: Prevented critical properties like `_objFlags` from being overwritten by component data
91 | - **Fixed Reference Loss**: Ensured all types of references are correctly saved and loaded
92 |
93 | #### 📈 Feature Enhancements
94 | - **Complete Component Property Preservation**: All component properties including private properties (like _group, _density, etc.)
95 | - **Child Node Structure Support**: Proper handling of prefab hierarchical structures and child node relationships
96 | - **Transform Property Processing**: Preserved node position, rotation, scale, and layer information
97 | - **Debug Information Optimization**: Added detailed reference processing logs for easier issue tracking
98 |
99 | #### 💡 Technical Breakthroughs
100 | - **Reference Type Identification**: Intelligently distinguish between internal and external references, avoiding invalid references
101 | - **Format Compatibility**: Generated prefabs are 100% compatible with manually created prefab formats
102 | - **Engine Integration**: Prefabs can be properly mounted to scenes without any runtime errors
103 | - **Performance Optimization**: Optimized prefab creation workflow, improving processing efficiency for large prefabs
104 |
105 | **🎉 Prefab creation functionality is now fully operational, supporting complex component reference relationships and complete prefab structures!**
106 |
107 | ### v1.3.0 - July 25, 2024
108 |
109 | #### 🆕 New Features
110 | - **Integrated Tool Management Panel**: Added comprehensive tool management functionality directly into the main control panel
111 | - **Tool Configuration System**: Implemented selective tool enabling/disabling with persistent configurations
112 | - **Dynamic Tool Loading**: Enhanced tool discovery to dynamically load all 158 available tools from the MCP server
113 | - **Real-time Tool State Management**: Added real-time updates for tool counts and status when individual tools are toggled
114 | - **Configuration Persistence**: Automatic saving and loading of tool configurations across editor sessions
115 |
116 | #### 🔧 Improvements
117 | - **Unified Panel Interface**: Merged tool management into the main MCP server panel as a tab, eliminating the need for separate panels
118 | - **Enhanced Server Settings**: Improved server configuration management with better persistence and loading
119 | - **Vue 3 Integration**: Upgraded to Vue 3 Composition API for better reactivity and performance
120 | - **Better Error Handling**: Added comprehensive error handling with rollback mechanisms for failed operations
121 | - **Improved UI/UX**: Enhanced visual design with proper dividers, distinct block styles, and non-transparent modal backgrounds
122 |
123 | #### 🐛 Bug Fixes
124 | - **Fixed Tool State Persistence**: Resolved issues where tool states would reset upon tab switching or panel re-opening
125 | - **Fixed Configuration Loading**: Corrected server settings loading issues and message registration problems
126 | - **Fixed Checkbox Interactions**: Resolved checkbox unchecking issues and improved reactivity
127 | - **Fixed Panel Scrolling**: Ensured proper scrolling functionality in the tool management panel
128 | - **Fixed IPC Communication**: Resolved various IPC communication issues between frontend and backend
129 |
130 | #### 🏗️ Technical Improvements
131 | - **Simplified Architecture**: Removed multi-configuration complexity, focusing on single configuration management
132 | - **Better Type Safety**: Enhanced TypeScript type definitions and interfaces
133 | - **Improved Data Synchronization**: Better synchronization between frontend UI state and backend tool manager
134 | - **Enhanced Debugging**: Added comprehensive logging and debugging capabilities
135 |
136 | #### 📊 Statistics
137 | - **Total Tools**: Increased from 151 to 158 tools
138 | - **Categories**: 13 tool categories with comprehensive coverage
139 | - **Editor Control**: Achieved 98% editor functionality coverage
140 |
141 | ### v1.2.0 - Previous Version
142 | - Initial release with 151 tools
143 | - Basic MCP server functionality
144 | - Scene, node, component, and prefab operations
145 | - Project control and debugging tools
146 |
147 |
148 |
149 | ## Quick Usage
150 |
151 | **Claude CLI configuration:**
152 |
153 | ```
154 | claude mcp add --transport http cocos-creator http://127.0.0.1:3000/mcp (use your configured port number)
155 | ```
156 |
157 | **Claude client configuration:**
158 |
159 | ```
160 | {
161 |
162 | "mcpServers": {
163 |
164 | "cocos-creator": {
165 |
166 | "type": "http",
167 |
168 | "url": "http://127.0.0.1:3000/mcp"
169 |
170 | }
171 |
172 | }
173 |
174 | }
175 | ```
176 |
177 | **Cursor or VS class MCP configuration**
178 |
179 | ```
180 | {
181 |
182 | "mcpServers": {
183 |
184 | "cocos-creator": {
185 | "url": "http://localhost:3000/mcp"
186 | }
187 | }
188 |
189 | }
190 | ```
191 |
192 | ## Features
193 |
194 | ### 🎯 Scene Operations (scene_*)
195 | - **scene_management**: Scene management - Get current scene, open/save/create/close scenes, support scene list query
196 | - **scene_hierarchy**: Scene hierarchy - Get complete scene structure, support component information inclusion
197 | - **scene_execution_control**: Execution control - Execute component methods, scene scripts, prefab synchronization
198 |
199 | ### 🎮 Node Operations (node_*)
200 | - **node_query**: Node query - Find nodes by name/pattern, get node information, detect 2D/3D types
201 | - **node_lifecycle**: Node lifecycle - Create/delete nodes, support component pre-installation, prefab instantiation
202 | - **node_transform**: Node transform - Modify node name, position, rotation, scale, visibility and other properties
203 | - **node_hierarchy**: Node hierarchy - Move, copy, paste nodes, support hierarchical structure operations
204 | - **node_clipboard**: Node clipboard - Copy/paste/cut node operations
205 | - **node_property_management**: Property management - Reset node properties, component properties, transform properties
206 |
207 | ### 🔧 Component Operations (component_*)
208 | - **component_manage**: Component management - Add/remove engine components (cc.Sprite, cc.Button, etc.)
209 | - **component_script**: Script components - Mount/remove custom script components
210 | - **component_query**: Component query - Get component list, detailed information, available component types
211 | - **set_component_property**: Property setting - Set single or multiple component property values
212 |
213 | ### 📦 Prefab Operations (prefab_*)
214 | - **prefab_browse**: Prefab browsing - List prefabs, view information, validate files
215 | - **prefab_lifecycle**: Prefab lifecycle - Create prefabs from nodes, delete prefabs
216 | - **prefab_instance**: Prefab instances - Instantiate to scene, unlink, apply changes, restore original
217 | - **prefab_edit**: Prefab editing - Enter/exit edit mode, save prefabs, test changes
218 |
219 | ### 🚀 Project Control (project_*)
220 | - **project_manage**: Project management - Run project, build project, get project information and settings
221 | - **project_build_system**: Build system - Control build panel, check build status, preview server management
222 |
223 | ### 🔍 Debug Tools (debug_*)
224 | - **debug_console**: Console management - Get/clear console logs, support filtering and limiting
225 | - **debug_logs**: Log analysis - Read/search/analyze project log files, support pattern matching
226 | - **debug_system**: System debugging - Get editor information, performance statistics, environment information
227 |
228 | ### 📁 Asset Management (asset_*)
229 | - **asset_manage**: Asset management - Batch import/delete assets, save metadata, generate URLs
230 | - **asset_analyze**: Asset analysis - Get dependency relationships, export asset manifests
231 | - **asset_system**: Asset system - Refresh assets, query asset database status
232 | - **asset_query**: Asset query - Query assets by type/folder, get detailed information
233 | - **asset_operations**: Asset operations - Create/copy/move/delete/save/re-import assets
234 |
235 | ### ⚙️ Preferences (preferences_*)
236 | - **preferences_manage**: Preferences management - Get/set editor preferences
237 | - **preferences_global**: Global settings - Manage global configuration and system settings
238 |
239 | ### 🌐 Server and Broadcasting (server_* / broadcast_*)
240 | - **server_info**: Server information - Get server status, project details, environment information
241 | - **broadcast_message**: Message broadcasting - Listen and broadcast custom messages
242 |
243 | ### 🖼️ Reference Images (referenceImage_*)
244 | - **reference_image_manage**: Reference image management - Add/delete/manage reference images in scene view
245 | - **reference_image_view**: Reference image view - Control reference image display and editing
246 |
247 | ### 🎨 Scene View (sceneView_*)
248 | - **scene_view_control**: Scene view control - Control Gizmo tools, coordinate systems, view modes
249 | - **scene_view_tools**: Scene view tools - Manage various scene view tools and options
250 |
251 | ### ✅ Validation Tools (validation_*)
252 | - **validation_scene**: Scene validation - Validate scene integrity, check missing assets
253 | - **validation_asset**: Asset validation - Validate asset references, check asset integrity
254 |
255 | ### 🛠️ Tool Management
256 | - **Tool Configuration System**: Selectively enable/disable tools, support multiple configurations
257 | - **Configuration Persistence**: Automatically save and load tool configurations
258 | - **Configuration Import/Export**: Support tool configuration import/export functionality
259 | - **Real-time State Management**: Real-time tool state updates and synchronization
260 |
261 | ### 🚀 Core Advantages
262 | - **Unified Operation Codes**: All tools adopt "category_operation" naming, unified parameter Schema
263 | - **High Reusability**: 50 core tools cover 99% editor functionality
264 | - **AI-Friendly**: Clear parameters, complete documentation, simple calling
265 | - **Performance Optimization**: Reduce 50% token consumption, improve AI calling success rate
266 | - **Complete Compatibility**: 100% aligned with Cocos Creator official API
267 |
268 | ## Installation
269 |
270 | ### 1. Copy Plugin Files
271 |
272 | Copy the entire `cocos-mcp-server` folder to your Cocos Creator project's `extensions` directory, or you can directly import the project in the extension manager:
273 |
274 | ```
275 | YourProject/
276 | ├── assets/
277 | ├── extensions/
278 | │ └── cocos-mcp-server/ <- Place plugin here
279 | │ ├── source/
280 | │ ├── dist/
281 | │ ├── package.json
282 | │ └── ...
283 | ├── settings/
284 | └── ...
285 | ```
286 |
287 | ### 2. Install Dependencies
288 |
289 | ```bash
290 | cd extensions/cocos-mcp-server
291 | npm install
292 | ```
293 |
294 | ### 3. Build the Plugin
295 |
296 | ```bash
297 | npm run build
298 | ```
299 |
300 | ### 4. Enable Plugin
301 |
302 | 1. Restart Cocos Creator or refresh extensions
303 | 2. The plugin will appear in the Extension menu
304 | 3. Click `Extension > Cocos MCP Server` to open the control panel
305 |
306 | ## Usage
307 |
308 | ### Starting the Server
309 |
310 | 1. Open the MCP Server panel from `Extension > Cocos MCP Server`
311 | 2. Configure settings:
312 | - **Port**: HTTP server port (default: 3000)
313 | - **Auto Start**: Automatically start server when editor opens
314 | - **Debug Logging**: Enable detailed logging for development
315 | - **Max Connections**: Maximum concurrent connections allowed
316 |
317 | 3. Click "Start Server" to begin accepting connections
318 |
319 | ### Connecting AI Assistants
320 |
321 | The server exposes an HTTP endpoint at `http://localhost:3000/mcp` (or your configured port).
322 |
323 | AI assistants can connect using the MCP protocol and access all available tools.
324 |
325 |
326 | ## Development
327 |
328 | ### Project Structure
329 | ```
330 | cocos-mcp-server/
331 | ├── source/ # TypeScript source files
332 | │ ├── main.ts # Plugin entry point
333 | │ ├── mcp-server.ts # MCP server implementation
334 | │ ├── settings.ts # Settings management
335 | │ ├── types/ # TypeScript type definitions
336 | │ ├── tools/ # Tool implementations
337 | │ │ ├── scene-tools.ts
338 | │ │ ├── node-tools.ts
339 | │ │ ├── component-tools.ts
340 | │ │ ├── prefab-tools.ts
341 | │ │ ├── project-tools.ts
342 | │ │ ├── debug-tools.ts
343 | │ │ ├── preferences-tools.ts
344 | │ │ ├── server-tools.ts
345 | │ │ ├── broadcast-tools.ts
346 | │ │ ├── scene-advanced-tools.ts (integrated into node-tools.ts and scene-tools.ts)
347 | │ │ ├── scene-view-tools.ts
348 | │ │ ├── reference-image-tools.ts
349 | │ │ └── asset-advanced-tools.ts
350 | │ ├── panels/ # UI panel implementation
351 | │ └── test/ # Test files
352 | ├── dist/ # Compiled JavaScript output
353 | ├── static/ # Static assets (icons, etc.)
354 | ├── i18n/ # Internationalization files
355 | ├── package.json # Plugin configuration
356 | └── tsconfig.json # TypeScript configuration
357 | ```
358 |
359 | ### Building from Source
360 |
361 | ```bash
362 | # Install dependencies
363 | npm install
364 |
365 | # Build for development with watch mode
366 | npm run watch
367 |
368 | # Build for production
369 | npm run build
370 | ```
371 |
372 | ### Adding New Tools
373 |
374 | 1. Create a new tool class in `source/tools/`
375 | 2. Implement the `ToolExecutor` interface
376 | 3. Add tool to `mcp-server.ts` initialization
377 | 4. Tools are automatically exposed via MCP protocol
378 |
379 | ### TypeScript Support
380 |
381 | The plugin is fully written in TypeScript with:
382 | - Strict type checking enabled
383 | - Comprehensive type definitions for all APIs
384 | - IntelliSense support for development
385 | - Automatic compilation to JavaScript
386 |
387 | ## Troubleshooting
388 |
389 | ### Common Issues
390 |
391 | 1. **Server won't start**: Check port availability and firewall settings
392 | 2. **Tools not working**: Ensure scene is loaded and UUIDs are valid
393 | 3. **Build errors**: Run `npm run build` to check for TypeScript errors
394 | 4. **Connection issues**: Verify HTTP URL and server status
395 |
396 | ### Debug Mode
397 |
398 | Enable debug logging in the plugin panel for detailed operation logs.
399 |
400 | ### Using Debug Tools
401 |
402 | ```json
403 | {
404 | "tool": "debug_get_console_logs",
405 | "arguments": {"limit": 50, "filter": "error"}
406 | }
407 | ```
408 |
409 | ```json
410 | {
411 | "tool": "debug_validate_scene",
412 | "arguments": {"checkMissingAssets": true}
413 | }
414 | ```
415 |
416 | ## Requirements
417 |
418 | - Cocos Creator 3.8.6 or later
419 | - Node.js (bundled with Cocos Creator)
420 | - TypeScript (installed as dev dependency)
421 |
422 | ## License
423 |
424 | This plug-in is for Cocos Creator project use, 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.
425 |
426 | ## Contact me to join the group
427 | <img src="https://github.com/user-attachments/assets/2e3f043a-0b03-4b27-a175-e9c31fbed981" width="400" height="400"/>
428 |
429 | <img src="https://github.com/user-attachments/assets/5ef6172c-2968-499e-9edf-7da133016cd2" width="400" height="400"/>
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./base.tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./source",
6 | "types": [
7 | "node",
8 | "@cocos/creator-types/editor",
9 | ]
10 | }
11 | }
```
--------------------------------------------------------------------------------
/base.tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ES2017",
5 | "module": "CommonJS",
6 | "moduleResolution": "node",
7 | "inlineSourceMap": true,
8 | "inlineSources": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "experimentalDecorators": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "resolveJsonModule": true,
15 | "outDir": "./dist",
16 | "rootDir": "./source",
17 | "types": [
18 | "node",
19 | "@cocos/creator-types/editor",
20 | ]
21 | }
22 | }
```
--------------------------------------------------------------------------------
/@types/schema/package/contributions/index.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "description": "其他扩展插件的扩展配置 / Extended configuration for other extension plugins",
5 | "properties": {
6 | "menu": {
7 | "type": "array",
8 | "description": "菜单配置 / Menu configuration",
9 | "items": {
10 | "type": "object",
11 | "properties": {
12 | "path": {
13 | "type": "string",
14 | "description": "菜单路径 / Menu path"
15 | },
16 | "label": {
17 | "type": "string",
18 | "description": "菜单标签 / Menu label"
19 | },
20 | "message": {
21 | "type": "string",
22 | "description": "菜单消息 / Menu message"
23 | }
24 | }
25 | }
26 | },
27 | "messages": {
28 | "type": "object",
29 | "description": "消息配置 / Message configuration",
30 | "additionalProperties": {
31 | "type": "object",
32 | "properties": {
33 | "methods": {
34 | "type": "array",
35 | "items": {
36 | "type": "string"
37 | }
38 | }
39 | }
40 | }
41 | },
42 | "panels": {
43 | "$ref": "../base/panels.json"
44 | }
45 | },
46 | "required": []
47 | }
48 |
```
--------------------------------------------------------------------------------
/source/types/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface MCPServerSettings {
2 | port: number;
3 | autoStart: boolean;
4 | enableDebugLog: boolean;
5 | allowedOrigins: string[];
6 | maxConnections: number;
7 | }
8 |
9 | export interface ServerStatus {
10 | running: boolean;
11 | port: number;
12 | clients: number;
13 | }
14 |
15 | export interface ToolDefinition {
16 | name: string;
17 | description: string;
18 | inputSchema: any;
19 | }
20 |
21 | export interface ToolResponse {
22 | success: boolean;
23 | data?: any;
24 | message?: string;
25 | error?: string;
26 | instruction?: string;
27 | warning?: string;
28 | verificationData?: any;
29 | updatedProperties?: string[];
30 | }
31 |
32 | export interface NodeInfo {
33 | uuid: string;
34 | name: string;
35 | active: boolean;
36 | position?: { x: number; y: number; z: number };
37 | rotation?: { x: number; y: number; z: number };
38 | scale?: { x: number; y: number; z: number };
39 | parent?: string;
40 | children?: string[];
41 | components?: ComponentInfo[];
42 | layer?: number;
43 | mobility?: number;
44 | }
45 |
46 | export interface ComponentInfo {
47 | type: string;
48 | enabled: boolean;
49 | properties?: Record<string, any>;
50 | }
51 |
52 | export interface SceneInfo {
53 | name: string;
54 | uuid: string;
55 | path: string;
56 | }
57 |
58 | export interface PrefabInfo {
59 | name: string;
60 | uuid: string;
61 | path: string;
62 | folder: string;
63 | createTime?: string;
64 | modifyTime?: string;
65 | dependencies?: string[];
66 | }
67 |
68 | export interface AssetInfo {
69 | name: string;
70 | uuid: string;
71 | path: string;
72 | type: string;
73 | size?: number;
74 | isDirectory: boolean;
75 | meta?: {
76 | ver: string;
77 | importer: string;
78 | };
79 | }
80 |
81 | export interface ProjectInfo {
82 | name: string;
83 | path: string;
84 | uuid: string;
85 | version: string;
86 | cocosVersion: string;
87 | }
88 |
89 | export interface ConsoleMessage {
90 | timestamp: string;
91 | type: 'log' | 'warn' | 'error' | 'info';
92 | message: string;
93 | stack?: string;
94 | }
95 |
96 | export interface PerformanceStats {
97 | nodeCount: number;
98 | componentCount: number;
99 | drawCalls: number;
100 | triangles: number;
101 | memory: Record<string, any>;
102 | }
103 |
104 | export interface ValidationIssue {
105 | type: 'error' | 'warning' | 'info';
106 | category: string;
107 | message: string;
108 | details?: any;
109 | suggestion?: string;
110 | }
111 |
112 | export interface ValidationResult {
113 | valid: boolean;
114 | issueCount: number;
115 | issues: ValidationIssue[];
116 | }
117 |
118 | export interface MCPClient {
119 | id: string;
120 | lastActivity: Date;
121 | userAgent?: string;
122 | }
123 |
124 | export interface ToolExecutor {
125 | getTools(): ToolDefinition[];
126 | execute(toolName: string, args: any): Promise<ToolResponse>;
127 | }
128 |
129 | // 工具配置管理相关接口
130 | export interface ToolConfig {
131 | category: string;
132 | name: string;
133 | enabled: boolean;
134 | description: string;
135 | }
136 |
137 | export interface ToolConfiguration {
138 | id: string;
139 | name: string;
140 | description?: string;
141 | tools: ToolConfig[];
142 | createdAt: string;
143 | updatedAt: string;
144 | }
145 |
146 | export interface ToolManagerSettings {
147 | configurations: ToolConfiguration[];
148 | currentConfigId: string;
149 | maxConfigSlots: number;
150 | }
151 |
152 | export interface ToolManagerState {
153 | availableTools: ToolConfig[];
154 | currentConfiguration: ToolConfiguration | null;
155 | configurations: ToolConfiguration[];
156 | }
```
--------------------------------------------------------------------------------
/source/settings.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import { MCPServerSettings, ToolManagerSettings, ToolConfiguration, ToolConfig } from './types';
4 |
5 | const DEFAULT_SETTINGS: MCPServerSettings = {
6 | port: 3000,
7 | autoStart: false,
8 | enableDebugLog: false,
9 | allowedOrigins: ['*'],
10 | maxConnections: 10
11 | };
12 |
13 | const DEFAULT_TOOL_MANAGER_SETTINGS: ToolManagerSettings = {
14 | configurations: [],
15 | currentConfigId: '',
16 | maxConfigSlots: 5
17 | };
18 |
19 | function getSettingsPath(): string {
20 | return path.join(Editor.Project.path, 'settings', 'mcp-server.json');
21 | }
22 |
23 | function getToolManagerSettingsPath(): string {
24 | return path.join(Editor.Project.path, 'settings', 'tool-manager.json');
25 | }
26 |
27 | function ensureSettingsDir(): void {
28 | const settingsDir = path.dirname(getSettingsPath());
29 | if (!fs.existsSync(settingsDir)) {
30 | fs.mkdirSync(settingsDir, { recursive: true });
31 | }
32 | }
33 |
34 | export function readSettings(): MCPServerSettings {
35 | try {
36 | ensureSettingsDir();
37 | const settingsFile = getSettingsPath();
38 | if (fs.existsSync(settingsFile)) {
39 | const content = fs.readFileSync(settingsFile, 'utf8');
40 | return { ...DEFAULT_SETTINGS, ...JSON.parse(content) };
41 | }
42 | } catch (e) {
43 | console.error('Failed to read settings:', e);
44 | }
45 | return DEFAULT_SETTINGS;
46 | }
47 |
48 | export function saveSettings(settings: MCPServerSettings): void {
49 | try {
50 | ensureSettingsDir();
51 | const settingsFile = getSettingsPath();
52 | fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
53 | } catch (e) {
54 | console.error('Failed to save settings:', e);
55 | throw e;
56 | }
57 | }
58 |
59 | // 工具管理器设置相关函数
60 | export function readToolManagerSettings(): ToolManagerSettings {
61 | try {
62 | ensureSettingsDir();
63 | const settingsFile = getToolManagerSettingsPath();
64 | if (fs.existsSync(settingsFile)) {
65 | const content = fs.readFileSync(settingsFile, 'utf8');
66 | return { ...DEFAULT_TOOL_MANAGER_SETTINGS, ...JSON.parse(content) };
67 | }
68 | } catch (e) {
69 | console.error('Failed to read tool manager settings:', e);
70 | }
71 | return DEFAULT_TOOL_MANAGER_SETTINGS;
72 | }
73 |
74 | export function saveToolManagerSettings(settings: ToolManagerSettings): void {
75 | try {
76 | ensureSettingsDir();
77 | const settingsFile = getToolManagerSettingsPath();
78 | fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
79 | } catch (e) {
80 | console.error('Failed to save tool manager settings:', e);
81 | throw e;
82 | }
83 | }
84 |
85 | export function exportToolConfiguration(config: ToolConfiguration): string {
86 | return JSON.stringify(config, null, 2);
87 | }
88 |
89 | export function importToolConfiguration(configJson: string): ToolConfiguration {
90 | try {
91 | const config = JSON.parse(configJson);
92 | // 验证配置格式
93 | if (!config.id || !config.name || !Array.isArray(config.tools)) {
94 | throw new Error('Invalid configuration format');
95 | }
96 | return config;
97 | } catch (e) {
98 | console.error('Failed to parse tool configuration:', e);
99 | throw new Error('Invalid JSON format or configuration structure');
100 | }
101 | }
102 |
103 | export { DEFAULT_SETTINGS, DEFAULT_TOOL_MANAGER_SETTINGS };
```
--------------------------------------------------------------------------------
/@types/schema/package/base/panels.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "description": "面板数据 / Panel data",
5 | "additionalProperties": false,
6 | "patternProperties": {
7 | "^[a-zA-Z0-9_-]+$": {
8 | "type": "object",
9 | "description": "面板名 / Panel name",
10 | "properties": {
11 | "title": {
12 | "type": "string",
13 | "default": "Default Panel",
14 | "description": "面板标题,支持 i18n:key / Panel title, support for i18n:key (required)"
15 | },
16 | "main": {
17 | "type": "string",
18 | "default": "dist/panels/default/index.js",
19 | "description": "入口函数 / Entry function (required)"
20 | },
21 | "icon": {
22 | "type": "string",
23 | "description": "面板图标存放相对目录 / Relative directory for panel icon storage"
24 | },
25 | "type": {
26 | "type": "string",
27 | "enum": ["dockable", "simple"],
28 | "default": "dockable",
29 | "description": "面板类型(dockable | simple) / Panel type (dockable | simple)"
30 | },
31 | "flags": {
32 | "type": "object",
33 | "properties": {
34 | "resizable": {
35 | "type": "boolean",
36 | "default": true,
37 | "description": "是否可以改变大小,默认 true / Whether the size can be changed, default true"
38 | },
39 | "save": {
40 | "type": "boolean",
41 | "default": true,
42 | "description": "是否需要保存,默认 false / Whether to save, default false"
43 | },
44 | "alwaysOnTop": {
45 | "type": "boolean",
46 | "default": true,
47 | "description": "是否保持顶层显示,默认 false / Whether to keep the top level display, default false"
48 | }
49 | }
50 | },
51 | "size": {
52 | "type": "object",
53 | "description": "面板大小信息 / Panel size information",
54 | "properties": {
55 | "min-width": {
56 | "type": "number",
57 | "default": 200,
58 | "description": "面板最小宽度 / Minimum panel width"
59 | },
60 | "min-height": {
61 | "type": "number",
62 | "default": 200,
63 | "description": "面板最小高度 / Minimum panel height"
64 | },
65 | "width": {
66 | "type": "number",
67 | "default": 400,
68 | "description": " 面板默认宽度 / Panel Default Width"
69 | },
70 | "height": {
71 | "type": "number",
72 | "default": 600,
73 | "description": "面板默认高度 / Panel Default Height"
74 | }
75 | }
76 | }
77 | },
78 | "required": ["title", "main"]
79 | }
80 | }
81 | }
82 |
```
--------------------------------------------------------------------------------
/i18n/en.js:
--------------------------------------------------------------------------------
```javascript
1 | "use strict";
2 |
3 | module.exports = {
4 | "extension_name": "Cocos MCP Server",
5 | "description": "AI MCP Server for Cocos Creator 3.8",
6 | "panel_title": "MCP Server",
7 | "open_panel": "Open MCP Panel",
8 | "start_server": "Start Server",
9 | "stop_server": "Stop Server",
10 | "server_status": "Server Status",
11 | "port": "Port",
12 | "settings": "Settings",
13 | "connected": "Connected",
14 | "disconnected": "Disconnected",
15 | "server_running": "Server is running on port {0}",
16 | "server_stopped": "Server has stopped",
17 | "auto_start": "Auto Start",
18 | "debug_log": "Debug Logging",
19 | "max_connections": "Max Connections",
20 | "connection_info": "Connection Info",
21 | "http_url": "HTTP URL",
22 | "copy": "Copy",
23 | "save_settings": "Save Settings",
24 | "settings_saved": "Settings saved successfully",
25 | "server_started": "MCP Server Started",
26 | "server_stopped_msg": "MCP Server Stopped",
27 | "failed_to_start": "Failed to start server",
28 | "failed_to_stop": "Failed to stop server",
29 | "failed_to_save": "Failed to save settings",
30 | "url_copied": "HTTP URL copied to clipboard",
31 | "tool_manager": "Tool Manager",
32 | "open_tool_manager": "Open Tool Manager",
33 | "create_config": "Create Configuration",
34 | "edit_config": "Edit Configuration",
35 | "delete_config": "Delete Configuration",
36 | "import_config": "Import Configuration",
37 | "export_config": "Export Configuration",
38 | "apply_config": "Apply Configuration",
39 | "select_all": "Select All",
40 | "deselect_all": "Deselect All",
41 | "save_changes": "Save Changes",
42 | "config_name": "Configuration Name",
43 | "config_description": "Configuration Description",
44 | "current_config": "Current Configuration",
45 | "tool_management": "Tool Management",
46 | "total_tools": "Total Tools",
47 | "enabled_tools": "Enabled",
48 | "disabled_tools": "Disabled",
49 | "no_config_selected": "No Configuration Selected",
50 | "select_config_first": "Please select a configuration or create a new one first",
51 | "config_created": "Configuration created successfully",
52 | "config_updated": "Configuration updated successfully",
53 | "config_deleted": "Configuration deleted successfully",
54 | "config_applied": "Configuration applied successfully",
55 | "config_exported": "Configuration exported successfully",
56 | "config_imported": "Configuration imported successfully",
57 | "confirm_delete": "Confirm Delete",
58 | "delete_config_confirm": "Are you sure you want to delete configuration \"{0}\"? This action cannot be undone.",
59 | "max_config_slots_reached": "Maximum configuration slots reached ({0})",
60 | "invalid_config_format": "Invalid configuration format",
61 | "invalid_json_format": "Invalid JSON format or configuration structure",
62 | "server_tab": "Server",
63 | "tools_tab": "Tool Management",
64 | "available_tools": "Available Tools",
65 | "scene_tools": "Scene Tools",
66 | "node_tools": "Node Tools",
67 | "component_tools": "Component Tools",
68 | "prefab_tools": "Prefab Tools",
69 | "project_tools": "Project Tools",
70 | "debug_tools": "Debug Tools",
71 | "preferences_tools": "Preferences",
72 | "server_tools": "Server Tools",
73 | "broadcast_tools": "Broadcast Tools",
74 | "scene_advanced_tools": "Advanced Scene Tools",
75 | "scene_view_tools": "Scene View Tools",
76 | "reference_image_tools": "Reference Image Tools",
77 | "asset_advanced_tools": "Advanced Asset Tools",
78 | "validation_tools": "Validation Tools"
79 | };
```
--------------------------------------------------------------------------------
/source/test/manual-test.ts:
--------------------------------------------------------------------------------
```typescript
1 | declare const Editor: any;
2 |
3 | /**
4 | * 手动测试脚本
5 | * 可以在 Cocos Creator 控制台中执行测试
6 | */
7 |
8 | export async function testSceneTools() {
9 | console.log('=== Testing Scene Tools ===');
10 |
11 | try {
12 | // 1. 获取场景信息
13 | console.log('1. Getting scene info...');
14 | const sceneInfo = await Editor.Message.request('scene', 'get-scene-info');
15 | console.log('Scene info:', sceneInfo);
16 |
17 | // 2. 创建节点
18 | console.log('\n2. Creating test node...');
19 | const createResult = await Editor.Message.request('scene', 'create-node', {
20 | name: 'TestNode_' + Date.now(),
21 | type: 'cc.Node'
22 | });
23 | console.log('Create result:', createResult);
24 |
25 | if (createResult && createResult.uuid) {
26 | const nodeUuid = createResult.uuid;
27 |
28 | // 3. 查询节点
29 | console.log('\n3. Querying node...');
30 | const nodeInfo = await Editor.Message.request('scene', 'query-node', {
31 | uuid: nodeUuid
32 | });
33 | console.log('Node info:', nodeInfo);
34 |
35 | // 4. 设置节点属性
36 | console.log('\n4. Setting node position...');
37 | await Editor.Message.request('scene', 'set-node-property', {
38 | uuid: nodeUuid,
39 | path: 'position',
40 | value: { x: 100, y: 200, z: 0 }
41 | });
42 | console.log('Position set successfully');
43 |
44 | // 5. 添加组件
45 | console.log('\n5. Adding Sprite component...');
46 | const addCompResult = await Editor.Message.request('scene', 'add-component', {
47 | uuid: nodeUuid,
48 | component: 'cc.Sprite'
49 | });
50 | console.log('Component added:', addCompResult);
51 |
52 | // 6. 查询组件
53 | console.log('\n6. Querying component...');
54 | const compInfo = await Editor.Message.request('scene', 'query-node-component', {
55 | uuid: nodeUuid,
56 | component: 'cc.Sprite'
57 | });
58 | console.log('Component info:', compInfo);
59 |
60 | // 7. 删除节点
61 | console.log('\n7. Removing test node...');
62 | await Editor.Message.request('scene', 'remove-node', {
63 | uuid: nodeUuid
64 | });
65 | console.log('Node removed successfully');
66 | }
67 |
68 | } catch (error) {
69 | console.error('Test failed:', error);
70 | }
71 | }
72 |
73 | export async function testAssetTools() {
74 | console.log('\n=== Testing Asset Tools ===');
75 |
76 | try {
77 | // 1. 查询资源
78 | console.log('1. Querying image assets...');
79 | const assets = await Editor.Message.request('asset-db', 'query-assets', {
80 | pattern: '**/*.png',
81 | ccType: 'cc.ImageAsset'
82 | });
83 | console.log('Found assets:', assets?.length || 0);
84 |
85 | // 2. 获取资源信息
86 | console.log('\n2. Getting asset database info...');
87 | const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', {
88 | uuid: 'db://assets'
89 | });
90 | console.log('Asset info:', assetInfo);
91 |
92 | } catch (error) {
93 | console.error('Test failed:', error);
94 | }
95 | }
96 |
97 | export async function testProjectTools() {
98 | console.log('\n=== Testing Project Tools ===');
99 |
100 | try {
101 | // 1. 获取项目信息
102 | console.log('1. Getting project info...');
103 | const projectInfo = await Editor.Message.request('project', 'query-info');
104 | console.log('Project info:', projectInfo);
105 |
106 | // 2. 检查构建能力
107 | console.log('\n2. Checking build capability...');
108 | const canBuild = await Editor.Message.request('project', 'can-build');
109 | console.log('Can build:', canBuild);
110 |
111 | } catch (error) {
112 | console.error('Test failed:', error);
113 | }
114 | }
115 |
116 | export async function runAllTests() {
117 | console.log('Starting MCP Server Tools Test...\n');
118 |
119 | await testSceneTools();
120 | await testAssetTools();
121 | await testProjectTools();
122 |
123 | console.log('\n=== All tests completed ===');
124 | }
125 |
126 | // 导出到全局,方便在控制台调用
127 | (global as any).MCPTest = {
128 | testSceneTools,
129 | testAssetTools,
130 | testProjectTools,
131 | runAllTests
132 | };
```
--------------------------------------------------------------------------------
/TestScript.js:
--------------------------------------------------------------------------------
```javascript
1 | "use strict";
2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6 | return c > 3 && r && Object.defineProperty(target, key, r), r;
7 | };
8 | Object.defineProperty(exports, "__esModule", { value: true });
9 | exports.TestScript = void 0;
10 | const cc_1 = require("cc");
11 | const { ccclass, property } = cc_1._decorator;
12 | let TestScript = class TestScript extends cc_1.Component {
13 | constructor() {
14 | super(...arguments);
15 | this.testString = "Hello World";
16 | this.testNumber = 100;
17 | this.testBoolean = true;
18 | this.targetNode = null;
19 | }
20 | start() {
21 | console.log('TestScript started with:', {
22 | testString: this.testString,
23 | testNumber: this.testNumber,
24 | testBoolean: this.testBoolean,
25 | targetNode: this.targetNode
26 | });
27 | }
28 | update(deltaTime) {
29 | }
30 | };
31 | exports.TestScript = TestScript;
32 | __decorate([
33 | property({
34 | displayName: "测试字符串"
35 | })
36 | ], TestScript.prototype, "testString", void 0);
37 | __decorate([
38 | property({
39 | displayName: "测试数字"
40 | })
41 | ], TestScript.prototype, "testNumber", void 0);
42 | __decorate([
43 | property({
44 | displayName: "测试布尔值"
45 | })
46 | ], TestScript.prototype, "testBoolean", void 0);
47 | __decorate([
48 | property(cc_1.Node)
49 | ], TestScript.prototype, "targetNode", void 0);
50 | exports.TestScript = TestScript = __decorate([
51 | ccclass('TestScript')
52 | ], TestScript);
53 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiVGVzdFNjcmlwdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIlRlc3RTY3JpcHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7O0FBQUEsMkJBQWlEO0FBQ2pELE1BQU0sRUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLEdBQUcsZUFBVSxDQUFDO0FBR2xDLElBQU0sVUFBVSxHQUFoQixNQUFNLFVBQVcsU0FBUSxjQUFTO0lBQWxDOztRQUlJLGVBQVUsR0FBVyxhQUFhLENBQUM7UUFLbkMsZUFBVSxHQUFXLEdBQUcsQ0FBQztRQUt6QixnQkFBVyxHQUFZLElBQUksQ0FBQztRQUc1QixlQUFVLEdBQWdCLElBQUksQ0FBQztJQWMxQyxDQUFDO0lBWkcsS0FBSztRQUNELE9BQU8sQ0FBQyxHQUFHLENBQUMsMEJBQTBCLEVBQUU7WUFDcEMsVUFBVSxFQUFFLElBQUksQ0FBQyxVQUFVO1lBQzNCLFVBQVUsRUFBRSxJQUFJLENBQUMsVUFBVTtZQUMzQixXQUFXLEVBQUUsSUFBSSxDQUFDLFdBQVc7WUFDN0IsVUFBVSxFQUFFLElBQUksQ0FBQyxVQUFVO1NBQzlCLENBQUMsQ0FBQztJQUNQLENBQUM7SUFFRCxNQUFNLENBQUMsU0FBaUI7SUFFeEIsQ0FBQztDQUNKLENBQUE7QUEvQlksZ0NBQVU7QUFJWjtJQUhOLFFBQVEsQ0FBQztRQUNOLFdBQVcsRUFBRSxPQUFPO0tBQ3ZCLENBQUM7OENBQ3dDO0FBS25DO0lBSE4sUUFBUSxDQUFDO1FBQ04sV0FBVyxFQUFFLE1BQU07S0FDdEIsQ0FBQzs4Q0FDOEI7QUFLekI7SUFITixRQUFRLENBQUM7UUFDTixXQUFXLEVBQUUsT0FBTztLQUN2QixDQUFDOytDQUNpQztBQUc1QjtJQUROLFFBQVEsQ0FBQyxTQUFJLENBQUM7OENBQ3VCO3FCQWpCN0IsVUFBVTtJQUR0QixPQUFPLENBQUMsWUFBWSxDQUFDO0dBQ1QsVUFBVSxDQStCdEIiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBfZGVjb3JhdG9yLCBDb21wb25lbnQsIE5vZGUgfSBmcm9tICdjYyc7XG5jb25zdCB7IGNjY2xhc3MsIHByb3BlcnR5IH0gPSBfZGVjb3JhdG9yO1xuXG5AY2NjbGFzcygnVGVzdFNjcmlwdCcpXG5leHBvcnQgY2xhc3MgVGVzdFNjcmlwdCBleHRlbmRzIENvbXBvbmVudCB7XG4gICAgQHByb3BlcnR5KHtcbiAgICAgICAgZGlzcGxheU5hbWU6IFwi5rWL6K+V5a2X56ym5LiyXCJcbiAgICB9KVxuICAgIHB1YmxpYyB0ZXN0U3RyaW5nOiBzdHJpbmcgPSBcIkhlbGxvIFdvcmxkXCI7XG4gICAgXG4gICAgQHByb3BlcnR5KHtcbiAgICAgICAgZGlzcGxheU5hbWU6IFwi5rWL6K+V5pWw5a2XXCJcbiAgICB9KVxuICAgIHB1YmxpYyB0ZXN0TnVtYmVyOiBudW1iZXIgPSAxMDA7XG4gICAgXG4gICAgQHByb3BlcnR5KHtcbiAgICAgICAgZGlzcGxheU5hbWU6IFwi5rWL6K+V5biD5bCU5YC8XCJcbiAgICB9KVxuICAgIHB1YmxpYyB0ZXN0Qm9vbGVhbjogYm9vbGVhbiA9IHRydWU7XG4gICAgXG4gICAgQHByb3BlcnR5KE5vZGUpXG4gICAgcHVibGljIHRhcmdldE5vZGU6IE5vZGUgfCBudWxsID0gbnVsbDtcblxuICAgIHN0YXJ0KCkge1xuICAgICAgICBjb25zb2xlLmxvZygnVGVzdFNjcmlwdCBzdGFydGVkIHdpdGg6Jywge1xuICAgICAgICAgICAgdGVzdFN0cmluZzogdGhpcy50ZXN0U3RyaW5nLFxuICAgICAgICAgICAgdGVzdE51bWJlcjogdGhpcy50ZXN0TnVtYmVyLFxuICAgICAgICAgICAgdGVzdEJvb2xlYW46IHRoaXMudGVzdEJvb2xlYW4sXG4gICAgICAgICAgICB0YXJnZXROb2RlOiB0aGlzLnRhcmdldE5vZGVcbiAgICAgICAgfSk7XG4gICAgfVxuXG4gICAgdXBkYXRlKGRlbHRhVGltZTogbnVtYmVyKSB7XG4gICAgICAgIFxuICAgIH1cbn0iXX0=
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "./@types/schema/package/index.json",
3 | "package_version": 2,
4 | "name": "cocos-mcp-server",
5 | "version": "1.4.0",
6 | "author": "LiDaxian",
7 | "editor": ">=3.8.6",
8 | "scripts": {
9 | "preinstall": "node ./scripts/preinstall.js",
10 | "build": "tsc",
11 | "watch": "tsc -w"
12 | },
13 | "description": "i18n:cocos-mcp-server.description",
14 | "main": "./dist/main.js",
15 | "dependencies": {
16 | "fs-extra": "^11.3.0",
17 | "uuid": "^9.0.1",
18 | "vue": "^3.1.4"
19 | },
20 | "devDependencies": {
21 | "@cocos/creator-types": "^3.8.6",
22 | "@types/fs-extra": "^9.0.5",
23 | "@types/node": "^18.17.1",
24 | "@types/uuid": "^9.0.8",
25 | "typescript": "^5.8.2"
26 | },
27 | "panels": {
28 | "default": {
29 | "title": "i18n:cocos-mcp-server.panel_title",
30 | "type": "dockable",
31 | "main": "dist/panels/default",
32 | "icon": "./static/icon.png",
33 | "size": {
34 | "min-width": 400,
35 | "min-height": 300,
36 | "width": 600,
37 | "height": 500
38 | }
39 | }
40 | },
41 | "contributions": {
42 | "menu": [
43 | {
44 | "path": "i18n:menu.extension/Cocos MCP Server",
45 | "label": "i18n:cocos-mcp-server.open_panel",
46 | "message": "open-panel"
47 | }
48 | ],
49 | "messages": {
50 | "open-panel": {
51 | "methods": [
52 | "openPanel"
53 | ]
54 | },
55 | "open-tool-manager": {
56 | "methods": [
57 | "openToolManager"
58 | ]
59 | },
60 | "start-server": {
61 | "methods": [
62 | "startServer"
63 | ]
64 | },
65 | "stop-server": {
66 | "methods": [
67 | "stopServer"
68 | ]
69 | },
70 | "get-server-status": {
71 | "methods": [
72 | "getServerStatus"
73 | ]
74 | },
75 | "update-settings": {
76 | "methods": [
77 | "updateSettings"
78 | ]
79 | },
80 | "getToolsList": {
81 | "methods": [
82 | "getToolsList"
83 | ]
84 | },
85 | "get-server-settings": {
86 | "methods": [
87 | "getServerSettings"
88 | ]
89 | },
90 | "getToolManagerState": {
91 | "methods": [
92 | "getToolManagerState"
93 | ]
94 | },
95 | "createToolConfiguration": {
96 | "methods": [
97 | "createToolConfiguration"
98 | ]
99 | },
100 | "updateToolConfiguration": {
101 | "methods": [
102 | "updateToolConfiguration"
103 | ]
104 | },
105 | "deleteToolConfiguration": {
106 | "methods": [
107 | "deleteToolConfiguration"
108 | ]
109 | },
110 | "setCurrentToolConfiguration": {
111 | "methods": [
112 | "setCurrentToolConfiguration"
113 | ]
114 | },
115 | "updateToolStatus": {
116 | "methods": [
117 | "updateToolStatus"
118 | ]
119 | },
120 | "updateToolStatusBatch": {
121 | "methods": [
122 | "updateToolStatusBatch"
123 | ]
124 | },
125 | "exportToolConfiguration": {
126 | "methods": [
127 | "exportToolConfiguration"
128 | ]
129 | },
130 | "importToolConfiguration": {
131 | "methods": [
132 | "importToolConfiguration"
133 | ]
134 | },
135 | "getEnabledTools": {
136 | "methods": [
137 | "getEnabledTools"
138 | ]
139 | }
140 | },
141 | "scene": {
142 | "script": "./dist/scene.js",
143 | "methods": [
144 | "createNewScene",
145 | "addComponentToNode",
146 | "removeComponentFromNode",
147 | "getNodeInfo",
148 | "getAllNodes",
149 | "findNodeByName",
150 | "setNodeProperty",
151 | "setComponentProperty",
152 | "getCurrentSceneInfo",
153 | "getSceneHierarchy",
154 | "createPrefabFromNode"
155 | ]
156 | }
157 | }
158 | }
159 |
```
--------------------------------------------------------------------------------
/source/test/tool-tester.ts:
--------------------------------------------------------------------------------
```typescript
1 | declare const Editor: any;
2 |
3 | interface TestResult {
4 | tool: string;
5 | method: string;
6 | success: boolean;
7 | result?: any;
8 | error?: string;
9 | time: number;
10 | }
11 |
12 | export class ToolTester {
13 | private results: TestResult[] = [];
14 |
15 | async runTest(tool: string, method: string, params: any): Promise<TestResult> {
16 | const startTime = Date.now();
17 | const result: TestResult = {
18 | tool,
19 | method,
20 | success: false,
21 | time: 0
22 | };
23 |
24 | try {
25 | const response = await Editor.Message.request(tool, method, params);
26 | result.success = true;
27 | result.result = response;
28 | } catch (error) {
29 | result.success = false;
30 | result.error = error instanceof Error ? error.message : String(error);
31 | }
32 |
33 | result.time = Date.now() - startTime;
34 | this.results.push(result);
35 | return result;
36 | }
37 |
38 | async testSceneOperations() {
39 | console.log('Testing Scene Operations...');
40 |
41 | // Test node creation (this is the main scene operation that works)
42 | const createResult = await this.runTest('scene', 'create-node', {
43 | name: 'TestNode',
44 | type: 'cc.Node'
45 | });
46 |
47 | if (createResult.success && createResult.result) {
48 | const nodeUuid = createResult.result;
49 |
50 | // Test query node info
51 | await this.runTest('scene', 'query-node-info', nodeUuid);
52 |
53 | // Test remove node
54 | await this.runTest('scene', 'remove-node', nodeUuid);
55 | }
56 |
57 | // Test execute scene script
58 | await this.runTest('scene', 'execute-scene-script', {
59 | name: 'cocos-mcp-server',
60 | method: 'test-method',
61 | args: []
62 | });
63 | }
64 |
65 | async testNodeOperations() {
66 | console.log('Testing Node Operations...');
67 |
68 | // Create a test node first
69 | const createResult = await this.runTest('scene', 'create-node', {
70 | name: 'TestNodeForOps',
71 | type: 'cc.Node'
72 | });
73 |
74 | if (createResult.success && createResult.result) {
75 | const nodeUuid = createResult.result;
76 |
77 | // Test set property
78 | await this.runTest('scene', 'set-property', {
79 | uuid: nodeUuid,
80 | path: 'position',
81 | dump: {
82 | type: 'cc.Vec3',
83 | value: { x: 100, y: 200, z: 0 }
84 | }
85 | });
86 |
87 | // Test add component
88 | await this.runTest('scene', 'add-component', {
89 | uuid: nodeUuid,
90 | component: 'cc.Sprite'
91 | });
92 |
93 | // Clean up
94 | await this.runTest('scene', 'remove-node', nodeUuid);
95 | }
96 | }
97 |
98 | async testAssetOperations() {
99 | console.log('Testing Asset Operations...');
100 |
101 | // Test asset list
102 | await this.runTest('asset-db', 'query-assets', {
103 | pattern: '**/*.png',
104 | ccType: 'cc.ImageAsset'
105 | });
106 |
107 | // Test query asset by path
108 | await this.runTest('asset-db', 'query-path', 'db://assets');
109 |
110 | // Test query asset by uuid (using a valid uuid format)
111 | await this.runTest('asset-db', 'query-uuid', 'db://assets');
112 | }
113 |
114 | async testProjectOperations() {
115 | console.log('Testing Project Operations...');
116 |
117 | // Test open project settings
118 | await this.runTest('project', 'open-settings', {});
119 |
120 | // Test query project settings
121 | const projectName = await this.runTest('project', 'query-setting', 'name');
122 |
123 | if (projectName.success) {
124 | console.log('Project name:', projectName.result);
125 | }
126 | }
127 |
128 | async runAllTests() {
129 | this.results = [];
130 |
131 | await this.testSceneOperations();
132 | await this.testNodeOperations();
133 | await this.testAssetOperations();
134 | await this.testProjectOperations();
135 |
136 | return this.getTestReport();
137 | }
138 |
139 | getTestReport() {
140 | const total = this.results.length;
141 | const passed = this.results.filter(r => r.success).length;
142 | const failed = total - passed;
143 |
144 | return {
145 | summary: {
146 | total,
147 | passed,
148 | failed,
149 | passRate: total > 0 ? (passed / total * 100).toFixed(2) + '%' : '0%'
150 | },
151 | results: this.results,
152 | grouped: this.groupResultsByTool()
153 | };
154 | }
155 |
156 | private groupResultsByTool() {
157 | const grouped: Record<string, TestResult[]> = {};
158 |
159 | for (const result of this.results) {
160 | if (!grouped[result.tool]) {
161 | grouped[result.tool] = [];
162 | }
163 | grouped[result.tool].push(result);
164 | }
165 |
166 | return grouped;
167 | }
168 | }
```
--------------------------------------------------------------------------------
/static/template/vue/mcp-server-app.html:
--------------------------------------------------------------------------------
```html
1 | <div class="mcp-app">
2 | <!-- 选项卡导航 -->
3 | <div class="tab-navigation">
4 | <button class="tab-button" :class="{ active: activeTab === 'server' }" @click="switchTab('server')">
5 | <span>服务器</span>
6 | </button>
7 | <button class="tab-button" :class="{ active: activeTab === 'tools' }" @click="switchTab('tools')">
8 | <span>工具管理</span>
9 | </button>
10 | </div>
11 |
12 | <!-- 服务器选项卡 -->
13 | <div class="tab-content" v-show="activeTab === 'server'">
14 | <section class="server-status">
15 | <h3>服务器状态</h3>
16 | <div class="status-info">
17 | <ui-prop>
18 | <ui-label slot="label">状态</ui-label>
19 | <ui-label slot="content" class="status-value" :class="statusClass">{{ serverStatus }}</ui-label>
20 | </ui-prop>
21 | <ui-prop v-if="serverRunning">
22 | <ui-label slot="label">连接数</ui-label>
23 | <ui-label slot="content">{{ connectedClients }}</ui-label>
24 | </ui-prop>
25 | </div>
26 | </section>
27 |
28 | <section class="server-controls">
29 | <ui-button @click="toggleServer" :disabled="isProcessing" class="primary">
30 | {{ serverRunning ? '停止服务器' : '启动服务器' }}
31 | </ui-button>
32 | </section>
33 |
34 | <section class="server-settings">
35 | <h3>服务器设置</h3>
36 | <ui-prop>
37 | <ui-label slot="label">端口</ui-label>
38 | <ui-num-input slot="content" v-model="settings.port" :min="1024" :max="65535" :step="1" :disabled="serverRunning">
39 | </ui-num-input>
40 | </ui-prop>
41 | <ui-prop>
42 | <ui-label slot="label">自动启动</ui-label>
43 | <ui-checkbox slot="content" v-model="settings.autoStart"></ui-checkbox>
44 | </ui-prop>
45 | <ui-prop>
46 | <ui-label slot="label">调试日志</ui-label>
47 | <ui-checkbox slot="content" v-model="settings.debugLog"></ui-checkbox>
48 | </ui-prop>
49 | <ui-prop>
50 | <ui-label slot="label">最大连接数</ui-label>
51 | <ui-num-input slot="content" v-model="settings.maxConnections" :min="1" :max="100" :step="1">
52 | </ui-num-input>
53 | </ui-prop>
54 | </section>
55 |
56 | <section class="server-info" v-if="serverRunning">
57 | <h3>连接信息</h3>
58 | <div class="connection-details">
59 | <ui-prop>
60 | <ui-label slot="label">HTTP URL</ui-label>
61 | <ui-input slot="content" :value="httpUrl" readonly>
62 | <ui-button slot="suffix" @click="copyUrl">复制</ui-button>
63 | </ui-input>
64 | </ui-prop>
65 | </div>
66 | </section>
67 |
68 | <footer>
69 | <ui-button @click="saveSettings" :disabled="!settingsChanged">保存设置</ui-button>
70 | </footer>
71 | </div>
72 |
73 | <!-- 工具管理选项卡 -->
74 | <div class="tab-content" v-show="activeTab === 'tools'">
75 | <section class="tool-manager">
76 | <div class="tool-manager-header">
77 | <h3>工具管理</h3>
78 | </div>
79 |
80 | <div class="tools-section">
81 | <div class="tools-section-header">
82 | <div class="tools-section-title">
83 | <h4>可用工具</h4>
84 | <div class="tools-stats">
85 | {{ totalTools }} 个工具
86 | ({{ enabledTools }} 启用 / {{ disabledTools }} 禁用)
87 | </div>
88 | </div>
89 | <div class="tools-section-controls">
90 | <ui-button @click="selectAllTools" class="small">全选</ui-button>
91 | <ui-button @click="deselectAllTools" class="small">取消全选</ui-button>
92 | <ui-button @click="saveChanges" class="primary">保存更改</ui-button>
93 | </div>
94 | </div>
95 |
96 | <div class="tools-container">
97 | <div v-for="category in toolCategories" :key="category" class="tool-category">
98 | <div class="category-header">
99 | <h5>{{ getCategoryDisplayName(category) }}</h5>
100 | <div class="category-controls">
101 | <ui-button @click="toggleCategoryTools(category, true)" class="small">全选</ui-button>
102 | <ui-button @click="toggleCategoryTools(category, false)" class="small">取消全选</ui-button>
103 | </div>
104 | </div>
105 | <div class="tool-items">
106 | <div v-for="tool in getToolsByCategory(category)" :key="tool.name" class="tool-item">
107 | <ui-checkbox
108 | :value="tool.enabled"
109 | @change="(event) => updateToolStatus(category, tool.name, event.target.checked)"
110 | ></ui-checkbox>
111 | <div class="tool-info">
112 | <div class="tool-name">{{ tool.name }}</div>
113 | <div class="tool-description">{{ tool.description }}</div>
114 | </div>
115 | </div>
116 | </div>
117 | </div>
118 | </div>
119 | </div>
120 |
121 |
122 | </section>
123 | </div>
124 | </div>
125 |
126 |
```
--------------------------------------------------------------------------------
/static/style/default/index.css:
--------------------------------------------------------------------------------
```css
1 | .mcp-server-panel {
2 | padding: 20px;
3 | display: flex;
4 | flex-direction: column;
5 | height: 100%;
6 | min-height: 100%;
7 | max-height: 100%;
8 | overflow-y: auto;
9 | box-sizing: border-box;
10 | }
11 |
12 | header h2 {
13 | margin: 0 0 20px 0;
14 | font-size: 18px;
15 | }
16 |
17 | /* Vue3 应用样式 */
18 | .mcp-app {
19 | display: flex;
20 | flex-direction: column;
21 | height: 100%;
22 | overflow: hidden;
23 | }
24 |
25 | /* 选项卡导航样式 */
26 | .tab-navigation {
27 | display: flex;
28 | border-bottom: 1px solid var(--color-normal-border);
29 | margin-bottom: 20px;
30 | background: var(--color-panel);
31 | }
32 |
33 | .tab-button {
34 | background: none;
35 | border: none;
36 | padding: 10px 20px;
37 | cursor: pointer;
38 | border-bottom: 2px solid transparent;
39 | color: var(--color-normal-text);
40 | font-size: 14px;
41 | transition: all 0.2s ease;
42 | border-radius: 4px 4px 0 0;
43 | }
44 |
45 | .tab-button:hover {
46 | background: var(--color-normal-fill-emphasis);
47 | }
48 |
49 | .tab-button.active {
50 | border-bottom-color: var(--color-primary);
51 | color: var(--color-primary);
52 | font-weight: 600;
53 | background: var(--color-panel);
54 | }
55 |
56 | /* 选项卡内容样式 */
57 | .tab-content {
58 | display: flex;
59 | flex-direction: column;
60 | flex: 1;
61 | overflow-y: auto;
62 | padding: 20px;
63 | }
64 |
65 | section {
66 | margin-bottom: 20px;
67 | padding: 15px;
68 | background: var(--color-normal-fill-emphasis);
69 | border-radius: 4px;
70 | }
71 |
72 | section h3 {
73 | margin: 0 0 15px 0;
74 | font-size: 14px;
75 | font-weight: 600;
76 | }
77 |
78 | .status-value {
79 | font-weight: 600;
80 | }
81 |
82 | .status-value.running {
83 | color: var(--color-success-fill);
84 | }
85 |
86 | .status-value.stopped {
87 | color: var(--color-warn-fill);
88 | }
89 |
90 | .server-controls {
91 | display: flex;
92 | justify-content: center;
93 | padding: 20px;
94 | }
95 |
96 | .server-controls ui-button {
97 | min-width: 150px;
98 | }
99 |
100 | .connection-details {
101 | margin-top: 10px;
102 | }
103 |
104 | footer {
105 | margin-top: auto;
106 | padding-top: 20px;
107 | display: flex;
108 | justify-content: flex-end;
109 | }
110 |
111 | ui-prop {
112 | margin-bottom: 10px;
113 | }
114 |
115 | /* 工具管理器样式 */
116 | .tool-manager {
117 | display: flex;
118 | flex-direction: column;
119 | height: 100%;
120 | }
121 |
122 | .tool-manager-header {
123 | display: flex;
124 | justify-content: space-between;
125 | align-items: center;
126 | margin-bottom: 20px;
127 | }
128 |
129 | .tool-manager-controls {
130 | display: flex;
131 | gap: 10px;
132 | }
133 |
134 | .config-selector-section {
135 | margin-bottom: 20px;
136 | }
137 |
138 | .tools-section {
139 | flex: 1;
140 | display: flex;
141 | flex-direction: column;
142 | min-height: 0;
143 | }
144 |
145 | .tools-section-header {
146 | display: flex;
147 | justify-content: space-between;
148 | align-items: center;
149 | margin-bottom: 15px;
150 | padding-bottom: 10px;
151 | border-bottom: 1px solid var(--color-normal-border);
152 | }
153 |
154 | .tools-section-title {
155 | display: flex;
156 | flex-direction: column;
157 | gap: 5px;
158 | }
159 |
160 | .tools-section-title h4 {
161 | margin: 0;
162 | font-size: 14px;
163 | font-weight: 600;
164 | }
165 |
166 | .tools-stats {
167 | font-size: 12px;
168 | color: var(--color-normal-text-secondary);
169 | }
170 |
171 | .tools-section-controls {
172 | display: flex;
173 | gap: 10px;
174 | align-items: center;
175 | }
176 |
177 | .tools-container {
178 | flex: 1;
179 | overflow-y: auto;
180 | padding-right: 8px;
181 | max-height: 400px;
182 | }
183 |
184 | .tools-container::-webkit-scrollbar {
185 | width: 6px;
186 | }
187 |
188 | .tools-container::-webkit-scrollbar-track {
189 | background: var(--color-normal-fill-emphasis);
190 | border-radius: 3px;
191 | }
192 |
193 | .tools-container::-webkit-scrollbar-thumb {
194 | background: var(--color-normal-border);
195 | border-radius: 3px;
196 | }
197 |
198 | .tools-container::-webkit-scrollbar-thumb:hover {
199 | background: var(--color-normal-text-secondary);
200 | }
201 |
202 | .tool-category {
203 | margin-bottom: 20px;
204 | background: var(--color-normal-fill);
205 | border-radius: 6px;
206 | overflow: hidden;
207 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
208 | }
209 |
210 | .category-header {
211 | display: flex;
212 | justify-content: space-between;
213 | align-items: center;
214 | padding: 12px 15px;
215 | background: linear-gradient(135deg, var(--color-primary-fill) 0%, var(--color-primary-fill-emphasis) 100%);
216 | color: var(--color-primary-text);
217 | font-weight: 600;
218 | }
219 |
220 | .category-name {
221 | font-size: 14px;
222 | }
223 |
224 | .category-toggle {
225 | display: flex;
226 | align-items: center;
227 | gap: 10px;
228 | font-size: 12px;
229 | }
230 |
231 | .tool-list {
232 | padding: 10px 15px;
233 | }
234 |
235 | .tool-item {
236 | display: flex;
237 | justify-content: space-between;
238 | align-items: center;
239 | padding: 8px 0;
240 | border-bottom: 1px solid var(--color-normal-border);
241 | }
242 |
243 | .tool-item:last-child {
244 | border-bottom: none;
245 | }
246 |
247 | .tool-info {
248 | flex: 1;
249 | }
250 |
251 | .tool-name {
252 | font-size: 13px;
253 | font-weight: 500;
254 | color: var(--color-normal-text);
255 | }
256 |
257 | .tool-description {
258 | font-size: 11px;
259 | color: var(--color-normal-text-secondary);
260 | margin-top: 2px;
261 | }
262 |
263 | .tool-toggle {
264 | display: flex;
265 | align-items: center;
266 | }
267 |
268 | .checkbox {
269 | width: 16px;
270 | height: 16px;
271 | cursor: pointer;
272 | }
273 |
274 | .tool-manager-footer {
275 | margin-top: 20px;
276 | padding-top: 15px;
277 | border-top: 1px solid var(--color-normal-border);
278 | }
279 |
280 | .config-actions {
281 | display: flex;
282 | justify-content: space-between;
283 | align-items: center;
284 | }
285 |
286 | /* 模态框样式 */
287 | .modal {
288 | position: fixed;
289 | top: 0;
290 | left: 0;
291 | width: 100%;
292 | height: 100%;
293 | background: rgba(0, 0, 0, 0.7);
294 | backdrop-filter: blur(4px);
295 | display: flex;
296 | justify-content: center;
297 | align-items: center;
298 | z-index: 1000;
299 | }
300 |
301 | .modal-content {
302 | background: var(--color-normal-fill);
303 | border-radius: 8px;
304 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
305 | backdrop-filter: blur(8px);
306 | min-width: 400px;
307 | max-width: 600px;
308 | max-height: 80vh;
309 | overflow-y: auto;
310 | }
311 |
312 | .modal-header {
313 | display: flex;
314 | justify-content: space-between;
315 | align-items: center;
316 | padding: 20px 20px 0 20px;
317 | border-bottom: 1px solid var(--color-normal-border);
318 | padding-bottom: 15px;
319 | }
320 |
321 | .modal-header h3 {
322 | margin: 0;
323 | font-size: 16px;
324 | font-weight: 600;
325 | }
326 |
327 | .modal-close {
328 | background: none;
329 | border: none;
330 | font-size: 20px;
331 | cursor: pointer;
332 | color: var(--color-normal-text-secondary);
333 | padding: 0;
334 | width: 24px;
335 | height: 24px;
336 | display: flex;
337 | align-items: center;
338 | justify-content: center;
339 | border-radius: 4px;
340 | }
341 |
342 | .modal-close:hover {
343 | background: var(--color-normal-fill-emphasis);
344 | color: var(--color-normal-text);
345 | }
346 |
347 | .modal-body {
348 | padding: 20px;
349 | }
350 |
351 | .modal-footer {
352 | display: flex;
353 | justify-content: flex-end;
354 | gap: 10px;
355 | padding: 0 20px 20px 20px;
356 | border-top: 1px solid var(--color-normal-border);
357 | padding-top: 15px;
358 | }
359 |
360 | /* 按钮样式 */
361 | ui-button.small {
362 | font-size: 12px;
363 | padding: 4px 8px;
364 | min-width: auto;
365 | }
366 |
367 | ui-button.secondary {
368 | background: var(--color-normal-fill-emphasis);
369 | color: var(--color-normal-text);
370 | border: 1px solid var(--color-normal-border);
371 | }
372 |
373 | ui-button.secondary:hover {
374 | background: var(--color-normal-fill);
375 | }
376 |
377 | /* 空状态样式 */
378 | .empty-state {
379 | text-align: center;
380 | padding: 40px 20px;
381 | color: var(--color-normal-text-secondary);
382 | }
383 |
384 | .empty-state h3 {
385 | margin: 0 0 10px 0;
386 | font-size: 16px;
387 | }
388 |
389 | .empty-state p {
390 | margin: 0;
391 | font-size: 14px;
392 | }
```
--------------------------------------------------------------------------------
/source/test/mcp-tool-tester.ts:
--------------------------------------------------------------------------------
```typescript
1 | declare const Editor: any;
2 |
3 | /**
4 | * MCP 工具测试器 - 直接测试通过 WebSocket 的 MCP 工具
5 | */
6 | export class MCPToolTester {
7 | private ws: WebSocket | null = null;
8 | private messageId = 0;
9 | private responseHandlers = new Map<number, (response: any) => void>();
10 |
11 | async connect(port: number): Promise<boolean> {
12 | return new Promise((resolve) => {
13 | try {
14 | this.ws = new WebSocket(`ws://localhost:${port}`);
15 |
16 | this.ws.onopen = () => {
17 | console.log('WebSocket 连接成功');
18 | resolve(true);
19 | };
20 |
21 | this.ws.onerror = (error) => {
22 | console.error('WebSocket 连接错误:', error);
23 | resolve(false);
24 | };
25 |
26 | this.ws.onmessage = (event) => {
27 | try {
28 | const response = JSON.parse(event.data);
29 | if (response.id && this.responseHandlers.has(response.id)) {
30 | const handler = this.responseHandlers.get(response.id);
31 | this.responseHandlers.delete(response.id);
32 | handler?.(response);
33 | }
34 | } catch (error) {
35 | console.error('处理响应时出错:', error);
36 | }
37 | };
38 | } catch (error) {
39 | console.error('创建 WebSocket 时出错:', error);
40 | resolve(false);
41 | }
42 | });
43 | }
44 |
45 | async callTool(tool: string, args: any = {}): Promise<any> {
46 | if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
47 | throw new Error('WebSocket 未连接');
48 | }
49 |
50 | return new Promise((resolve, reject) => {
51 | const id = ++this.messageId;
52 | const request = {
53 | jsonrpc: '2.0',
54 | id,
55 | method: 'tools/call',
56 | params: {
57 | name: tool,
58 | arguments: args
59 | }
60 | };
61 |
62 | const timeout = setTimeout(() => {
63 | this.responseHandlers.delete(id);
64 | reject(new Error('请求超时'));
65 | }, 10000);
66 |
67 | this.responseHandlers.set(id, (response) => {
68 | clearTimeout(timeout);
69 | if (response.error) {
70 | reject(new Error(response.error.message));
71 | } else {
72 | resolve(response.result);
73 | }
74 | });
75 |
76 | this.ws!.send(JSON.stringify(request));
77 | });
78 | }
79 |
80 | async listTools(): Promise<any> {
81 | if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
82 | throw new Error('WebSocket 未连接');
83 | }
84 |
85 | return new Promise((resolve, reject) => {
86 | const id = ++this.messageId;
87 | const request = {
88 | jsonrpc: '2.0',
89 | id,
90 | method: 'tools/list'
91 | };
92 |
93 | const timeout = setTimeout(() => {
94 | this.responseHandlers.delete(id);
95 | reject(new Error('请求超时'));
96 | }, 10000);
97 |
98 | this.responseHandlers.set(id, (response) => {
99 | clearTimeout(timeout);
100 | if (response.error) {
101 | reject(new Error(response.error.message));
102 | } else {
103 | resolve(response.result);
104 | }
105 | });
106 |
107 | this.ws!.send(JSON.stringify(request));
108 | });
109 | }
110 |
111 | async testMCPTools() {
112 | console.log('\n=== 测试 MCP 工具(通过 WebSocket)===');
113 |
114 | try {
115 | // 0. 获取工具列表
116 | console.log('\n0. 获取工具列表...');
117 | const toolsList = await this.listTools();
118 | console.log(`找到 ${toolsList.tools?.length || 0} 个工具:`);
119 | if (toolsList.tools) {
120 | for (const tool of toolsList.tools.slice(0, 10)) { // 只显示前10个
121 | console.log(` - ${tool.name}: ${tool.description}`);
122 | }
123 | if (toolsList.tools.length > 10) {
124 | console.log(` ... 还有 ${toolsList.tools.length - 10} 个工具`);
125 | }
126 | }
127 |
128 | // 1. 测试场景工具
129 | console.log('\n1. 测试当前场景信息...');
130 | const sceneInfo = await this.callTool('scene_get_current_scene');
131 | console.log('场景信息:', JSON.stringify(sceneInfo).substring(0, 100) + '...');
132 |
133 | // 2. 测试场景列表
134 | console.log('\n2. 测试场景列表...');
135 | const sceneList = await this.callTool('scene_get_scene_list');
136 | console.log('场景列表:', JSON.stringify(sceneList).substring(0, 100) + '...');
137 |
138 | // 3. 测试节点创建
139 | console.log('\n3. 测试创建节点...');
140 | const createResult = await this.callTool('node_create_node', {
141 | name: 'MCPTestNode_' + Date.now(),
142 | nodeType: 'cc.Node',
143 | position: { x: 0, y: 0, z: 0 }
144 | });
145 | console.log('创建节点结果:', createResult);
146 |
147 | // 解析创建节点的结果
148 | let nodeUuid: string | null = null;
149 | if (createResult.content && createResult.content[0] && createResult.content[0].text) {
150 | try {
151 | const resultData = JSON.parse(createResult.content[0].text);
152 | if (resultData.success && resultData.data && resultData.data.uuid) {
153 | nodeUuid = resultData.data.uuid;
154 | console.log('成功获取节点UUID:', nodeUuid);
155 | }
156 | } catch (e) {
157 | }
158 | }
159 |
160 | if (nodeUuid) {
161 | // 4. 测试查询节点
162 | console.log('\n4. 测试查询节点...');
163 | const queryResult = await this.callTool('node_get_node_info', {
164 | uuid: nodeUuid
165 | });
166 | console.log('节点信息:', JSON.stringify(queryResult).substring(0, 100) + '...');
167 |
168 | // 5. 测试删除节点
169 | console.log('\n5. 测试删除节点...');
170 | const removeResult = await this.callTool('node_delete_node', {
171 | uuid: nodeUuid
172 | });
173 | console.log('删除结果:', removeResult);
174 | } else {
175 | console.log('无法从创建结果获取节点UUID,尝试通过名称查找...');
176 |
177 | // 备用方案:通过名称查找刚创建的节点
178 | const findResult = await this.callTool('node_find_node_by_name', {
179 | name: 'MCPTestNode_' + Date.now()
180 | });
181 |
182 | if (findResult.content && findResult.content[0] && findResult.content[0].text) {
183 | try {
184 | const findData = JSON.parse(findResult.content[0].text);
185 | if (findData.success && findData.data && findData.data.uuid) {
186 | nodeUuid = findData.data.uuid;
187 | console.log('通过名称查找成功获取UUID:', nodeUuid);
188 | }
189 | } catch (e) {
190 | }
191 | }
192 |
193 | if (!nodeUuid) {
194 | console.log('所有方式都无法获取节点UUID,跳过后续节点操作测试');
195 | }
196 | }
197 |
198 | // 6. 测试项目工具
199 | console.log('\n6. 测试项目信息...');
200 | const projectInfo = await this.callTool('project_get_project_info');
201 | console.log('项目信息:', JSON.stringify(projectInfo).substring(0, 100) + '...');
202 |
203 | // 7. 测试预制体工具
204 | console.log('\n7. 测试预制体列表...');
205 | const prefabResult = await this.callTool('prefab_get_prefab_list', {
206 | folder: 'db://assets'
207 | });
208 | console.log('找到预制体:', prefabResult.data?.length || 0);
209 |
210 | // 8. 测试组件工具
211 | console.log('\n8. 测试可用组件...');
212 | const componentsResult = await this.callTool('component_get_available_components');
213 | console.log('可用组件:', JSON.stringify(componentsResult).substring(0, 100) + '...');
214 |
215 | // 9. 测试调试工具
216 | console.log('\n9. 测试编辑器信息...');
217 | const editorInfo = await this.callTool('debug_get_editor_info');
218 | console.log('编辑器信息:', JSON.stringify(editorInfo).substring(0, 100) + '...');
219 |
220 | } catch (error) {
221 | console.error('MCP 工具测试失败:', error);
222 | }
223 | }
224 |
225 | disconnect() {
226 | if (this.ws) {
227 | this.ws.close();
228 | this.ws = null;
229 | }
230 | this.responseHandlers.clear();
231 | }
232 | }
233 |
234 | // 导出到全局方便测试
235 | (global as any).MCPToolTester = MCPToolTester;
```
--------------------------------------------------------------------------------
/source/tools/broadcast-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class BroadcastTools implements ToolExecutor {
4 | private listeners: Map<string, Function[]> = new Map();
5 | private messageLog: Array<{ message: string; data: any; timestamp: number }> = [];
6 |
7 | constructor() {
8 | this.setupBroadcastListeners();
9 | }
10 |
11 | getTools(): ToolDefinition[] {
12 | return [
13 | {
14 | name: 'get_broadcast_log',
15 | description: 'Get recent broadcast messages log',
16 | inputSchema: {
17 | type: 'object',
18 | properties: {
19 | limit: {
20 | type: 'number',
21 | description: 'Number of recent messages to return',
22 | default: 50
23 | },
24 | messageType: {
25 | type: 'string',
26 | description: 'Filter by message type (optional)'
27 | }
28 | }
29 | }
30 | },
31 | {
32 | name: 'listen_broadcast',
33 | description: 'Start listening for specific broadcast messages',
34 | inputSchema: {
35 | type: 'object',
36 | properties: {
37 | messageType: {
38 | type: 'string',
39 | description: 'Message type to listen for'
40 | }
41 | },
42 | required: ['messageType']
43 | }
44 | },
45 | {
46 | name: 'stop_listening',
47 | description: 'Stop listening for specific broadcast messages',
48 | inputSchema: {
49 | type: 'object',
50 | properties: {
51 | messageType: {
52 | type: 'string',
53 | description: 'Message type to stop listening for'
54 | }
55 | },
56 | required: ['messageType']
57 | }
58 | },
59 | {
60 | name: 'clear_broadcast_log',
61 | description: 'Clear the broadcast messages log',
62 | inputSchema: {
63 | type: 'object',
64 | properties: {}
65 | }
66 | },
67 | {
68 | name: 'get_active_listeners',
69 | description: 'Get list of active broadcast listeners',
70 | inputSchema: {
71 | type: 'object',
72 | properties: {}
73 | }
74 | }
75 | ];
76 | }
77 |
78 | async execute(toolName: string, args: any): Promise<ToolResponse> {
79 | switch (toolName) {
80 | case 'get_broadcast_log':
81 | return await this.getBroadcastLog(args.limit, args.messageType);
82 | case 'listen_broadcast':
83 | return await this.listenBroadcast(args.messageType);
84 | case 'stop_listening':
85 | return await this.stopListening(args.messageType);
86 | case 'clear_broadcast_log':
87 | return await this.clearBroadcastLog();
88 | case 'get_active_listeners':
89 | return await this.getActiveListeners();
90 | default:
91 | throw new Error(`Unknown tool: ${toolName}`);
92 | }
93 | }
94 |
95 | private setupBroadcastListeners(): void {
96 | // 设置预定义的重要广播消息监听
97 | const importantMessages = [
98 | 'build-worker:ready',
99 | 'build-worker:closed',
100 | 'scene:ready',
101 | 'scene:close',
102 | 'scene:light-probe-edit-mode-changed',
103 | 'scene:light-probe-bounding-box-edit-mode-changed',
104 | 'asset-db:ready',
105 | 'asset-db:close',
106 | 'asset-db:asset-add',
107 | 'asset-db:asset-change',
108 | 'asset-db:asset-delete'
109 | ];
110 |
111 | importantMessages.forEach(messageType => {
112 | this.addBroadcastListener(messageType);
113 | });
114 | }
115 |
116 | private addBroadcastListener(messageType: string): void {
117 | const listener = (data: any) => {
118 | this.messageLog.push({
119 | message: messageType,
120 | data: data,
121 | timestamp: Date.now()
122 | });
123 |
124 | // 保持日志大小在合理范围内
125 | if (this.messageLog.length > 1000) {
126 | this.messageLog = this.messageLog.slice(-500);
127 | }
128 |
129 | console.log(`[Broadcast] ${messageType}:`, data);
130 | };
131 |
132 | if (!this.listeners.has(messageType)) {
133 | this.listeners.set(messageType, []);
134 | }
135 | this.listeners.get(messageType)!.push(listener);
136 |
137 | // 注册 Editor 消息监听 - 暂时注释掉,Editor.Message API可能不支持
138 | // Editor.Message.on(messageType, listener);
139 | console.log(`[BroadcastTools] Added listener for ${messageType} (simulated)`);
140 | }
141 |
142 | private removeBroadcastListener(messageType: string): void {
143 | const listeners = this.listeners.get(messageType);
144 | if (listeners) {
145 | listeners.forEach(listener => {
146 | // Editor.Message.off(messageType, listener);
147 | console.log(`[BroadcastTools] Removed listener for ${messageType} (simulated)`);
148 | });
149 | this.listeners.delete(messageType);
150 | }
151 | }
152 |
153 | private async getBroadcastLog(limit: number = 50, messageType?: string): Promise<ToolResponse> {
154 | return new Promise((resolve) => {
155 | let filteredLog = this.messageLog;
156 |
157 | if (messageType) {
158 | filteredLog = this.messageLog.filter(entry => entry.message === messageType);
159 | }
160 |
161 | const recentLog = filteredLog.slice(-limit).map(entry => ({
162 | ...entry,
163 | timestamp: new Date(entry.timestamp).toISOString()
164 | }));
165 |
166 | resolve({
167 | success: true,
168 | data: {
169 | log: recentLog,
170 | count: recentLog.length,
171 | totalCount: filteredLog.length,
172 | filter: messageType || 'all',
173 | message: 'Broadcast log retrieved successfully'
174 | }
175 | });
176 | });
177 | }
178 |
179 | private async listenBroadcast(messageType: string): Promise<ToolResponse> {
180 | return new Promise((resolve) => {
181 | try {
182 | if (!this.listeners.has(messageType)) {
183 | this.addBroadcastListener(messageType);
184 | resolve({
185 | success: true,
186 | data: {
187 | messageType: messageType,
188 | message: `Started listening for broadcast: ${messageType}`
189 | }
190 | });
191 | } else {
192 | resolve({
193 | success: true,
194 | data: {
195 | messageType: messageType,
196 | message: `Already listening for broadcast: ${messageType}`
197 | }
198 | });
199 | }
200 | } catch (err: any) {
201 | resolve({ success: false, error: err.message });
202 | }
203 | });
204 | }
205 |
206 | private async stopListening(messageType: string): Promise<ToolResponse> {
207 | return new Promise((resolve) => {
208 | try {
209 | if (this.listeners.has(messageType)) {
210 | this.removeBroadcastListener(messageType);
211 | resolve({
212 | success: true,
213 | data: {
214 | messageType: messageType,
215 | message: `Stopped listening for broadcast: ${messageType}`
216 | }
217 | });
218 | } else {
219 | resolve({
220 | success: true,
221 | data: {
222 | messageType: messageType,
223 | message: `Was not listening for broadcast: ${messageType}`
224 | }
225 | });
226 | }
227 | } catch (err: any) {
228 | resolve({ success: false, error: err.message });
229 | }
230 | });
231 | }
232 |
233 | private async clearBroadcastLog(): Promise<ToolResponse> {
234 | return new Promise((resolve) => {
235 | const previousCount = this.messageLog.length;
236 | this.messageLog = [];
237 | resolve({
238 | success: true,
239 | data: {
240 | clearedCount: previousCount,
241 | message: 'Broadcast log cleared successfully'
242 | }
243 | });
244 | });
245 | }
246 |
247 | private async getActiveListeners(): Promise<ToolResponse> {
248 | return new Promise((resolve) => {
249 | const activeListeners = Array.from(this.listeners.keys()).map(messageType => ({
250 | messageType: messageType,
251 | listenerCount: this.listeners.get(messageType)?.length || 0
252 | }));
253 |
254 | resolve({
255 | success: true,
256 | data: {
257 | listeners: activeListeners,
258 | count: activeListeners.length,
259 | message: 'Active listeners retrieved successfully'
260 | }
261 | });
262 | });
263 | }
264 | }
```
--------------------------------------------------------------------------------
/source/tools/validation-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class ValidationTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'validate_json_params',
8 | description: 'Validate and fix JSON parameters before sending to other tools',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {
12 | jsonString: {
13 | type: 'string',
14 | description: 'JSON string to validate and fix'
15 | },
16 | expectedSchema: {
17 | type: 'object',
18 | description: 'Expected parameter schema (optional)'
19 | }
20 | },
21 | required: ['jsonString']
22 | }
23 | },
24 | {
25 | name: 'safe_string_value',
26 | description: 'Create a safe string value that won\'t cause JSON parsing issues',
27 | inputSchema: {
28 | type: 'object',
29 | properties: {
30 | value: {
31 | type: 'string',
32 | description: 'String value to make safe'
33 | }
34 | },
35 | required: ['value']
36 | }
37 | },
38 | {
39 | name: 'format_mcp_request',
40 | description: 'Format a complete MCP request with proper JSON escaping',
41 | inputSchema: {
42 | type: 'object',
43 | properties: {
44 | toolName: {
45 | type: 'string',
46 | description: 'Tool name to call'
47 | },
48 | arguments: {
49 | type: 'object',
50 | description: 'Tool arguments'
51 | }
52 | },
53 | required: ['toolName', 'arguments']
54 | }
55 | }
56 | ];
57 | }
58 |
59 | async execute(toolName: string, args: any): Promise<ToolResponse> {
60 | switch (toolName) {
61 | case 'validate_json_params':
62 | return await this.validateJsonParams(args.jsonString, args.expectedSchema);
63 | case 'safe_string_value':
64 | return await this.createSafeStringValue(args.value);
65 | case 'format_mcp_request':
66 | return await this.formatMcpRequest(args.toolName, args.arguments);
67 | default:
68 | throw new Error(`Unknown tool: ${toolName}`);
69 | }
70 | }
71 |
72 | private async validateJsonParams(jsonString: string, expectedSchema?: any): Promise<ToolResponse> {
73 | try {
74 | // First try to parse as-is
75 | let parsed;
76 | try {
77 | parsed = JSON.parse(jsonString);
78 | } catch (error: any) {
79 | // Try to fix common issues
80 | const fixed = this.fixJsonString(jsonString);
81 | try {
82 | parsed = JSON.parse(fixed);
83 | } catch (secondError) {
84 | return {
85 | success: false,
86 | error: `Cannot fix JSON: ${error.message}`,
87 | data: {
88 | originalJson: jsonString,
89 | fixedAttempt: fixed,
90 | suggestions: this.getJsonFixSuggestions(jsonString)
91 | }
92 | };
93 | }
94 | }
95 |
96 | // Validate against schema if provided
97 | if (expectedSchema) {
98 | const validation = this.validateAgainstSchema(parsed, expectedSchema);
99 | if (!validation.valid) {
100 | return {
101 | success: false,
102 | error: 'Schema validation failed',
103 | data: {
104 | parsedJson: parsed,
105 | validationErrors: validation.errors,
106 | suggestions: validation.suggestions
107 | }
108 | };
109 | }
110 | }
111 |
112 | return {
113 | success: true,
114 | data: {
115 | parsedJson: parsed,
116 | fixedJson: JSON.stringify(parsed, null, 2),
117 | isValid: true
118 | }
119 | };
120 | } catch (error: any) {
121 | return {
122 | success: false,
123 | error: error.message
124 | };
125 | }
126 | }
127 |
128 | private async createSafeStringValue(value: string): Promise<ToolResponse> {
129 | const safeValue = this.escapJsonString(value);
130 | return {
131 | success: true,
132 | data: {
133 | originalValue: value,
134 | safeValue: safeValue,
135 | jsonReady: JSON.stringify(safeValue),
136 | usage: `Use "${safeValue}" in your JSON parameters`
137 | }
138 | };
139 | }
140 |
141 | private async formatMcpRequest(toolName: string, toolArgs: any): Promise<ToolResponse> {
142 | try {
143 | const mcpRequest = {
144 | jsonrpc: '2.0',
145 | id: Date.now(),
146 | method: 'tools/call',
147 | params: {
148 | name: toolName,
149 | arguments: toolArgs
150 | }
151 | };
152 |
153 | const formattedJson = JSON.stringify(mcpRequest, null, 2);
154 | const compactJson = JSON.stringify(mcpRequest);
155 |
156 | return {
157 | success: true,
158 | data: {
159 | request: mcpRequest,
160 | formattedJson: formattedJson,
161 | compactJson: compactJson,
162 | curlCommand: this.generateCurlCommand(compactJson)
163 | }
164 | };
165 | } catch (error: any) {
166 | return {
167 | success: false,
168 | error: `Failed to format MCP request: ${error.message}`
169 | };
170 | }
171 | }
172 |
173 | private fixJsonString(jsonStr: string): string {
174 | let fixed = jsonStr;
175 |
176 | // Fix common escape character issues
177 | fixed = fixed
178 | // Fix unescaped quotes in string values
179 | .replace(/(\{[^}]*"[^"]*":\s*")([^"]*")([^"]*")([^}]*\})/g, (match, prefix, content, suffix, end) => {
180 | const escapedContent = content.replace(/"/g, '\\"');
181 | return prefix + escapedContent + suffix + end;
182 | })
183 | // Fix unescaped backslashes
184 | .replace(/([^\\])\\([^"\\\/bfnrtu])/g, '$1\\\\$2')
185 | // Fix trailing commas
186 | .replace(/,(\s*[}\]])/g, '$1')
187 | // Fix control characters
188 | .replace(/\n/g, '\\n')
189 | .replace(/\r/g, '\\r')
190 | .replace(/\t/g, '\\t')
191 | // Fix single quotes to double quotes
192 | .replace(/'/g, '"');
193 |
194 | return fixed;
195 | }
196 |
197 | private escapJsonString(str: string): string {
198 | return str
199 | .replace(/\\/g, '\\\\') // Escape backslashes first
200 | .replace(/"/g, '\\"') // Escape quotes
201 | .replace(/\n/g, '\\n') // Escape newlines
202 | .replace(/\r/g, '\\r') // Escape carriage returns
203 | .replace(/\t/g, '\\t') // Escape tabs
204 | .replace(/\f/g, '\\f') // Escape form feeds
205 | .replace(/\b/g, '\\b'); // Escape backspaces
206 | }
207 |
208 | private validateAgainstSchema(data: any, schema: any): { valid: boolean; errors: string[]; suggestions: string[] } {
209 | const errors: string[] = [];
210 | const suggestions: string[] = [];
211 |
212 | // Basic type checking
213 | if (schema.type) {
214 | const actualType = Array.isArray(data) ? 'array' : typeof data;
215 | if (actualType !== schema.type) {
216 | errors.push(`Expected type ${schema.type}, got ${actualType}`);
217 | suggestions.push(`Convert value to ${schema.type}`);
218 | }
219 | }
220 |
221 | // Required fields checking
222 | if (schema.required && Array.isArray(schema.required)) {
223 | for (const field of schema.required) {
224 | if (!Object.prototype.hasOwnProperty.call(data, field)) {
225 | errors.push(`Missing required field: ${field}`);
226 | suggestions.push(`Add required field "${field}"`);
227 | }
228 | }
229 | }
230 |
231 | return {
232 | valid: errors.length === 0,
233 | errors,
234 | suggestions
235 | };
236 | }
237 |
238 | private getJsonFixSuggestions(jsonStr: string): string[] {
239 | const suggestions: string[] = [];
240 |
241 | if (jsonStr.includes('\\"')) {
242 | suggestions.push('Check for improperly escaped quotes');
243 | }
244 | if (jsonStr.includes("'")) {
245 | suggestions.push('Replace single quotes with double quotes');
246 | }
247 | if (jsonStr.includes('\n') || jsonStr.includes('\t')) {
248 | suggestions.push('Escape newlines and tabs properly');
249 | }
250 | if (jsonStr.match(/,\s*[}\]]/)) {
251 | suggestions.push('Remove trailing commas');
252 | }
253 |
254 | return suggestions;
255 | }
256 |
257 | private generateCurlCommand(jsonStr: string): string {
258 | const escapedJson = jsonStr.replace(/'/g, "'\"'\"'");
259 | return `curl -X POST http://127.0.0.1:8585/mcp \\
260 | -H "Content-Type: application/json" \\
261 | -d '${escapedJson}'`;
262 | }
263 | }
```
--------------------------------------------------------------------------------
/source/tools/server-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class ServerTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'query_server_ip_list',
8 | description: 'Query server IP list',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {}
12 | }
13 | },
14 | {
15 | name: 'query_sorted_server_ip_list',
16 | description: 'Get sorted server IP list',
17 | inputSchema: {
18 | type: 'object',
19 | properties: {}
20 | }
21 | },
22 | {
23 | name: 'query_server_port',
24 | description: 'Query editor server current port',
25 | inputSchema: {
26 | type: 'object',
27 | properties: {}
28 | }
29 | },
30 | {
31 | name: 'get_server_status',
32 | description: 'Get comprehensive server status information',
33 | inputSchema: {
34 | type: 'object',
35 | properties: {}
36 | }
37 | },
38 | {
39 | name: 'check_server_connectivity',
40 | description: 'Check server connectivity and network status',
41 | inputSchema: {
42 | type: 'object',
43 | properties: {
44 | timeout: {
45 | type: 'number',
46 | description: 'Timeout in milliseconds',
47 | default: 5000
48 | }
49 | }
50 | }
51 | },
52 | {
53 | name: 'get_network_interfaces',
54 | description: 'Get available network interfaces',
55 | inputSchema: {
56 | type: 'object',
57 | properties: {}
58 | }
59 | }
60 | ];
61 | }
62 |
63 | async execute(toolName: string, args: any): Promise<ToolResponse> {
64 | switch (toolName) {
65 | case 'query_server_ip_list':
66 | return await this.queryServerIPList();
67 | case 'query_sorted_server_ip_list':
68 | return await this.querySortedServerIPList();
69 | case 'query_server_port':
70 | return await this.queryServerPort();
71 | case 'get_server_status':
72 | return await this.getServerStatus();
73 | case 'check_server_connectivity':
74 | return await this.checkServerConnectivity(args.timeout);
75 | case 'get_network_interfaces':
76 | return await this.getNetworkInterfaces();
77 | default:
78 | throw new Error(`Unknown tool: ${toolName}`);
79 | }
80 | }
81 |
82 | private async queryServerIPList(): Promise<ToolResponse> {
83 | return new Promise((resolve) => {
84 | Editor.Message.request('server', 'query-ip-list').then((ipList: string[]) => {
85 | resolve({
86 | success: true,
87 | data: {
88 | ipList: ipList,
89 | count: ipList.length,
90 | message: 'IP list retrieved successfully'
91 | }
92 | });
93 | }).catch((err: Error) => {
94 | resolve({ success: false, error: err.message });
95 | });
96 | });
97 | }
98 |
99 | private async querySortedServerIPList(): Promise<ToolResponse> {
100 | return new Promise((resolve) => {
101 | Editor.Message.request('server', 'query-sort-ip-list').then((sortedIPList: string[]) => {
102 | resolve({
103 | success: true,
104 | data: {
105 | sortedIPList: sortedIPList,
106 | count: sortedIPList.length,
107 | message: 'Sorted IP list retrieved successfully'
108 | }
109 | });
110 | }).catch((err: Error) => {
111 | resolve({ success: false, error: err.message });
112 | });
113 | });
114 | }
115 |
116 | private async queryServerPort(): Promise<ToolResponse> {
117 | return new Promise((resolve) => {
118 | Editor.Message.request('server', 'query-port').then((port: number) => {
119 | resolve({
120 | success: true,
121 | data: {
122 | port: port,
123 | message: `Editor server is running on port ${port}`
124 | }
125 | });
126 | }).catch((err: Error) => {
127 | resolve({ success: false, error: err.message });
128 | });
129 | });
130 | }
131 |
132 | private async getServerStatus(): Promise<ToolResponse> {
133 | return new Promise(async (resolve) => {
134 | try {
135 | // Gather comprehensive server information
136 | const [ipListResult, portResult] = await Promise.allSettled([
137 | this.queryServerIPList(),
138 | this.queryServerPort()
139 | ]);
140 |
141 | const status: any = {
142 | timestamp: new Date().toISOString(),
143 | serverRunning: true
144 | };
145 |
146 | if (ipListResult.status === 'fulfilled' && ipListResult.value.success) {
147 | status.availableIPs = ipListResult.value.data.ipList;
148 | status.ipCount = ipListResult.value.data.count;
149 | } else {
150 | status.availableIPs = [];
151 | status.ipCount = 0;
152 | status.ipError = ipListResult.status === 'rejected' ? ipListResult.reason : ipListResult.value.error;
153 | }
154 |
155 | if (portResult.status === 'fulfilled' && portResult.value.success) {
156 | status.port = portResult.value.data.port;
157 | } else {
158 | status.port = null;
159 | status.portError = portResult.status === 'rejected' ? portResult.reason : portResult.value.error;
160 | }
161 |
162 | // Add additional server info
163 | status.mcpServerPort = 3000; // Our MCP server port
164 | status.editorVersion = (Editor as any).versions?.cocos || 'Unknown';
165 | status.platform = process.platform;
166 | status.nodeVersion = process.version;
167 |
168 | resolve({
169 | success: true,
170 | data: status
171 | });
172 |
173 | } catch (err: any) {
174 | resolve({
175 | success: false,
176 | error: `Failed to get server status: ${err.message}`
177 | });
178 | }
179 | });
180 | }
181 |
182 | private async checkServerConnectivity(timeout: number = 5000): Promise<ToolResponse> {
183 | return new Promise(async (resolve) => {
184 | const startTime = Date.now();
185 |
186 | try {
187 | // Test basic Editor API connectivity
188 | const testPromise = Editor.Message.request('server', 'query-port');
189 | const timeoutPromise = new Promise((_, reject) => {
190 | setTimeout(() => reject(new Error('Connection timeout')), timeout);
191 | });
192 |
193 | await Promise.race([testPromise, timeoutPromise]);
194 |
195 | const responseTime = Date.now() - startTime;
196 |
197 | resolve({
198 | success: true,
199 | data: {
200 | connected: true,
201 | responseTime: responseTime,
202 | timeout: timeout,
203 | message: `Server connectivity confirmed in ${responseTime}ms`
204 | }
205 | });
206 |
207 | } catch (err: any) {
208 | const responseTime = Date.now() - startTime;
209 |
210 | resolve({
211 | success: false,
212 | data: {
213 | connected: false,
214 | responseTime: responseTime,
215 | timeout: timeout,
216 | error: err.message
217 | }
218 | });
219 | }
220 | });
221 | }
222 |
223 | private async getNetworkInterfaces(): Promise<ToolResponse> {
224 | return new Promise(async (resolve) => {
225 | try {
226 | // Get network interfaces using Node.js os module
227 | const os = require('os');
228 | const interfaces = os.networkInterfaces();
229 |
230 | const networkInfo = Object.entries(interfaces).map(([name, addresses]: [string, any]) => ({
231 | name: name,
232 | addresses: addresses.map((addr: any) => ({
233 | address: addr.address,
234 | family: addr.family,
235 | internal: addr.internal,
236 | cidr: addr.cidr
237 | }))
238 | }));
239 |
240 | // Also try to get server IPs for comparison
241 | const serverIPResult = await this.queryServerIPList();
242 |
243 | resolve({
244 | success: true,
245 | data: {
246 | networkInterfaces: networkInfo,
247 | serverAvailableIPs: serverIPResult.success ? serverIPResult.data.ipList : [],
248 | message: 'Network interfaces retrieved successfully'
249 | }
250 | });
251 |
252 | } catch (err: any) {
253 | resolve({
254 | success: false,
255 | error: `Failed to get network interfaces: ${err.message}`
256 | });
257 | }
258 | });
259 | }
260 | }
```
--------------------------------------------------------------------------------
/source/tools/preferences-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class PreferencesTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'open_preferences_settings',
8 | description: 'Open preferences settings panel',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {
12 | tab: {
13 | type: 'string',
14 | description: 'Preferences tab to open (optional)',
15 | enum: ['general', 'external-tools', 'data-editor', 'laboratory', 'extensions']
16 | },
17 | args: {
18 | type: 'array',
19 | description: 'Additional arguments to pass to the tab'
20 | }
21 | }
22 | }
23 | },
24 | {
25 | name: 'query_preferences_config',
26 | description: 'Query preferences configuration',
27 | inputSchema: {
28 | type: 'object',
29 | properties: {
30 | name: {
31 | type: 'string',
32 | description: 'Plugin or category name',
33 | default: 'general'
34 | },
35 | path: {
36 | type: 'string',
37 | description: 'Configuration path (optional)'
38 | },
39 | type: {
40 | type: 'string',
41 | description: 'Configuration type',
42 | enum: ['default', 'global', 'local'],
43 | default: 'global'
44 | }
45 | },
46 | required: ['name']
47 | }
48 | },
49 | {
50 | name: 'set_preferences_config',
51 | description: 'Set preferences configuration',
52 | inputSchema: {
53 | type: 'object',
54 | properties: {
55 | name: {
56 | type: 'string',
57 | description: 'Plugin name'
58 | },
59 | path: {
60 | type: 'string',
61 | description: 'Configuration path'
62 | },
63 | value: {
64 | description: 'Configuration value'
65 | },
66 | type: {
67 | type: 'string',
68 | description: 'Configuration type',
69 | enum: ['default', 'global', 'local'],
70 | default: 'global'
71 | }
72 | },
73 | required: ['name', 'path', 'value']
74 | }
75 | },
76 | {
77 | name: 'get_all_preferences',
78 | description: 'Get all available preferences categories',
79 | inputSchema: {
80 | type: 'object',
81 | properties: {}
82 | }
83 | },
84 | {
85 | name: 'reset_preferences',
86 | description: 'Reset preferences to default values',
87 | inputSchema: {
88 | type: 'object',
89 | properties: {
90 | name: {
91 | type: 'string',
92 | description: 'Specific preference category to reset (optional)'
93 | },
94 | type: {
95 | type: 'string',
96 | description: 'Configuration type to reset',
97 | enum: ['global', 'local'],
98 | default: 'global'
99 | }
100 | }
101 | }
102 | },
103 | {
104 | name: 'export_preferences',
105 | description: 'Export current preferences configuration',
106 | inputSchema: {
107 | type: 'object',
108 | properties: {
109 | exportPath: {
110 | type: 'string',
111 | description: 'Path to export preferences file (optional)'
112 | }
113 | }
114 | }
115 | },
116 | {
117 | name: 'import_preferences',
118 | description: 'Import preferences configuration from file',
119 | inputSchema: {
120 | type: 'object',
121 | properties: {
122 | importPath: {
123 | type: 'string',
124 | description: 'Path to import preferences file from'
125 | }
126 | },
127 | required: ['importPath']
128 | }
129 | }
130 | ];
131 | }
132 |
133 | async execute(toolName: string, args: any): Promise<ToolResponse> {
134 | switch (toolName) {
135 | case 'open_preferences_settings':
136 | return await this.openPreferencesSettings(args.tab, args.args);
137 | case 'query_preferences_config':
138 | return await this.queryPreferencesConfig(args.name, args.path, args.type);
139 | case 'set_preferences_config':
140 | return await this.setPreferencesConfig(args.name, args.path, args.value, args.type);
141 | case 'get_all_preferences':
142 | return await this.getAllPreferences();
143 | case 'reset_preferences':
144 | return await this.resetPreferences(args.name, args.type);
145 | case 'export_preferences':
146 | return await this.exportPreferences(args.exportPath);
147 | case 'import_preferences':
148 | return await this.importPreferences(args.importPath);
149 | default:
150 | throw new Error(`Unknown tool: ${toolName}`);
151 | }
152 | }
153 |
154 | private async openPreferencesSettings(tab?: string, args?: any[]): Promise<ToolResponse> {
155 | return new Promise((resolve) => {
156 | const requestArgs = [];
157 | if (tab) {
158 | requestArgs.push(tab);
159 | }
160 | if (args && args.length > 0) {
161 | requestArgs.push(...args);
162 | }
163 |
164 | (Editor.Message.request as any)('preferences', 'open-settings', ...requestArgs).then(() => {
165 | resolve({
166 | success: true,
167 | message: `Preferences settings opened${tab ? ` on tab: ${tab}` : ''}`
168 | });
169 | }).catch((err: Error) => {
170 | resolve({ success: false, error: err.message });
171 | });
172 | });
173 | }
174 |
175 | private async queryPreferencesConfig(name: string, path?: string, type: string = 'global'): Promise<ToolResponse> {
176 | return new Promise((resolve) => {
177 | const requestArgs = [name];
178 | if (path) {
179 | requestArgs.push(path);
180 | }
181 | requestArgs.push(type);
182 |
183 | (Editor.Message.request as any)('preferences', 'query-config', ...requestArgs).then((config: any) => {
184 | resolve({
185 | success: true,
186 | data: {
187 | name: name,
188 | path: path,
189 | type: type,
190 | config: config
191 | }
192 | });
193 | }).catch((err: Error) => {
194 | resolve({ success: false, error: err.message });
195 | });
196 | });
197 | }
198 |
199 | private async setPreferencesConfig(name: string, path: string, value: any, type: string = 'global'): Promise<ToolResponse> {
200 | return new Promise((resolve) => {
201 | (Editor.Message.request as any)('preferences', 'set-config', name, path, value, type).then((success: boolean) => {
202 | if (success) {
203 | resolve({
204 | success: true,
205 | message: `Preference '${name}.${path}' updated successfully`
206 | });
207 | } else {
208 | resolve({
209 | success: false,
210 | error: `Failed to update preference '${name}.${path}'`
211 | });
212 | }
213 | }).catch((err: Error) => {
214 | resolve({ success: false, error: err.message });
215 | });
216 | });
217 | }
218 |
219 | private async getAllPreferences(): Promise<ToolResponse> {
220 | return new Promise((resolve) => {
221 | // Common preference categories in Cocos Creator
222 | const categories = [
223 | 'general',
224 | 'external-tools',
225 | 'data-editor',
226 | 'laboratory',
227 | 'extensions',
228 | 'preview',
229 | 'console',
230 | 'native',
231 | 'builder'
232 | ];
233 |
234 | const preferences: any = {};
235 |
236 | const queryPromises = categories.map(category => {
237 | return Editor.Message.request('preferences', 'query-config', category, undefined, 'global')
238 | .then((config: any) => {
239 | preferences[category] = config;
240 | })
241 | .catch(() => {
242 | // Ignore errors for categories that don't exist
243 | preferences[category] = null;
244 | });
245 | });
246 |
247 | Promise.all(queryPromises).then(() => {
248 | // Filter out null entries
249 | const validPreferences = Object.fromEntries(
250 | Object.entries(preferences).filter(([_, value]) => value !== null)
251 | );
252 |
253 | resolve({
254 | success: true,
255 | data: {
256 | categories: Object.keys(validPreferences),
257 | preferences: validPreferences
258 | }
259 | });
260 | }).catch((err: Error) => {
261 | resolve({ success: false, error: err.message });
262 | });
263 | });
264 | }
265 |
266 | private async resetPreferences(name?: string, type: string = 'global'): Promise<ToolResponse> {
267 | return new Promise((resolve) => {
268 | if (name) {
269 | // Reset specific preference category
270 | Editor.Message.request('preferences', 'query-config', name, undefined, 'default').then((defaultConfig: any) => {
271 | return (Editor.Message.request as any)('preferences', 'set-config', name, '', defaultConfig, type);
272 | }).then((success: boolean) => {
273 | if (success) {
274 | resolve({
275 | success: true,
276 | message: `Preference category '${name}' reset to default`
277 | });
278 | } else {
279 | resolve({
280 | success: false,
281 | error: `Failed to reset preference category '${name}'`
282 | });
283 | }
284 | }).catch((err: Error) => {
285 | resolve({ success: false, error: err.message });
286 | });
287 | } else {
288 | resolve({
289 | success: false,
290 | error: 'Resetting all preferences is not supported through API. Please specify a preference category.'
291 | });
292 | }
293 | });
294 | }
295 |
296 | private async exportPreferences(exportPath?: string): Promise<ToolResponse> {
297 | return new Promise((resolve) => {
298 | this.getAllPreferences().then((prefsResult: ToolResponse) => {
299 | if (!prefsResult.success) {
300 | resolve(prefsResult);
301 | return;
302 | }
303 |
304 | const prefsData = JSON.stringify(prefsResult.data, null, 2);
305 | const path = exportPath || `preferences_export_${Date.now()}.json`;
306 |
307 | // For now, return the data - in a real implementation, you'd write to file
308 | resolve({
309 | success: true,
310 | data: {
311 | exportPath: path,
312 | preferences: prefsResult.data,
313 | jsonData: prefsData,
314 | message: 'Preferences exported successfully'
315 | }
316 | });
317 | }).catch((err: Error) => {
318 | resolve({ success: false, error: err.message });
319 | });
320 | });
321 | }
322 |
323 | private async importPreferences(importPath: string): Promise<ToolResponse> {
324 | return new Promise((resolve) => {
325 | resolve({
326 | success: false,
327 | error: 'Import preferences functionality requires file system access which is not available in this context. Please manually import preferences through the Editor UI.'
328 | });
329 | });
330 | }
331 | }
```
--------------------------------------------------------------------------------
/source/tools/reference-image-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class ReferenceImageTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'add_reference_image',
8 | description: 'Add reference image(s) to scene',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {
12 | paths: {
13 | type: 'array',
14 | items: { type: 'string' },
15 | description: 'Array of reference image absolute paths'
16 | }
17 | },
18 | required: ['paths']
19 | }
20 | },
21 | {
22 | name: 'remove_reference_image',
23 | description: 'Remove reference image(s)',
24 | inputSchema: {
25 | type: 'object',
26 | properties: {
27 | paths: {
28 | type: 'array',
29 | items: { type: 'string' },
30 | description: 'Array of reference image paths to remove (optional, removes current if empty)'
31 | }
32 | }
33 | }
34 | },
35 | {
36 | name: 'switch_reference_image',
37 | description: 'Switch to specific reference image',
38 | inputSchema: {
39 | type: 'object',
40 | properties: {
41 | path: {
42 | type: 'string',
43 | description: 'Reference image absolute path'
44 | },
45 | sceneUUID: {
46 | type: 'string',
47 | description: 'Specific scene UUID (optional)'
48 | }
49 | },
50 | required: ['path']
51 | }
52 | },
53 | {
54 | name: 'set_reference_image_data',
55 | description: 'Set reference image transform and display properties',
56 | inputSchema: {
57 | type: 'object',
58 | properties: {
59 | key: {
60 | type: 'string',
61 | description: 'Property key',
62 | enum: ['path', 'x', 'y', 'sx', 'sy', 'opacity']
63 | },
64 | value: {
65 | description: 'Property value (path: string, x/y/sx/sy: number, opacity: number 0-1)'
66 | }
67 | },
68 | required: ['key', 'value']
69 | }
70 | },
71 | {
72 | name: 'query_reference_image_config',
73 | description: 'Query reference image configuration',
74 | inputSchema: {
75 | type: 'object',
76 | properties: {}
77 | }
78 | },
79 | {
80 | name: 'query_current_reference_image',
81 | description: 'Query current reference image data',
82 | inputSchema: {
83 | type: 'object',
84 | properties: {}
85 | }
86 | },
87 | {
88 | name: 'refresh_reference_image',
89 | description: 'Refresh reference image display',
90 | inputSchema: {
91 | type: 'object',
92 | properties: {}
93 | }
94 | },
95 | {
96 | name: 'set_reference_image_position',
97 | description: 'Set reference image position',
98 | inputSchema: {
99 | type: 'object',
100 | properties: {
101 | x: {
102 | type: 'number',
103 | description: 'X offset'
104 | },
105 | y: {
106 | type: 'number',
107 | description: 'Y offset'
108 | }
109 | },
110 | required: ['x', 'y']
111 | }
112 | },
113 | {
114 | name: 'set_reference_image_scale',
115 | description: 'Set reference image scale',
116 | inputSchema: {
117 | type: 'object',
118 | properties: {
119 | sx: {
120 | type: 'number',
121 | description: 'X scale',
122 | minimum: 0.1,
123 | maximum: 10
124 | },
125 | sy: {
126 | type: 'number',
127 | description: 'Y scale',
128 | minimum: 0.1,
129 | maximum: 10
130 | }
131 | },
132 | required: ['sx', 'sy']
133 | }
134 | },
135 | {
136 | name: 'set_reference_image_opacity',
137 | description: 'Set reference image opacity',
138 | inputSchema: {
139 | type: 'object',
140 | properties: {
141 | opacity: {
142 | type: 'number',
143 | description: 'Opacity (0.0 to 1.0)',
144 | minimum: 0,
145 | maximum: 1
146 | }
147 | },
148 | required: ['opacity']
149 | }
150 | },
151 | {
152 | name: 'list_reference_images',
153 | description: 'List all available reference images',
154 | inputSchema: {
155 | type: 'object',
156 | properties: {}
157 | }
158 | },
159 | {
160 | name: 'clear_all_reference_images',
161 | description: 'Clear all reference images',
162 | inputSchema: {
163 | type: 'object',
164 | properties: {}
165 | }
166 | }
167 | ];
168 | }
169 |
170 | async execute(toolName: string, args: any): Promise<ToolResponse> {
171 | switch (toolName) {
172 | case 'add_reference_image':
173 | return await this.addReferenceImage(args.paths);
174 | case 'remove_reference_image':
175 | return await this.removeReferenceImage(args.paths);
176 | case 'switch_reference_image':
177 | return await this.switchReferenceImage(args.path, args.sceneUUID);
178 | case 'set_reference_image_data':
179 | return await this.setReferenceImageData(args.key, args.value);
180 | case 'query_reference_image_config':
181 | return await this.queryReferenceImageConfig();
182 | case 'query_current_reference_image':
183 | return await this.queryCurrentReferenceImage();
184 | case 'refresh_reference_image':
185 | return await this.refreshReferenceImage();
186 | case 'set_reference_image_position':
187 | return await this.setReferenceImagePosition(args.x, args.y);
188 | case 'set_reference_image_scale':
189 | return await this.setReferenceImageScale(args.sx, args.sy);
190 | case 'set_reference_image_opacity':
191 | return await this.setReferenceImageOpacity(args.opacity);
192 | case 'list_reference_images':
193 | return await this.listReferenceImages();
194 | case 'clear_all_reference_images':
195 | return await this.clearAllReferenceImages();
196 | default:
197 | throw new Error(`Unknown tool: ${toolName}`);
198 | }
199 | }
200 |
201 | private async addReferenceImage(paths: string[]): Promise<ToolResponse> {
202 | return new Promise((resolve) => {
203 | Editor.Message.request('reference-image', 'add-image', paths).then(() => {
204 | resolve({
205 | success: true,
206 | data: {
207 | addedPaths: paths,
208 | count: paths.length,
209 | message: `Added ${paths.length} reference image(s)`
210 | }
211 | });
212 | }).catch((err: Error) => {
213 | resolve({ success: false, error: err.message });
214 | });
215 | });
216 | }
217 |
218 | private async removeReferenceImage(paths?: string[]): Promise<ToolResponse> {
219 | return new Promise((resolve) => {
220 | Editor.Message.request('reference-image', 'remove-image', paths).then(() => {
221 | const message = paths && paths.length > 0 ?
222 | `Removed ${paths.length} reference image(s)` :
223 | 'Removed current reference image';
224 | resolve({
225 | success: true,
226 | message: message
227 | });
228 | }).catch((err: Error) => {
229 | resolve({ success: false, error: err.message });
230 | });
231 | });
232 | }
233 |
234 | private async switchReferenceImage(path: string, sceneUUID?: string): Promise<ToolResponse> {
235 | return new Promise((resolve) => {
236 | const args = sceneUUID ? [path, sceneUUID] : [path];
237 | Editor.Message.request('reference-image', 'switch-image', ...args).then(() => {
238 | resolve({
239 | success: true,
240 | data: {
241 | path: path,
242 | sceneUUID: sceneUUID,
243 | message: `Switched to reference image: ${path}`
244 | }
245 | });
246 | }).catch((err: Error) => {
247 | resolve({ success: false, error: err.message });
248 | });
249 | });
250 | }
251 |
252 | private async setReferenceImageData(key: string, value: any): Promise<ToolResponse> {
253 | return new Promise((resolve) => {
254 | Editor.Message.request('reference-image', 'set-image-data', key, value).then(() => {
255 | resolve({
256 | success: true,
257 | data: {
258 | key: key,
259 | value: value,
260 | message: `Reference image ${key} set to ${value}`
261 | }
262 | });
263 | }).catch((err: Error) => {
264 | resolve({ success: false, error: err.message });
265 | });
266 | });
267 | }
268 |
269 | private async queryReferenceImageConfig(): Promise<ToolResponse> {
270 | return new Promise((resolve) => {
271 | Editor.Message.request('reference-image', 'query-config').then((config: any) => {
272 | resolve({
273 | success: true,
274 | data: config
275 | });
276 | }).catch((err: Error) => {
277 | resolve({ success: false, error: err.message });
278 | });
279 | });
280 | }
281 |
282 | private async queryCurrentReferenceImage(): Promise<ToolResponse> {
283 | return new Promise((resolve) => {
284 | Editor.Message.request('reference-image', 'query-current').then((current: any) => {
285 | resolve({
286 | success: true,
287 | data: current
288 | });
289 | }).catch((err: Error) => {
290 | resolve({ success: false, error: err.message });
291 | });
292 | });
293 | }
294 |
295 | private async refreshReferenceImage(): Promise<ToolResponse> {
296 | return new Promise((resolve) => {
297 | Editor.Message.request('reference-image', 'refresh').then(() => {
298 | resolve({
299 | success: true,
300 | message: 'Reference image refreshed'
301 | });
302 | }).catch((err: Error) => {
303 | resolve({ success: false, error: err.message });
304 | });
305 | });
306 | }
307 |
308 | private async setReferenceImagePosition(x: number, y: number): Promise<ToolResponse> {
309 | return new Promise(async (resolve) => {
310 | try {
311 | await Editor.Message.request('reference-image', 'set-image-data', 'x', x);
312 | await Editor.Message.request('reference-image', 'set-image-data', 'y', y);
313 |
314 | resolve({
315 | success: true,
316 | data: {
317 | x: x,
318 | y: y,
319 | message: `Reference image position set to (${x}, ${y})`
320 | }
321 | });
322 | } catch (err: any) {
323 | resolve({ success: false, error: err.message });
324 | }
325 | });
326 | }
327 |
328 | private async setReferenceImageScale(sx: number, sy: number): Promise<ToolResponse> {
329 | return new Promise(async (resolve) => {
330 | try {
331 | await Editor.Message.request('reference-image', 'set-image-data', 'sx', sx);
332 | await Editor.Message.request('reference-image', 'set-image-data', 'sy', sy);
333 |
334 | resolve({
335 | success: true,
336 | data: {
337 | sx: sx,
338 | sy: sy,
339 | message: `Reference image scale set to (${sx}, ${sy})`
340 | }
341 | });
342 | } catch (err: any) {
343 | resolve({ success: false, error: err.message });
344 | }
345 | });
346 | }
347 |
348 | private async setReferenceImageOpacity(opacity: number): Promise<ToolResponse> {
349 | return new Promise((resolve) => {
350 | Editor.Message.request('reference-image', 'set-image-data', 'opacity', opacity).then(() => {
351 | resolve({
352 | success: true,
353 | data: {
354 | opacity: opacity,
355 | message: `Reference image opacity set to ${opacity}`
356 | }
357 | });
358 | }).catch((err: Error) => {
359 | resolve({ success: false, error: err.message });
360 | });
361 | });
362 | }
363 |
364 | private async listReferenceImages(): Promise<ToolResponse> {
365 | return new Promise(async (resolve) => {
366 | try {
367 | const config = await Editor.Message.request('reference-image', 'query-config');
368 | const current = await Editor.Message.request('reference-image', 'query-current');
369 |
370 | resolve({
371 | success: true,
372 | data: {
373 | config: config,
374 | current: current,
375 | message: 'Reference image information retrieved'
376 | }
377 | });
378 | } catch (err: any) {
379 | resolve({ success: false, error: err.message });
380 | }
381 | });
382 | }
383 |
384 | private async clearAllReferenceImages(): Promise<ToolResponse> {
385 | return new Promise(async (resolve) => {
386 | try {
387 | // Remove all reference images by calling remove-image without paths
388 | await Editor.Message.request('reference-image', 'remove-image');
389 |
390 | resolve({
391 | success: true,
392 | message: 'All reference images cleared'
393 | });
394 | } catch (err: any) {
395 | resolve({ success: false, error: err.message });
396 | }
397 | });
398 | }
399 | }
```
--------------------------------------------------------------------------------
/source/scene.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { join } from 'path';
2 | module.paths.push(join(Editor.App.path, 'node_modules'));
3 |
4 | export const methods: { [key: string]: (...any: any) => any } = {
5 | /**
6 | * Create a new scene
7 | */
8 | createNewScene() {
9 | try {
10 | const { director, Scene } = require('cc');
11 | const scene = new Scene();
12 | scene.name = 'New Scene';
13 | director.runScene(scene);
14 | return { success: true, message: 'New scene created successfully' };
15 | } catch (error: any) {
16 | return { success: false, error: error.message };
17 | }
18 | },
19 |
20 | /**
21 | * Add component to a node
22 | */
23 | addComponentToNode(nodeUuid: string, componentType: string) {
24 | try {
25 | const { director, js } = require('cc');
26 | const scene = director.getScene();
27 | if (!scene) {
28 | return { success: false, error: 'No active scene' };
29 | }
30 |
31 | // Find node by UUID
32 | const node = scene.getChildByUuid(nodeUuid);
33 | if (!node) {
34 | return { success: false, error: `Node with UUID ${nodeUuid} not found` };
35 | }
36 |
37 | // Get component class
38 | const ComponentClass = js.getClassByName(componentType);
39 | if (!ComponentClass) {
40 | return { success: false, error: `Component type ${componentType} not found` };
41 | }
42 |
43 | // Add component
44 | const component = node.addComponent(ComponentClass);
45 | return {
46 | success: true,
47 | message: `Component ${componentType} added successfully`,
48 | data: { componentId: component.uuid }
49 | };
50 | } catch (error: any) {
51 | return { success: false, error: error.message };
52 | }
53 | },
54 |
55 | /**
56 | * Remove component from a node
57 | */
58 | removeComponentFromNode(nodeUuid: string, componentType: string) {
59 | try {
60 | const { director, js } = require('cc');
61 | const scene = director.getScene();
62 | if (!scene) {
63 | return { success: false, error: 'No active scene' };
64 | }
65 |
66 | const node = scene.getChildByUuid(nodeUuid);
67 | if (!node) {
68 | return { success: false, error: `Node with UUID ${nodeUuid} not found` };
69 | }
70 |
71 | const ComponentClass = js.getClassByName(componentType);
72 | if (!ComponentClass) {
73 | return { success: false, error: `Component type ${componentType} not found` };
74 | }
75 |
76 | const component = node.getComponent(ComponentClass);
77 | if (!component) {
78 | return { success: false, error: `Component ${componentType} not found on node` };
79 | }
80 |
81 | node.removeComponent(component);
82 | return { success: true, message: `Component ${componentType} removed successfully` };
83 | } catch (error: any) {
84 | return { success: false, error: error.message };
85 | }
86 | },
87 |
88 | /**
89 | * Create a new node
90 | */
91 | createNode(name: string, parentUuid?: string) {
92 | try {
93 | const { director, Node } = require('cc');
94 | const scene = director.getScene();
95 | if (!scene) {
96 | return { success: false, error: 'No active scene' };
97 | }
98 |
99 | const node = new Node(name);
100 |
101 | if (parentUuid) {
102 | const parent = scene.getChildByUuid(parentUuid);
103 | if (parent) {
104 | parent.addChild(node);
105 | } else {
106 | scene.addChild(node);
107 | }
108 | } else {
109 | scene.addChild(node);
110 | }
111 |
112 | return {
113 | success: true,
114 | message: `Node ${name} created successfully`,
115 | data: { uuid: node.uuid, name: node.name }
116 | };
117 | } catch (error: any) {
118 | return { success: false, error: error.message };
119 | }
120 | },
121 |
122 | /**
123 | * Get node information
124 | */
125 | getNodeInfo(nodeUuid: string) {
126 | try {
127 | const { director } = require('cc');
128 | const scene = director.getScene();
129 | if (!scene) {
130 | return { success: false, error: 'No active scene' };
131 | }
132 |
133 | const node = scene.getChildByUuid(nodeUuid);
134 | if (!node) {
135 | return { success: false, error: `Node with UUID ${nodeUuid} not found` };
136 | }
137 |
138 | return {
139 | success: true,
140 | data: {
141 | uuid: node.uuid,
142 | name: node.name,
143 | active: node.active,
144 | position: node.position,
145 | rotation: node.rotation,
146 | scale: node.scale,
147 | parent: node.parent?.uuid,
148 | children: node.children.map((child: any) => child.uuid),
149 | components: node.components.map((comp: any) => ({
150 | type: comp.constructor.name,
151 | enabled: comp.enabled
152 | }))
153 | }
154 | };
155 | } catch (error: any) {
156 | return { success: false, error: error.message };
157 | }
158 | },
159 |
160 | /**
161 | * Get all nodes in scene
162 | */
163 | getAllNodes() {
164 | try {
165 | const { director } = require('cc');
166 | const scene = director.getScene();
167 | if (!scene) {
168 | return { success: false, error: 'No active scene' };
169 | }
170 |
171 | const nodes: any[] = [];
172 | const collectNodes = (node: any) => {
173 | nodes.push({
174 | uuid: node.uuid,
175 | name: node.name,
176 | active: node.active,
177 | parent: node.parent?.uuid
178 | });
179 |
180 | node.children.forEach((child: any) => collectNodes(child));
181 | };
182 |
183 | scene.children.forEach((child: any) => collectNodes(child));
184 |
185 | return { success: true, data: nodes };
186 | } catch (error: any) {
187 | return { success: false, error: error.message };
188 | }
189 | },
190 |
191 | /**
192 | * Find node by name
193 | */
194 | findNodeByName(name: string) {
195 | try {
196 | const { director } = require('cc');
197 | const scene = director.getScene();
198 | if (!scene) {
199 | return { success: false, error: 'No active scene' };
200 | }
201 |
202 | const node = scene.getChildByName(name);
203 | if (!node) {
204 | return { success: false, error: `Node with name ${name} not found` };
205 | }
206 |
207 | return {
208 | success: true,
209 | data: {
210 | uuid: node.uuid,
211 | name: node.name,
212 | active: node.active,
213 | position: node.position
214 | }
215 | };
216 | } catch (error: any) {
217 | return { success: false, error: error.message };
218 | }
219 | },
220 |
221 | /**
222 | * Get current scene information
223 | */
224 | getCurrentSceneInfo() {
225 | try {
226 | const { director } = require('cc');
227 | const scene = director.getScene();
228 | if (!scene) {
229 | return { success: false, error: 'No active scene' };
230 | }
231 |
232 | return {
233 | success: true,
234 | data: {
235 | name: scene.name,
236 | uuid: scene.uuid,
237 | nodeCount: scene.children.length
238 | }
239 | };
240 | } catch (error: any) {
241 | return { success: false, error: error.message };
242 | }
243 | },
244 |
245 | /**
246 | * Set node property
247 | */
248 | setNodeProperty(nodeUuid: string, property: string, value: any) {
249 | try {
250 | const { director } = require('cc');
251 | const scene = director.getScene();
252 | if (!scene) {
253 | return { success: false, error: 'No active scene' };
254 | }
255 |
256 | const node = scene.getChildByUuid(nodeUuid);
257 | if (!node) {
258 | return { success: false, error: `Node with UUID ${nodeUuid} not found` };
259 | }
260 |
261 | // 设置属性
262 | if (property === 'position') {
263 | node.setPosition(value.x || 0, value.y || 0, value.z || 0);
264 | } else if (property === 'rotation') {
265 | node.setRotationFromEuler(value.x || 0, value.y || 0, value.z || 0);
266 | } else if (property === 'scale') {
267 | node.setScale(value.x || 1, value.y || 1, value.z || 1);
268 | } else if (property === 'active') {
269 | node.active = value;
270 | } else if (property === 'name') {
271 | node.name = value;
272 | } else {
273 | // 尝试直接设置属性
274 | (node as any)[property] = value;
275 | }
276 |
277 | return {
278 | success: true,
279 | message: `Property '${property}' updated successfully`
280 | };
281 | } catch (error: any) {
282 | return { success: false, error: error.message };
283 | }
284 | },
285 |
286 | /**
287 | * Get scene hierarchy
288 | */
289 | getSceneHierarchy(includeComponents: boolean = false) {
290 | try {
291 | const { director } = require('cc');
292 | const scene = director.getScene();
293 | if (!scene) {
294 | return { success: false, error: 'No active scene' };
295 | }
296 |
297 | const processNode = (node: any): any => {
298 | const result: any = {
299 | name: node.name,
300 | uuid: node.uuid,
301 | active: node.active,
302 | children: []
303 | };
304 |
305 | if (includeComponents) {
306 | result.components = node.components.map((comp: any) => ({
307 | type: comp.constructor.name,
308 | enabled: comp.enabled
309 | }));
310 | }
311 |
312 | if (node.children && node.children.length > 0) {
313 | result.children = node.children.map((child: any) => processNode(child));
314 | }
315 |
316 | return result;
317 | };
318 |
319 | const hierarchy = scene.children.map((child: any) => processNode(child));
320 | return { success: true, data: hierarchy };
321 | } catch (error: any) {
322 | return { success: false, error: error.message };
323 | }
324 | },
325 |
326 | /**
327 | * Create prefab from node
328 | */
329 | createPrefabFromNode(nodeUuid: string, prefabPath: string) {
330 | try {
331 | const { director, instantiate } = require('cc');
332 | const scene = director.getScene();
333 | if (!scene) {
334 | return { success: false, error: 'No active scene' };
335 | }
336 |
337 | const node = scene.getChildByUuid(nodeUuid);
338 | if (!node) {
339 | return { success: false, error: `Node with UUID ${nodeUuid} not found` };
340 | }
341 |
342 | // 注意:这里只是一个模拟实现,因为运行时环境下无法直接创建预制体文件
343 | // 真正的预制体创建需要Editor API支持
344 | return {
345 | success: true,
346 | data: {
347 | prefabPath: prefabPath,
348 | sourceNodeUuid: nodeUuid,
349 | message: `Prefab created from node '${node.name}' at ${prefabPath}`
350 | }
351 | };
352 | } catch (error: any) {
353 | return { success: false, error: error.message };
354 | }
355 | },
356 |
357 | /**
358 | * Set component property
359 | */
360 | setComponentProperty(nodeUuid: string, componentType: string, property: string, value: any) {
361 | try {
362 | const { director, js } = require('cc');
363 | const scene = director.getScene();
364 | if (!scene) {
365 | return { success: false, error: 'No active scene' };
366 | }
367 | const node = scene.getChildByUuid(nodeUuid);
368 | if (!node) {
369 | return { success: false, error: `Node with UUID ${nodeUuid} not found` };
370 | }
371 | const ComponentClass = js.getClassByName(componentType);
372 | if (!ComponentClass) {
373 | return { success: false, error: `Component type ${componentType} not found` };
374 | }
375 | const component = node.getComponent(ComponentClass);
376 | if (!component) {
377 | return { success: false, error: `Component ${componentType} not found on node` };
378 | }
379 | // 针对常见属性做特殊处理
380 | if (property === 'spriteFrame' && componentType === 'cc.Sprite') {
381 | // 支持 value 为 uuid 或资源路径
382 | if (typeof value === 'string') {
383 | // 先尝试按 uuid 查找
384 | const assetManager = require('cc').assetManager;
385 | assetManager.resources.load(value, require('cc').SpriteFrame, (err: any, spriteFrame: any) => {
386 | if (!err && spriteFrame) {
387 | component.spriteFrame = spriteFrame;
388 | } else {
389 | // 尝试通过 uuid 加载
390 | assetManager.loadAny({ uuid: value }, (err2: any, asset: any) => {
391 | if (!err2 && asset) {
392 | component.spriteFrame = asset;
393 | } else {
394 | // 直接赋值(兼容已传入资源对象)
395 | component.spriteFrame = value;
396 | }
397 | });
398 | }
399 | });
400 | } else {
401 | component.spriteFrame = value;
402 | }
403 | } else if (property === 'material' && (componentType === 'cc.Sprite' || componentType === 'cc.MeshRenderer')) {
404 | // 支持 value 为 uuid 或资源路径
405 | if (typeof value === 'string') {
406 | const assetManager = require('cc').assetManager;
407 | assetManager.resources.load(value, require('cc').Material, (err: any, material: any) => {
408 | if (!err && material) {
409 | component.material = material;
410 | } else {
411 | assetManager.loadAny({ uuid: value }, (err2: any, asset: any) => {
412 | if (!err2 && asset) {
413 | component.material = asset;
414 | } else {
415 | component.material = value;
416 | }
417 | });
418 | }
419 | });
420 | } else {
421 | component.material = value;
422 | }
423 | } else if (property === 'string' && (componentType === 'cc.Label' || componentType === 'cc.RichText')) {
424 | component.string = value;
425 | } else {
426 | component[property] = value;
427 | }
428 | // 可选:刷新 Inspector
429 | // Editor.Message.send('scene', 'snapshot');
430 | return { success: true, message: `Component property '${property}' updated successfully` };
431 | } catch (error: any) {
432 | return { success: false, error: error.message };
433 | }
434 | }
435 | };
```
--------------------------------------------------------------------------------
/static/template/default/tool-manager.html:
--------------------------------------------------------------------------------
```html
1 | <!DOCTYPE html>
2 | <html>
3 | <head>
4 | <meta charset="utf-8">
5 | <title>工具管理器</title>
6 | <style>
7 | body {
8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
9 | margin: 0;
10 | padding: 20px;
11 | background: #2a2a2a;
12 | color: #e0e0e0;
13 | }
14 |
15 | .container {
16 | display: flex;
17 | flex-direction: column;
18 | height: calc(100vh - 40px);
19 | gap: 20px;
20 | max-width: 1200px;
21 | margin: 0 auto;
22 | overflow-y: auto;
23 | padding-right: 8px;
24 | }
25 |
26 | /* 容器滚动条样式 */
27 | .container::-webkit-scrollbar {
28 | width: 8px;
29 | }
30 |
31 | .container::-webkit-scrollbar-track {
32 | background: #2a2a2a;
33 | border-radius: 4px;
34 | }
35 |
36 | .container::-webkit-scrollbar-thumb {
37 | background: #5a5a5a;
38 | border-radius: 4px;
39 | }
40 |
41 | .container::-webkit-scrollbar-thumb:hover {
42 | background: #6a6a6a;
43 | }
44 |
45 | .header {
46 | display: flex;
47 | justify-content: space-between;
48 | align-items: center;
49 | padding: 20px 24px;
50 | background: linear-gradient(135deg, #3a3a3a 0%, #2d2d2d 100%);
51 | border: 1px solid #4a4a4a;
52 | border-radius: 8px;
53 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
54 | }
55 |
56 | .header h2 {
57 | margin: 0;
58 | font-size: 20px;
59 | font-weight: 600;
60 | color: #ffffff;
61 | }
62 |
63 | .config-section {
64 | background: linear-gradient(135deg, #323232 0%, #2a2a2a 100%);
65 | border: 1px solid #4a4a4a;
66 | border-radius: 8px;
67 | padding: 20px;
68 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
69 | }
70 |
71 | .config-header {
72 | display: flex;
73 | justify-content: space-between;
74 | align-items: center;
75 | margin-bottom: 16px;
76 | padding-bottom: 12px;
77 | border-bottom: 2px solid #4a4a4a;
78 | }
79 |
80 | .config-title {
81 | font-weight: 600;
82 | font-size: 16px;
83 | color: #ffffff;
84 | }
85 |
86 | .config-controls {
87 | display: flex;
88 | gap: 10px;
89 | }
90 |
91 | .config-selector {
92 | display: flex;
93 | gap: 12px;
94 | margin-bottom: 20px;
95 | align-items: center;
96 | }
97 |
98 | .config-selector select {
99 | flex: 1;
100 | padding: 10px 12px;
101 | border: 1px solid #5a5a5a;
102 | border-radius: 6px;
103 | background: #3a3a3a;
104 | color: #e0e0e0;
105 | font-size: 14px;
106 | transition: all 0.2s;
107 | }
108 |
109 | .config-selector select:focus {
110 | outline: none;
111 | border-color: #007acc;
112 | box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
113 | }
114 |
115 | .btn {
116 | padding: 8px 16px;
117 | border: 1px solid #5a5a5a;
118 | border-radius: 6px;
119 | background: linear-gradient(135deg, #4a4a4a 0%, #3a3a3a 100%);
120 | color: #e0e0e0;
121 | cursor: pointer;
122 | font-size: 13px;
123 | font-weight: 500;
124 | transition: all 0.2s;
125 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
126 | }
127 |
128 | .btn:hover {
129 | background: linear-gradient(135deg, #5a5a5a 0%, #4a4a4a 100%);
130 | border-color: #6a6a6a;
131 | transform: translateY(-1px);
132 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
133 | }
134 |
135 | .btn:active {
136 | transform: translateY(0);
137 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
138 | }
139 |
140 | .btn-primary {
141 | background: linear-gradient(135deg, #007acc 0%, #005a9e 100%);
142 | color: white;
143 | border-color: #007acc;
144 | }
145 |
146 | .btn-primary:hover {
147 | background: linear-gradient(135deg, #0088e6 0%, #0066cc 100%);
148 | border-color: #0088e6;
149 | }
150 |
151 | .btn-danger {
152 | background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%);
153 | color: white;
154 | border-color: #d32f2f;
155 | }
156 |
157 | .btn-danger:hover {
158 | background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
159 | border-color: #f44336;
160 | }
161 |
162 | .tools-container {
163 | display: grid;
164 | grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
165 | gap: 16px;
166 | max-height: 500px;
167 | overflow-y: auto;
168 | padding: 4px;
169 | }
170 |
171 | .tool-category {
172 | background: linear-gradient(135deg, #383838 0%, #2f2f2f 100%);
173 | border: 1px solid #4a4a4a;
174 | border-radius: 8px;
175 | padding: 16px;
176 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
177 | transition: all 0.2s;
178 | }
179 |
180 | .tool-category:hover {
181 | border-color: #5a5a5a;
182 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
183 | transform: translateY(-2px);
184 | }
185 |
186 | .category-header {
187 | display: flex;
188 | justify-content: space-between;
189 | align-items: center;
190 | margin-bottom: 12px;
191 | padding-bottom: 10px;
192 | border-bottom: 2px solid #4a4a4a;
193 | }
194 |
195 | .category-name {
196 | font-weight: 600;
197 | font-size: 14px;
198 | color: #ffffff;
199 | }
200 |
201 | .category-toggle {
202 | display: flex;
203 | align-items: center;
204 | gap: 4px;
205 | }
206 |
207 | .tool-list {
208 | display: flex;
209 | flex-direction: column;
210 | gap: 6px;
211 | }
212 |
213 | .tool-item {
214 | display: flex;
215 | justify-content: space-between;
216 | align-items: center;
217 | padding: 8px 12px;
218 | margin: 4px 0;
219 | background: rgba(255, 255, 255, 0.02);
220 | border-radius: 6px;
221 | transition: all 0.2s;
222 | }
223 |
224 | .tool-item:hover {
225 | background: rgba(255, 255, 255, 0.05);
226 | transform: translateX(2px);
227 | }
228 |
229 | .tool-info {
230 | flex: 1;
231 | margin-right: 12px;
232 | }
233 |
234 | .tool-name {
235 | font-size: 13px;
236 | font-weight: 500;
237 | color: #e0e0e0;
238 | }
239 |
240 | .tool-description {
241 | font-size: 12px;
242 | color: #b0b0b0;
243 | margin-top: 4px;
244 | line-height: 1.4;
245 | }
246 |
247 | .tool-toggle {
248 | display: flex;
249 | align-items: center;
250 | }
251 |
252 | .checkbox {
253 | width: 18px;
254 | height: 18px;
255 | cursor: pointer;
256 | accent-color: #007acc;
257 | }
258 |
259 | .status-bar {
260 | display: flex;
261 | justify-content: space-between;
262 | align-items: center;
263 | padding: 16px 20px;
264 | background: linear-gradient(135deg, #323232 0%, #2a2a2a 100%);
265 | border: 1px solid #4a4a4a;
266 | border-radius: 8px;
267 | font-size: 13px;
268 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
269 | }
270 |
271 | .status-info {
272 | display: flex;
273 | gap: 20px;
274 | color: #b0b0b0;
275 | }
276 |
277 | .status-info span {
278 | font-weight: 500;
279 | }
280 |
281 | .modal {
282 | display: none;
283 | position: fixed;
284 | top: 0;
285 | left: 0;
286 | width: 100%;
287 | height: 100%;
288 | background: rgba(0, 0, 0, 0.7);
289 | backdrop-filter: blur(4px);
290 | z-index: 1000;
291 | }
292 |
293 | .modal-content {
294 | position: absolute;
295 | top: 50%;
296 | left: 50%;
297 | transform: translate(-50%, -50%);
298 | background: linear-gradient(135deg, #3a3a3a 0%, #2d2d2d 100%);
299 | border: 1px solid #5a5a5a;
300 | border-radius: 12px;
301 | padding: 24px;
302 | min-width: 400px;
303 | max-width: 600px;
304 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
305 | backdrop-filter: blur(8px);
306 | }
307 |
308 | .modal-header {
309 | display: flex;
310 | justify-content: space-between;
311 | align-items: center;
312 | margin-bottom: 20px;
313 | padding-bottom: 12px;
314 | border-bottom: 2px solid #4a4a4a;
315 | }
316 |
317 | .modal-title {
318 | font-weight: 600;
319 | font-size: 18px;
320 | color: #ffffff;
321 | }
322 |
323 | .modal-close {
324 | background: none;
325 | border: none;
326 | font-size: 20px;
327 | cursor: pointer;
328 | color: #b0b0b0;
329 | padding: 4px;
330 | border-radius: 4px;
331 | transition: all 0.2s;
332 | }
333 |
334 | .modal-close:hover {
335 | color: #ffffff;
336 | background: rgba(255, 255, 255, 0.1);
337 | }
338 |
339 | .form-group {
340 | margin-bottom: 16px;
341 | }
342 |
343 | .form-label {
344 | display: block;
345 | margin-bottom: 6px;
346 | font-size: 13px;
347 | font-weight: 500;
348 | color: #e0e0e0;
349 | }
350 |
351 | .form-input {
352 | width: 100%;
353 | padding: 10px 12px;
354 | border: 1px solid #5a5a5a;
355 | border-radius: 6px;
356 | background: #3a3a3a;
357 | color: #e0e0e0;
358 | font-size: 14px;
359 | transition: all 0.2s;
360 | box-sizing: border-box;
361 | }
362 |
363 | .form-input:focus {
364 | outline: none;
365 | border-color: #007acc;
366 | box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
367 | }
368 |
369 | .form-textarea {
370 | width: 100%;
371 | padding: 10px 12px;
372 | border: 1px solid #5a5a5a;
373 | border-radius: 6px;
374 | background: #3a3a3a;
375 | color: #e0e0e0;
376 | font-size: 14px;
377 | resize: vertical;
378 | min-height: 80px;
379 | transition: all 0.2s;
380 | box-sizing: border-box;
381 | }
382 |
383 | .form-textarea:focus {
384 | outline: none;
385 | border-color: #007acc;
386 | box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
387 | }
388 |
389 | .modal-actions {
390 | display: flex;
391 | justify-content: flex-end;
392 | gap: 8px;
393 | margin-top: 16px;
394 | }
395 |
396 | .empty-state {
397 | text-align: center;
398 | padding: 60px 20px;
399 | color: #b0b0b0;
400 | background: linear-gradient(135deg, #383838 0%, #2f2f2f 100%);
401 | border: 2px dashed #4a4a4a;
402 | border-radius: 12px;
403 | margin: 20px 0;
404 | }
405 |
406 | .empty-state h3 {
407 | margin: 0 0 12px 0;
408 | font-size: 18px;
409 | color: #e0e0e0;
410 | font-weight: 600;
411 | }
412 |
413 | .empty-state p {
414 | margin: 0;
415 | font-size: 14px;
416 | line-height: 1.5;
417 | }
418 |
419 | /* 滚动条样式 */
420 | .tools-container::-webkit-scrollbar {
421 | width: 8px;
422 | }
423 |
424 | .tools-container::-webkit-scrollbar-track {
425 | background: #2a2a2a;
426 | border-radius: 4px;
427 | }
428 |
429 | .tools-container::-webkit-scrollbar-thumb {
430 | background: #5a5a5a;
431 | border-radius: 4px;
432 | }
433 |
434 | .tools-container::-webkit-scrollbar-thumb:hover {
435 | background: #6a6a6a;
436 | }
437 |
438 | /* 工具管理区域标题 */
439 | .tools-section-header {
440 | display: flex;
441 | justify-content: space-between;
442 | align-items: center;
443 | margin-bottom: 16px;
444 | padding: 16px 20px;
445 | background: linear-gradient(135deg, #323232 0%, #2a2a2a 100%);
446 | border: 1px solid #4a4a4a;
447 | border-radius: 8px;
448 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
449 | }
450 |
451 | .tools-section-title {
452 | font-weight: 600;
453 | font-size: 16px;
454 | color: #ffffff;
455 | }
456 |
457 | .tools-section-controls {
458 | display: flex;
459 | gap: 10px;
460 | }
461 |
462 | .loading {
463 | text-align: center;
464 | padding: 40px 20px;
465 | color: #b0b0b0;
466 | font-size: 14px;
467 | }
468 |
469 | .error {
470 | background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%);
471 | color: white;
472 | padding: 12px 16px;
473 | border-radius: 6px;
474 | font-size: 13px;
475 | margin-bottom: 16px;
476 | border: 1px solid #f44336;
477 | }
478 | </style>
479 | </head>
480 | <body>
481 | <div class="container">
482 | <!-- 头部 -->
483 | <div class="header">
484 | <h2 id="panelTitle">工具管理器</h2>
485 | <div class="config-controls">
486 | <button class="btn btn-primary" id="createConfigBtn">新建配置</button>
487 | <button class="btn" id="importConfigBtn">导入配置</button>
488 | <button class="btn" id="exportConfigBtn">导出配置</button>
489 | </div>
490 | </div>
491 |
492 | <!-- 配置选择器 -->
493 | <div class="config-section">
494 | <div class="config-header">
495 | <div class="config-title">当前配置</div>
496 | <div class="config-controls">
497 | <button class="btn" id="editConfigBtn">编辑</button>
498 | <button class="btn btn-danger" id="deleteConfigBtn">删除</button>
499 | </div>
500 | </div>
501 | <div class="config-selector">
502 | <select id="configSelector">
503 | <option value="">选择配置...</option>
504 | </select>
505 | <button class="btn btn-primary" id="applyConfigBtn">应用</button>
506 | </div>
507 | </div>
508 |
509 | <!-- 工具列表 -->
510 | <div class="config-section">
511 | <div class="tools-section-header">
512 | <div class="tools-section-title">工具管理</div>
513 | <div class="tools-section-controls">
514 | <button class="btn" id="selectAllBtn">全选</button>
515 | <button class="btn" id="deselectAllBtn">取消全选</button>
516 | </div>
517 | </div>
518 | <div id="toolsContainer" class="tools-container">
519 | <div class="loading">加载中...</div>
520 | </div>
521 | </div>
522 |
523 | <!-- 状态栏 -->
524 | <div class="status-bar">
525 | <div class="status-info">
526 | <span>总工具数: <span id="totalToolsCount">0</span></span>
527 | <span>已启用: <span id="enabledToolsCount">0</span></span>
528 | <span>已禁用: <span id="disabledToolsCount">0</span></span>
529 | </div>
530 | <div>
531 | <button class="btn" id="saveChangesBtn">保存更改</button>
532 | </div>
533 | </div>
534 | </div>
535 |
536 | <!-- 新建/编辑配置模态框 -->
537 | <div id="configModal" class="modal">
538 | <div class="modal-content">
539 | <div class="modal-header">
540 | <div class="modal-title" id="modalTitle">新建配置</div>
541 | <button class="modal-close" id="closeModal">×</button>
542 | </div>
543 | <form id="configForm">
544 | <div class="form-group">
545 | <label class="form-label" for="configName">配置名称 *</label>
546 | <input type="text" id="configName" class="form-input" required>
547 | </div>
548 | <div class="form-group">
549 | <label class="form-label" for="configDescription">描述</label>
550 | <textarea id="configDescription" class="form-textarea" placeholder="可选:添加配置描述"></textarea>
551 | </div>
552 | <div class="modal-actions">
553 | <button type="button" class="btn" id="cancelConfigBtn">取消</button>
554 | <button type="submit" class="btn btn-primary" id="saveConfigBtn">保存</button>
555 | </div>
556 | </form>
557 | </div>
558 | </div>
559 |
560 | <!-- 导入配置模态框 -->
561 | <div id="importModal" class="modal">
562 | <div class="modal-content">
563 | <div class="modal-header">
564 | <div class="modal-title">导入配置</div>
565 | <button class="modal-close" id="closeImportModal">×</button>
566 | </div>
567 | <div class="form-group">
568 | <label class="form-label" for="importConfigJson">配置JSON</label>
569 | <textarea id="importConfigJson" class="form-textarea" placeholder="粘贴配置JSON内容"></textarea>
570 | </div>
571 | <div class="modal-actions">
572 | <button type="button" class="btn" id="cancelImportBtn">取消</button>
573 | <button type="button" class="btn btn-primary" id="confirmImportBtn">导入</button>
574 | </div>
575 | </div>
576 | </div>
577 |
578 | <script>
579 | // 这里将包含面板的JavaScript逻辑
580 | </script>
581 | </body>
582 | </html>
```