This is page 2 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
--------------------------------------------------------------------------------
/source/tools/tool-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { v4 as uuidv4 } from 'uuid';
2 | import { ToolConfig, ToolConfiguration, ToolManagerSettings, ToolDefinition } from '../types';
3 | import * as fs from 'fs';
4 | import * as path from 'path';
5 |
6 | export class ToolManager {
7 | private settings: ToolManagerSettings;
8 | private availableTools: ToolConfig[] = [];
9 |
10 | constructor() {
11 | this.settings = this.readToolManagerSettings();
12 | this.initializeAvailableTools();
13 |
14 | // 如果没有配置,自动创建一个默认配置
15 | if (this.settings.configurations.length === 0) {
16 | console.log('[ToolManager] No configurations found, creating default configuration...');
17 | this.createConfiguration('默认配置', '自动创建的默认工具配置');
18 | }
19 | }
20 |
21 | private getToolManagerSettingsPath(): string {
22 | return path.join(Editor.Project.path, 'settings', 'tool-manager.json');
23 | }
24 |
25 | private ensureSettingsDir(): void {
26 | const settingsDir = path.dirname(this.getToolManagerSettingsPath());
27 | if (!fs.existsSync(settingsDir)) {
28 | fs.mkdirSync(settingsDir, { recursive: true });
29 | }
30 | }
31 |
32 | private readToolManagerSettings(): ToolManagerSettings {
33 | const DEFAULT_TOOL_MANAGER_SETTINGS: ToolManagerSettings = {
34 | configurations: [],
35 | currentConfigId: '',
36 | maxConfigSlots: 5
37 | };
38 |
39 | try {
40 | this.ensureSettingsDir();
41 | const settingsFile = this.getToolManagerSettingsPath();
42 | if (fs.existsSync(settingsFile)) {
43 | const content = fs.readFileSync(settingsFile, 'utf8');
44 | return { ...DEFAULT_TOOL_MANAGER_SETTINGS, ...JSON.parse(content) };
45 | }
46 | } catch (e) {
47 | console.error('Failed to read tool manager settings:', e);
48 | }
49 | return DEFAULT_TOOL_MANAGER_SETTINGS;
50 | }
51 |
52 | private saveToolManagerSettings(settings: ToolManagerSettings): void {
53 | try {
54 | this.ensureSettingsDir();
55 | const settingsFile = this.getToolManagerSettingsPath();
56 | fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
57 | } catch (e) {
58 | console.error('Failed to save tool manager settings:', e);
59 | throw e;
60 | }
61 | }
62 |
63 | private exportToolConfiguration(config: ToolConfiguration): string {
64 | return JSON.stringify(config, null, 2);
65 | }
66 |
67 | private importToolConfiguration(configJson: string): ToolConfiguration {
68 | try {
69 | const config = JSON.parse(configJson);
70 | // 验证配置格式
71 | if (!config.id || !config.name || !Array.isArray(config.tools)) {
72 | throw new Error('Invalid configuration format');
73 | }
74 | return config;
75 | } catch (e) {
76 | console.error('Failed to parse tool configuration:', e);
77 | throw new Error('Invalid JSON format or configuration structure');
78 | }
79 | }
80 |
81 | private initializeAvailableTools(): void {
82 | // 从MCP服务器获取真实的工具列表
83 | try {
84 | // 导入所有工具类
85 | const { SceneTools } = require('./scene-tools');
86 | const { NodeTools } = require('./node-tools');
87 | const { ComponentTools } = require('./component-tools');
88 | const { PrefabTools } = require('./prefab-tools');
89 | const { ProjectTools } = require('./project-tools');
90 | const { DebugTools } = require('./debug-tools');
91 | const { PreferencesTools } = require('./preferences-tools');
92 | const { ServerTools } = require('./server-tools');
93 | const { BroadcastTools } = require('./broadcast-tools');
94 | const { SceneAdvancedTools } = require('./scene-advanced-tools');
95 | const { SceneViewTools } = require('./scene-view-tools');
96 | const { ReferenceImageTools } = require('./reference-image-tools');
97 | const { AssetAdvancedTools } = require('./asset-advanced-tools');
98 | const { ValidationTools } = require('./validation-tools');
99 |
100 | // 初始化工具实例
101 | const tools = {
102 | scene: new SceneTools(),
103 | node: new NodeTools(),
104 | component: new ComponentTools(),
105 | prefab: new PrefabTools(),
106 | project: new ProjectTools(),
107 | debug: new DebugTools(),
108 | preferences: new PreferencesTools(),
109 | server: new ServerTools(),
110 | broadcast: new BroadcastTools(),
111 | sceneAdvanced: new SceneAdvancedTools(),
112 | sceneView: new SceneViewTools(),
113 | referenceImage: new ReferenceImageTools(),
114 | assetAdvanced: new AssetAdvancedTools(),
115 | validation: new ValidationTools()
116 | };
117 |
118 | // 从每个工具类获取工具列表
119 | this.availableTools = [];
120 | for (const [category, toolSet] of Object.entries(tools)) {
121 | const toolDefinitions = toolSet.getTools();
122 | toolDefinitions.forEach((tool: any) => {
123 | this.availableTools.push({
124 | category: category,
125 | name: tool.name,
126 | enabled: true, // 默认启用
127 | description: tool.description
128 | });
129 | });
130 | }
131 |
132 | console.log(`[ToolManager] Initialized ${this.availableTools.length} tools from MCP server`);
133 | } catch (error) {
134 | console.error('[ToolManager] Failed to initialize tools from MCP server:', error);
135 | // 如果获取失败,使用默认工具列表作为后备
136 | this.initializeDefaultTools();
137 | }
138 | }
139 |
140 | private initializeDefaultTools(): void {
141 | // 默认工具列表作为后备方案
142 | const toolCategories = [
143 | { category: 'scene', name: '场景工具', tools: [
144 | { name: 'getCurrentSceneInfo', description: '获取当前场景信息' },
145 | { name: 'getSceneHierarchy', description: '获取场景层级结构' },
146 | { name: 'createNewScene', description: '创建新场景' },
147 | { name: 'saveScene', description: '保存场景' },
148 | { name: 'loadScene', description: '加载场景' }
149 | ]},
150 | { category: 'node', name: '节点工具', tools: [
151 | { name: 'getAllNodes', description: '获取所有节点' },
152 | { name: 'findNodeByName', description: '根据名称查找节点' },
153 | { name: 'createNode', description: '创建节点' },
154 | { name: 'deleteNode', description: '删除节点' },
155 | { name: 'setNodeProperty', description: '设置节点属性' },
156 | { name: 'getNodeInfo', description: '获取节点信息' }
157 | ]},
158 | { category: 'component', name: '组件工具', tools: [
159 | { name: 'addComponentToNode', description: '添加组件到节点' },
160 | { name: 'removeComponentFromNode', description: '从节点移除组件' },
161 | { name: 'setComponentProperty', description: '设置组件属性' },
162 | { name: 'getComponentInfo', description: '获取组件信息' }
163 | ]},
164 | { category: 'prefab', name: '预制体工具', tools: [
165 | { name: 'createPrefabFromNode', description: '从节点创建预制体' },
166 | { name: 'instantiatePrefab', description: '实例化预制体' },
167 | { name: 'getPrefabInfo', description: '获取预制体信息' },
168 | { name: 'savePrefab', description: '保存预制体' }
169 | ]},
170 | { category: 'project', name: '项目工具', tools: [
171 | { name: 'getProjectInfo', description: '获取项目信息' },
172 | { name: 'getAssetList', description: '获取资源列表' },
173 | { name: 'createAsset', description: '创建资源' },
174 | { name: 'deleteAsset', description: '删除资源' }
175 | ]},
176 | { category: 'debug', name: '调试工具', tools: [
177 | { name: 'getConsoleLogs', description: '获取控制台日志' },
178 | { name: 'getPerformanceStats', description: '获取性能统计' },
179 | { name: 'validateScene', description: '验证场景' },
180 | { name: 'getErrorLogs', description: '获取错误日志' }
181 | ]},
182 | { category: 'preferences', name: '偏好设置工具', tools: [
183 | { name: 'getPreferences', description: '获取偏好设置' },
184 | { name: 'setPreferences', description: '设置偏好设置' },
185 | { name: 'resetPreferences', description: '重置偏好设置' }
186 | ]},
187 | { category: 'server', name: '服务器工具', tools: [
188 | { name: 'getServerStatus', description: '获取服务器状态' },
189 | { name: 'getConnectedClients', description: '获取连接的客户端' },
190 | { name: 'getServerLogs', description: '获取服务器日志' }
191 | ]},
192 | { category: 'broadcast', name: '广播工具', tools: [
193 | { name: 'broadcastMessage', description: '广播消息' },
194 | { name: 'getBroadcastHistory', description: '获取广播历史' }
195 | ]},
196 | { category: 'sceneAdvanced', name: '高级场景工具', tools: [
197 | { name: 'optimizeScene', description: '优化场景' },
198 | { name: 'analyzeScene', description: '分析场景' },
199 | { name: 'batchOperation', description: '批量操作' }
200 | ]},
201 | { category: 'sceneView', name: '场景视图工具', tools: [
202 | { name: 'getViewportInfo', description: '获取视口信息' },
203 | { name: 'setViewportCamera', description: '设置视口相机' },
204 | { name: 'focusOnNode', description: '聚焦到节点' }
205 | ]},
206 | { category: 'referenceImage', name: '参考图片工具', tools: [
207 | { name: 'addReferenceImage', description: '添加参考图片' },
208 | { name: 'removeReferenceImage', description: '移除参考图片' },
209 | { name: 'getReferenceImages', description: '获取参考图片列表' }
210 | ]},
211 | { category: 'assetAdvanced', name: '高级资源工具', tools: [
212 | { name: 'importAsset', description: '导入资源' },
213 | { name: 'exportAsset', description: '导出资源' },
214 | { name: 'processAsset', description: '处理资源' }
215 | ]},
216 | { category: 'validation', name: '验证工具', tools: [
217 | { name: 'validateProject', description: '验证项目' },
218 | { name: 'validateAssets', description: '验证资源' },
219 | { name: 'generateReport', description: '生成报告' }
220 | ]}
221 | ];
222 |
223 | this.availableTools = [];
224 | toolCategories.forEach(category => {
225 | category.tools.forEach(tool => {
226 | this.availableTools.push({
227 | category: category.category,
228 | name: tool.name,
229 | enabled: true, // 默认启用
230 | description: tool.description
231 | });
232 | });
233 | });
234 |
235 | console.log(`[ToolManager] Initialized ${this.availableTools.length} default tools`);
236 | }
237 |
238 | public getAvailableTools(): ToolConfig[] {
239 | return [...this.availableTools];
240 | }
241 |
242 | public getConfigurations(): ToolConfiguration[] {
243 | return [...this.settings.configurations];
244 | }
245 |
246 | public getCurrentConfiguration(): ToolConfiguration | null {
247 | if (!this.settings.currentConfigId) {
248 | return null;
249 | }
250 | return this.settings.configurations.find(config => config.id === this.settings.currentConfigId) || null;
251 | }
252 |
253 | public createConfiguration(name: string, description?: string): ToolConfiguration {
254 | if (this.settings.configurations.length >= this.settings.maxConfigSlots) {
255 | throw new Error(`已达到最大配置槽位数量 (${this.settings.maxConfigSlots})`);
256 | }
257 |
258 | const config: ToolConfiguration = {
259 | id: uuidv4(),
260 | name,
261 | description,
262 | tools: this.availableTools.map(tool => ({ ...tool })),
263 | createdAt: new Date().toISOString(),
264 | updatedAt: new Date().toISOString()
265 | };
266 |
267 | this.settings.configurations.push(config);
268 | this.settings.currentConfigId = config.id;
269 | this.saveSettings();
270 |
271 | return config;
272 | }
273 |
274 | public updateConfiguration(configId: string, updates: Partial<ToolConfiguration>): ToolConfiguration {
275 | const configIndex = this.settings.configurations.findIndex(config => config.id === configId);
276 | if (configIndex === -1) {
277 | throw new Error('配置不存在');
278 | }
279 |
280 | const config = this.settings.configurations[configIndex];
281 | const updatedConfig: ToolConfiguration = {
282 | ...config,
283 | ...updates,
284 | updatedAt: new Date().toISOString()
285 | };
286 |
287 | this.settings.configurations[configIndex] = updatedConfig;
288 | this.saveSettings();
289 |
290 | return updatedConfig;
291 | }
292 |
293 | public deleteConfiguration(configId: string): void {
294 | const configIndex = this.settings.configurations.findIndex(config => config.id === configId);
295 | if (configIndex === -1) {
296 | throw new Error('配置不存在');
297 | }
298 |
299 | this.settings.configurations.splice(configIndex, 1);
300 |
301 | // 如果删除的是当前配置,清空当前配置ID
302 | if (this.settings.currentConfigId === configId) {
303 | this.settings.currentConfigId = this.settings.configurations.length > 0
304 | ? this.settings.configurations[0].id
305 | : '';
306 | }
307 |
308 | this.saveSettings();
309 | }
310 |
311 | public setCurrentConfiguration(configId: string): void {
312 | const config = this.settings.configurations.find(config => config.id === configId);
313 | if (!config) {
314 | throw new Error('配置不存在');
315 | }
316 |
317 | this.settings.currentConfigId = configId;
318 | this.saveSettings();
319 | }
320 |
321 | public updateToolStatus(configId: string, category: string, toolName: string, enabled: boolean): void {
322 | console.log(`Backend: Updating tool status - configId: ${configId}, category: ${category}, toolName: ${toolName}, enabled: ${enabled}`);
323 |
324 | const config = this.settings.configurations.find(config => config.id === configId);
325 | if (!config) {
326 | console.error(`Backend: Config not found with ID: ${configId}`);
327 | throw new Error('配置不存在');
328 | }
329 |
330 | console.log(`Backend: Found config: ${config.name}`);
331 |
332 | const tool = config.tools.find(t => t.category === category && t.name === toolName);
333 | if (!tool) {
334 | console.error(`Backend: Tool not found - category: ${category}, name: ${toolName}`);
335 | throw new Error('工具不存在');
336 | }
337 |
338 | console.log(`Backend: Found tool: ${tool.name}, current enabled: ${tool.enabled}, new enabled: ${enabled}`);
339 |
340 | tool.enabled = enabled;
341 | config.updatedAt = new Date().toISOString();
342 |
343 | console.log(`Backend: Tool updated, saving settings...`);
344 | this.saveSettings();
345 | console.log(`Backend: Settings saved successfully`);
346 | }
347 |
348 | public updateToolStatusBatch(configId: string, updates: { category: string; name: string; enabled: boolean }[]): void {
349 | console.log(`Backend: updateToolStatusBatch called with configId: ${configId}`);
350 | console.log(`Backend: Current configurations count: ${this.settings.configurations.length}`);
351 | console.log(`Backend: Current config IDs:`, this.settings.configurations.map(c => c.id));
352 |
353 | const config = this.settings.configurations.find(config => config.id === configId);
354 | if (!config) {
355 | console.error(`Backend: Config not found with ID: ${configId}`);
356 | console.error(`Backend: Available config IDs:`, this.settings.configurations.map(c => c.id));
357 | throw new Error('配置不存在');
358 | }
359 |
360 | console.log(`Backend: Found config: ${config.name}, updating ${updates.length} tools`);
361 |
362 | updates.forEach(update => {
363 | const tool = config.tools.find(t => t.category === update.category && t.name === update.name);
364 | if (tool) {
365 | tool.enabled = update.enabled;
366 | }
367 | });
368 |
369 | config.updatedAt = new Date().toISOString();
370 | this.saveSettings();
371 | console.log(`Backend: Batch update completed successfully`);
372 | }
373 |
374 | public exportConfiguration(configId: string): string {
375 | const config = this.settings.configurations.find(config => config.id === configId);
376 | if (!config) {
377 | throw new Error('配置不存在');
378 | }
379 |
380 | return this.exportToolConfiguration(config);
381 | }
382 |
383 | public importConfiguration(configJson: string): ToolConfiguration {
384 | const config = this.importToolConfiguration(configJson);
385 |
386 | // 生成新的ID和时间戳
387 | config.id = uuidv4();
388 | config.createdAt = new Date().toISOString();
389 | config.updatedAt = new Date().toISOString();
390 |
391 | if (this.settings.configurations.length >= this.settings.maxConfigSlots) {
392 | throw new Error(`已达到最大配置槽位数量 (${this.settings.maxConfigSlots})`);
393 | }
394 |
395 | this.settings.configurations.push(config);
396 | this.saveSettings();
397 |
398 | return config;
399 | }
400 |
401 | public getEnabledTools(): ToolConfig[] {
402 | const currentConfig = this.getCurrentConfiguration();
403 | if (!currentConfig) {
404 | return this.availableTools.filter(tool => tool.enabled);
405 | }
406 | return currentConfig.tools.filter(tool => tool.enabled);
407 | }
408 |
409 | public getToolManagerState() {
410 | const currentConfig = this.getCurrentConfiguration();
411 | return {
412 | success: true,
413 | availableTools: currentConfig ? currentConfig.tools : this.getAvailableTools(),
414 | selectedConfigId: this.settings.currentConfigId,
415 | configurations: this.getConfigurations(),
416 | maxConfigSlots: this.settings.maxConfigSlots
417 | };
418 | }
419 |
420 | private saveSettings(): void {
421 | console.log(`Backend: Saving settings, current configs count: ${this.settings.configurations.length}`);
422 | this.saveToolManagerSettings(this.settings);
423 | console.log(`Backend: Settings saved to file`);
424 | }
425 | }
```
--------------------------------------------------------------------------------
/source/mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as http from 'http';
2 | import * as url from 'url';
3 | import { v4 as uuidv4 } from 'uuid';
4 | import { MCPServerSettings, ServerStatus, MCPClient, ToolDefinition } from './types';
5 | import { SceneTools } from './tools/scene-tools';
6 | import { NodeTools } from './tools/node-tools';
7 | import { ComponentTools } from './tools/component-tools';
8 | import { PrefabTools } from './tools/prefab-tools';
9 | import { ProjectTools } from './tools/project-tools';
10 | import { DebugTools } from './tools/debug-tools';
11 | import { PreferencesTools } from './tools/preferences-tools';
12 | import { ServerTools } from './tools/server-tools';
13 | import { BroadcastTools } from './tools/broadcast-tools';
14 | import { SceneAdvancedTools } from './tools/scene-advanced-tools';
15 | import { SceneViewTools } from './tools/scene-view-tools';
16 | import { ReferenceImageTools } from './tools/reference-image-tools';
17 | import { AssetAdvancedTools } from './tools/asset-advanced-tools';
18 | import { ValidationTools } from './tools/validation-tools';
19 |
20 | export class MCPServer {
21 | private settings: MCPServerSettings;
22 | private httpServer: http.Server | null = null;
23 | private clients: Map<string, MCPClient> = new Map();
24 | private tools: Record<string, any> = {};
25 | private toolsList: ToolDefinition[] = [];
26 | private enabledTools: any[] = []; // 存储启用的工具列表
27 |
28 | constructor(settings: MCPServerSettings) {
29 | this.settings = settings;
30 | this.initializeTools();
31 | }
32 |
33 | private initializeTools(): void {
34 | try {
35 | console.log('[MCPServer] Initializing tools...');
36 | this.tools.scene = new SceneTools();
37 | this.tools.node = new NodeTools();
38 | this.tools.component = new ComponentTools();
39 | this.tools.prefab = new PrefabTools();
40 | this.tools.project = new ProjectTools();
41 | this.tools.debug = new DebugTools();
42 | this.tools.preferences = new PreferencesTools();
43 | this.tools.server = new ServerTools();
44 | this.tools.broadcast = new BroadcastTools();
45 | this.tools.sceneAdvanced = new SceneAdvancedTools();
46 | this.tools.sceneView = new SceneViewTools();
47 | this.tools.referenceImage = new ReferenceImageTools();
48 | this.tools.assetAdvanced = new AssetAdvancedTools();
49 | this.tools.validation = new ValidationTools();
50 | console.log('[MCPServer] Tools initialized successfully');
51 | } catch (error) {
52 | console.error('[MCPServer] Error initializing tools:', error);
53 | throw error;
54 | }
55 | }
56 |
57 | public async start(): Promise<void> {
58 | if (this.httpServer) {
59 | console.log('[MCPServer] Server is already running');
60 | return;
61 | }
62 |
63 | try {
64 | console.log(`[MCPServer] Starting HTTP server on port ${this.settings.port}...`);
65 | this.httpServer = http.createServer(this.handleHttpRequest.bind(this));
66 |
67 | await new Promise<void>((resolve, reject) => {
68 | this.httpServer!.listen(this.settings.port, '127.0.0.1', () => {
69 | console.log(`[MCPServer] ✅ HTTP server started successfully on http://127.0.0.1:${this.settings.port}`);
70 | console.log(`[MCPServer] Health check: http://127.0.0.1:${this.settings.port}/health`);
71 | console.log(`[MCPServer] MCP endpoint: http://127.0.0.1:${this.settings.port}/mcp`);
72 | resolve();
73 | });
74 | this.httpServer!.on('error', (err: any) => {
75 | console.error('[MCPServer] ❌ Failed to start server:', err);
76 | if (err.code === 'EADDRINUSE') {
77 | console.error(`[MCPServer] Port ${this.settings.port} is already in use. Please change the port in settings.`);
78 | }
79 | reject(err);
80 | });
81 | });
82 |
83 | this.setupTools();
84 | console.log('[MCPServer] 🚀 MCP Server is ready for connections');
85 | } catch (error) {
86 | console.error('[MCPServer] ❌ Failed to start server:', error);
87 | throw error;
88 | }
89 | }
90 |
91 | private setupTools(): void {
92 | this.toolsList = [];
93 |
94 | // 如果没有启用工具配置,返回所有工具
95 | if (!this.enabledTools || this.enabledTools.length === 0) {
96 | for (const [category, toolSet] of Object.entries(this.tools)) {
97 | const tools = toolSet.getTools();
98 | for (const tool of tools) {
99 | this.toolsList.push({
100 | name: `${category}_${tool.name}`,
101 | description: tool.description,
102 | inputSchema: tool.inputSchema
103 | });
104 | }
105 | }
106 | } else {
107 | // 根据启用的工具配置过滤
108 | const enabledToolNames = new Set(this.enabledTools.map(tool => `${tool.category}_${tool.name}`));
109 |
110 | for (const [category, toolSet] of Object.entries(this.tools)) {
111 | const tools = toolSet.getTools();
112 | for (const tool of tools) {
113 | const toolName = `${category}_${tool.name}`;
114 | if (enabledToolNames.has(toolName)) {
115 | this.toolsList.push({
116 | name: toolName,
117 | description: tool.description,
118 | inputSchema: tool.inputSchema
119 | });
120 | }
121 | }
122 | }
123 | }
124 |
125 | console.log(`[MCPServer] Setup tools: ${this.toolsList.length} tools available`);
126 | }
127 |
128 | public getFilteredTools(enabledTools: any[]): ToolDefinition[] {
129 | if (!enabledTools || enabledTools.length === 0) {
130 | return this.toolsList; // 如果没有过滤配置,返回所有工具
131 | }
132 |
133 | const enabledToolNames = new Set(enabledTools.map(tool => `${tool.category}_${tool.name}`));
134 | return this.toolsList.filter(tool => enabledToolNames.has(tool.name));
135 | }
136 |
137 | public async executeToolCall(toolName: string, args: any): Promise<any> {
138 | const parts = toolName.split('_');
139 | const category = parts[0];
140 | const toolMethodName = parts.slice(1).join('_');
141 |
142 | if (this.tools[category]) {
143 | return await this.tools[category].execute(toolMethodName, args);
144 | }
145 |
146 | throw new Error(`Tool ${toolName} not found`);
147 | }
148 |
149 | public getClients(): MCPClient[] {
150 | return Array.from(this.clients.values());
151 | }
152 | public getAvailableTools(): ToolDefinition[] {
153 | return this.toolsList;
154 | }
155 |
156 | public updateEnabledTools(enabledTools: any[]): void {
157 | console.log(`[MCPServer] Updating enabled tools: ${enabledTools.length} tools`);
158 | this.enabledTools = enabledTools;
159 | this.setupTools(); // 重新设置工具列表
160 | }
161 |
162 | public getSettings(): MCPServerSettings {
163 | return this.settings;
164 | }
165 |
166 | private async handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
167 | const parsedUrl = url.parse(req.url || '', true);
168 | const pathname = parsedUrl.pathname;
169 |
170 | // Set CORS headers
171 | res.setHeader('Access-Control-Allow-Origin', '*');
172 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
173 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
174 | res.setHeader('Content-Type', 'application/json');
175 |
176 | if (req.method === 'OPTIONS') {
177 | res.writeHead(200);
178 | res.end();
179 | return;
180 | }
181 |
182 | try {
183 | if (pathname === '/mcp' && req.method === 'POST') {
184 | await this.handleMCPRequest(req, res);
185 | } else if (pathname === '/health' && req.method === 'GET') {
186 | res.writeHead(200);
187 | res.end(JSON.stringify({ status: 'ok', tools: this.toolsList.length }));
188 | } else if (pathname?.startsWith('/api/') && req.method === 'POST') {
189 | await this.handleSimpleAPIRequest(req, res, pathname);
190 | } else if (pathname === '/api/tools' && req.method === 'GET') {
191 | res.writeHead(200);
192 | res.end(JSON.stringify({ tools: this.getSimplifiedToolsList() }));
193 | } else {
194 | res.writeHead(404);
195 | res.end(JSON.stringify({ error: 'Not found' }));
196 | }
197 | } catch (error) {
198 | console.error('HTTP request error:', error);
199 | res.writeHead(500);
200 | res.end(JSON.stringify({ error: 'Internal server error' }));
201 | }
202 | }
203 |
204 | private async handleMCPRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
205 | let body = '';
206 |
207 | req.on('data', (chunk) => {
208 | body += chunk.toString();
209 | });
210 |
211 | req.on('end', async () => {
212 | try {
213 | // Enhanced JSON parsing with better error handling
214 | let message;
215 | try {
216 | message = JSON.parse(body);
217 | } catch (parseError: any) {
218 | // Try to fix common JSON issues
219 | const fixedBody = this.fixCommonJsonIssues(body);
220 | try {
221 | message = JSON.parse(fixedBody);
222 | console.log('[MCPServer] Fixed JSON parsing issue');
223 | } catch (secondError) {
224 | throw new Error(`JSON parsing failed: ${parseError.message}. Original body: ${body.substring(0, 500)}...`);
225 | }
226 | }
227 |
228 | const response = await this.handleMessage(message);
229 | res.writeHead(200);
230 | res.end(JSON.stringify(response));
231 | } catch (error: any) {
232 | console.error('Error handling MCP request:', error);
233 | res.writeHead(400);
234 | res.end(JSON.stringify({
235 | jsonrpc: '2.0',
236 | id: null,
237 | error: {
238 | code: -32700,
239 | message: `Parse error: ${error.message}`
240 | }
241 | }));
242 | }
243 | });
244 | }
245 |
246 | private async handleMessage(message: any): Promise<any> {
247 | const { id, method, params } = message;
248 |
249 | try {
250 | let result: any;
251 |
252 | switch (method) {
253 | case 'tools/list':
254 | result = { tools: this.getAvailableTools() };
255 | break;
256 | case 'tools/call':
257 | const { name, arguments: args } = params;
258 | const toolResult = await this.executeToolCall(name, args);
259 | result = { content: [{ type: 'text', text: JSON.stringify(toolResult) }] };
260 | break;
261 | case 'initialize':
262 | // MCP initialization
263 | result = {
264 | protocolVersion: '2024-11-05',
265 | capabilities: {
266 | tools: {}
267 | },
268 | serverInfo: {
269 | name: 'cocos-mcp-server',
270 | version: '1.0.0'
271 | }
272 | };
273 | break;
274 | default:
275 | throw new Error(`Unknown method: ${method}`);
276 | }
277 |
278 | return {
279 | jsonrpc: '2.0',
280 | id,
281 | result
282 | };
283 | } catch (error: any) {
284 | return {
285 | jsonrpc: '2.0',
286 | id,
287 | error: {
288 | code: -32603,
289 | message: error.message
290 | }
291 | };
292 | }
293 | }
294 |
295 | private fixCommonJsonIssues(jsonStr: string): string {
296 | let fixed = jsonStr;
297 |
298 | // Fix common escape character issues
299 | fixed = fixed
300 | // Fix unescaped quotes in strings
301 | .replace(/([^\\])"([^"]*[^\\])"([^,}\]:])/g, '$1\\"$2\\"$3')
302 | // Fix unescaped backslashes
303 | .replace(/([^\\])\\([^"\\\/bfnrt])/g, '$1\\\\$2')
304 | // Fix trailing commas
305 | .replace(/,(\s*[}\]])/g, '$1')
306 | // Fix single quotes (should be double quotes)
307 | .replace(/'/g, '"')
308 | // Fix common control characters
309 | .replace(/\n/g, '\\n')
310 | .replace(/\r/g, '\\r')
311 | .replace(/\t/g, '\\t');
312 |
313 | return fixed;
314 | }
315 |
316 | public stop(): void {
317 | if (this.httpServer) {
318 | this.httpServer.close();
319 | this.httpServer = null;
320 | console.log('[MCPServer] HTTP server stopped');
321 | }
322 |
323 | this.clients.clear();
324 | }
325 |
326 | public getStatus(): ServerStatus {
327 | return {
328 | running: !!this.httpServer,
329 | port: this.settings.port,
330 | clients: 0 // HTTP is stateless, no persistent clients
331 | };
332 | }
333 |
334 | private async handleSimpleAPIRequest(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
335 | let body = '';
336 |
337 | req.on('data', (chunk) => {
338 | body += chunk.toString();
339 | });
340 |
341 | req.on('end', async () => {
342 | try {
343 | // Extract tool name from path like /api/node/set_position
344 | const pathParts = pathname.split('/').filter(p => p);
345 | if (pathParts.length < 3) {
346 | res.writeHead(400);
347 | res.end(JSON.stringify({ error: 'Invalid API path. Use /api/{category}/{tool_name}' }));
348 | return;
349 | }
350 |
351 | const category = pathParts[1];
352 | const toolName = pathParts[2];
353 | const fullToolName = `${category}_${toolName}`;
354 |
355 | // Parse parameters with enhanced error handling
356 | let params;
357 | try {
358 | params = body ? JSON.parse(body) : {};
359 | } catch (parseError: any) {
360 | // Try to fix JSON issues
361 | const fixedBody = this.fixCommonJsonIssues(body);
362 | try {
363 | params = JSON.parse(fixedBody);
364 | console.log('[MCPServer] Fixed API JSON parsing issue');
365 | } catch (secondError: any) {
366 | res.writeHead(400);
367 | res.end(JSON.stringify({
368 | error: 'Invalid JSON in request body',
369 | details: parseError.message,
370 | receivedBody: body.substring(0, 200)
371 | }));
372 | return;
373 | }
374 | }
375 |
376 | // Execute tool
377 | const result = await this.executeToolCall(fullToolName, params);
378 |
379 | res.writeHead(200);
380 | res.end(JSON.stringify({
381 | success: true,
382 | tool: fullToolName,
383 | result: result
384 | }));
385 |
386 | } catch (error: any) {
387 | console.error('Simple API error:', error);
388 | res.writeHead(500);
389 | res.end(JSON.stringify({
390 | success: false,
391 | error: error.message,
392 | tool: pathname
393 | }));
394 | }
395 | });
396 | }
397 |
398 | private getSimplifiedToolsList(): any[] {
399 | return this.toolsList.map(tool => {
400 | const parts = tool.name.split('_');
401 | const category = parts[0];
402 | const toolName = parts.slice(1).join('_');
403 |
404 | return {
405 | name: tool.name,
406 | category: category,
407 | toolName: toolName,
408 | description: tool.description,
409 | apiPath: `/api/${category}/${toolName}`,
410 | curlExample: this.generateCurlExample(category, toolName, tool.inputSchema)
411 | };
412 | });
413 | }
414 |
415 | private generateCurlExample(category: string, toolName: string, schema: any): string {
416 | // Generate sample parameters based on schema
417 | const sampleParams = this.generateSampleParams(schema);
418 | const jsonString = JSON.stringify(sampleParams, null, 2);
419 |
420 | return `curl -X POST http://127.0.0.1:8585/api/${category}/${toolName} \\
421 | -H "Content-Type: application/json" \\
422 | -d '${jsonString}'`;
423 | }
424 |
425 | private generateSampleParams(schema: any): any {
426 | if (!schema || !schema.properties) return {};
427 |
428 | const sample: any = {};
429 | for (const [key, prop] of Object.entries(schema.properties as any)) {
430 | const propSchema = prop as any;
431 | switch (propSchema.type) {
432 | case 'string':
433 | sample[key] = propSchema.default || 'example_string';
434 | break;
435 | case 'number':
436 | sample[key] = propSchema.default || 42;
437 | break;
438 | case 'boolean':
439 | sample[key] = propSchema.default || true;
440 | break;
441 | case 'object':
442 | sample[key] = propSchema.default || { x: 0, y: 0, z: 0 };
443 | break;
444 | default:
445 | sample[key] = 'example_value';
446 | }
447 | }
448 | return sample;
449 | }
450 |
451 | public updateSettings(settings: MCPServerSettings) {
452 | this.settings = settings;
453 | if (this.httpServer) {
454 | this.stop();
455 | this.start();
456 | }
457 | }
458 | }
459 |
460 | // HTTP transport doesn't need persistent connections
461 | // MCP over HTTP uses request-response pattern
```
--------------------------------------------------------------------------------
/source/panels/default/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /* eslint-disable vue/one-component-per-file */
2 |
3 | import { readFileSync } from 'fs-extra';
4 | import { join } from 'path';
5 | import { createApp, App, defineComponent, ref, computed, onMounted, watch, nextTick } from 'vue';
6 |
7 | const panelDataMap = new WeakMap<any, App>();
8 |
9 | // 定义工具配置接口
10 | interface ToolConfig {
11 | category: string;
12 | name: string;
13 | enabled: boolean;
14 | description: string;
15 | }
16 |
17 | // 定义配置接口
18 | interface Configuration {
19 | id: string;
20 | name: string;
21 | description: string;
22 | tools: ToolConfig[];
23 | createdAt: string;
24 | updatedAt: string;
25 | }
26 |
27 | // 定义服务器设置接口
28 | interface ServerSettings {
29 | port: number;
30 | autoStart: boolean;
31 | debugLog: boolean;
32 | maxConnections: number;
33 | }
34 |
35 | module.exports = Editor.Panel.define({
36 | listeners: {
37 | show() {
38 | console.log('[MCP Panel] Panel shown');
39 | },
40 | hide() {
41 | console.log('[MCP Panel] Panel hidden');
42 | },
43 | },
44 | template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'),
45 | style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
46 | $: {
47 | app: '#app',
48 | panelTitle: '#panelTitle',
49 | },
50 | ready() {
51 | if (this.$.app) {
52 | const app = createApp({});
53 | app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
54 |
55 | // 创建主应用组件
56 | app.component('McpServerApp', defineComponent({
57 | setup() {
58 | // 响应式数据
59 | const activeTab = ref('server');
60 | const serverRunning = ref(false);
61 | const serverStatus = ref('已停止');
62 | const connectedClients = ref(0);
63 | const httpUrl = ref('');
64 | const isProcessing = ref(false);
65 |
66 | const settings = ref<ServerSettings>({
67 | port: 3000,
68 | autoStart: false,
69 | debugLog: false,
70 | maxConnections: 10
71 | });
72 |
73 | const availableTools = ref<ToolConfig[]>([]);
74 | const toolCategories = ref<string[]>([]);
75 |
76 |
77 |
78 | // 计算属性
79 | const statusClass = computed(() => ({
80 | 'status-running': serverRunning.value,
81 | 'status-stopped': !serverRunning.value
82 | }));
83 |
84 | const totalTools = computed(() => availableTools.value.length);
85 | const enabledTools = computed(() => availableTools.value.filter(t => t.enabled).length);
86 | const disabledTools = computed(() => totalTools.value - enabledTools.value);
87 |
88 |
89 |
90 | const settingsChanged = ref(false);
91 |
92 | // 方法
93 | const switchTab = (tabName: string) => {
94 | activeTab.value = tabName;
95 | if (tabName === 'tools') {
96 | loadToolManagerState();
97 | }
98 | };
99 |
100 | const toggleServer = async () => {
101 | try {
102 | if (serverRunning.value) {
103 | await Editor.Message.request('cocos-mcp-server', 'stop-server');
104 | } else {
105 | // 启动服务器时使用当前面板设置
106 | const currentSettings = {
107 | port: settings.value.port,
108 | autoStart: settings.value.autoStart,
109 | enableDebugLog: settings.value.debugLog,
110 | maxConnections: settings.value.maxConnections
111 | };
112 | await Editor.Message.request('cocos-mcp-server', 'update-settings', currentSettings);
113 | await Editor.Message.request('cocos-mcp-server', 'start-server');
114 | }
115 | console.log('[Vue App] Server toggled');
116 | } catch (error) {
117 | console.error('[Vue App] Failed to toggle server:', error);
118 | }
119 | };
120 |
121 | const saveSettings = async () => {
122 | try {
123 | // 创建一个简单的对象,避免克隆错误
124 | const settingsData = {
125 | port: settings.value.port,
126 | autoStart: settings.value.autoStart,
127 | debugLog: settings.value.debugLog,
128 | maxConnections: settings.value.maxConnections
129 | };
130 |
131 | const result = await Editor.Message.request('cocos-mcp-server', 'update-settings', settingsData);
132 | console.log('[Vue App] Save settings result:', result);
133 | settingsChanged.value = false;
134 | } catch (error) {
135 | console.error('[Vue App] Failed to save settings:', error);
136 | }
137 | };
138 |
139 | const copyUrl = async () => {
140 | try {
141 | await navigator.clipboard.writeText(httpUrl.value);
142 | console.log('[Vue App] URL copied to clipboard');
143 | } catch (error) {
144 | console.error('[Vue App] Failed to copy URL:', error);
145 | }
146 | };
147 |
148 | const loadToolManagerState = async () => {
149 | try {
150 | const result = await Editor.Message.request('cocos-mcp-server', 'getToolManagerState');
151 | if (result && result.success) {
152 | // 总是加载后端状态,确保数据是最新的
153 | availableTools.value = result.availableTools || [];
154 | console.log('[Vue App] Loaded tools:', availableTools.value.length);
155 |
156 | // 更新工具分类
157 | const categories = new Set(availableTools.value.map(tool => tool.category));
158 | toolCategories.value = Array.from(categories);
159 | }
160 | } catch (error) {
161 | console.error('[Vue App] Failed to load tool manager state:', error);
162 | }
163 | };
164 |
165 | const updateToolStatus = async (category: string, name: string, enabled: boolean) => {
166 | try {
167 | console.log('[Vue App] updateToolStatus called:', category, name, enabled);
168 |
169 | // 先更新本地状态
170 | const toolIndex = availableTools.value.findIndex(t => t.category === category && t.name === name);
171 | if (toolIndex !== -1) {
172 | availableTools.value[toolIndex].enabled = enabled;
173 | // 强制触发响应式更新
174 | availableTools.value = [...availableTools.value];
175 | console.log('[Vue App] Local state updated, tool enabled:', availableTools.value[toolIndex].enabled);
176 | }
177 |
178 | // 调用后端更新
179 | const result = await Editor.Message.request('cocos-mcp-server', 'updateToolStatus', category, name, enabled);
180 | if (!result || !result.success) {
181 | // 如果后端更新失败,回滚本地状态
182 | if (toolIndex !== -1) {
183 | availableTools.value[toolIndex].enabled = !enabled;
184 | availableTools.value = [...availableTools.value];
185 | }
186 | console.error('[Vue App] Backend update failed, rolled back local state');
187 | } else {
188 | console.log('[Vue App] Backend update successful');
189 | }
190 | } catch (error) {
191 | // 如果发生错误,回滚本地状态
192 | const toolIndex = availableTools.value.findIndex(t => t.category === category && t.name === name);
193 | if (toolIndex !== -1) {
194 | availableTools.value[toolIndex].enabled = !enabled;
195 | availableTools.value = [...availableTools.value];
196 | }
197 | console.error('[Vue App] Failed to update tool status:', error);
198 | }
199 | };
200 |
201 | const selectAllTools = async () => {
202 | try {
203 | // 直接更新本地状态,然后保存
204 | availableTools.value.forEach(tool => tool.enabled = true);
205 | await saveChanges();
206 | } catch (error) {
207 | console.error('[Vue App] Failed to select all tools:', error);
208 | }
209 | };
210 |
211 | const deselectAllTools = async () => {
212 | try {
213 | // 直接更新本地状态,然后保存
214 | availableTools.value.forEach(tool => tool.enabled = false);
215 | await saveChanges();
216 | } catch (error) {
217 | console.error('[Vue App] Failed to deselect all tools:', error);
218 | }
219 | };
220 |
221 | const saveChanges = async () => {
222 | try {
223 | // 创建普通对象,避免Vue3响应式对象克隆错误
224 | const updates = availableTools.value.map(tool => ({
225 | category: String(tool.category),
226 | name: String(tool.name),
227 | enabled: Boolean(tool.enabled)
228 | }));
229 |
230 | console.log('[Vue App] Sending updates:', updates.length, 'tools');
231 |
232 | const result = await Editor.Message.request('cocos-mcp-server', 'updateToolStatusBatch', updates);
233 |
234 | if (result && result.success) {
235 | console.log('[Vue App] Tool changes saved successfully');
236 | }
237 | } catch (error) {
238 | console.error('[Vue App] Failed to save tool changes:', error);
239 | }
240 | };
241 |
242 |
243 |
244 | const toggleCategoryTools = async (category: string, enabled: boolean) => {
245 | try {
246 | // 直接更新本地状态,然后保存
247 | availableTools.value.forEach(tool => {
248 | if (tool.category === category) {
249 | tool.enabled = enabled;
250 | }
251 | });
252 | await saveChanges();
253 | } catch (error) {
254 | console.error('[Vue App] Failed to toggle category tools:', error);
255 | }
256 | };
257 |
258 | const getToolsByCategory = (category: string) => {
259 | return availableTools.value.filter(tool => tool.category === category);
260 | };
261 |
262 | const getCategoryDisplayName = (category: string): string => {
263 | const categoryNames: { [key: string]: string } = {
264 | 'scene': '场景工具',
265 | 'node': '节点工具',
266 | 'component': '组件工具',
267 | 'prefab': '预制体工具',
268 | 'project': '项目工具',
269 | 'debug': '调试工具',
270 | 'preferences': '偏好设置工具',
271 | 'server': '服务器工具',
272 | 'broadcast': '广播工具',
273 | 'sceneAdvanced': '高级场景工具',
274 | 'sceneView': '场景视图工具',
275 | 'referenceImage': '参考图片工具',
276 | 'assetAdvanced': '高级资源工具',
277 | 'validation': '验证工具'
278 | };
279 | return categoryNames[category] || category;
280 | };
281 |
282 |
283 |
284 |
285 |
286 | // 监听设置变化
287 | watch(settings, () => {
288 | settingsChanged.value = true;
289 | }, { deep: true });
290 |
291 |
292 |
293 | // 组件挂载时加载数据
294 | onMounted(async () => {
295 | // 加载工具管理器状态
296 | await loadToolManagerState();
297 |
298 | // 从服务器状态获取设置信息
299 | try {
300 | const serverStatus = await Editor.Message.request('cocos-mcp-server', 'get-server-status');
301 | if (serverStatus && serverStatus.settings) {
302 | settings.value = {
303 | port: serverStatus.settings.port || 3000,
304 | autoStart: serverStatus.settings.autoStart || false,
305 | debugLog: serverStatus.settings.enableDebugLog || false,
306 | maxConnections: serverStatus.settings.maxConnections || 10
307 | };
308 | console.log('[Vue App] Server settings loaded from status:', serverStatus.settings);
309 | } else if (serverStatus && serverStatus.port) {
310 | // 兼容旧版本,只获取端口信息
311 | settings.value.port = serverStatus.port;
312 | console.log('[Vue App] Port loaded from server status:', serverStatus.port);
313 | }
314 | } catch (error) {
315 | console.error('[Vue App] Failed to get server status:', error);
316 | console.log('[Vue App] Using default server settings');
317 | }
318 |
319 | // 定期更新服务器状态
320 | setInterval(async () => {
321 | try {
322 | const result = await Editor.Message.request('cocos-mcp-server', 'get-server-status');
323 | if (result) {
324 | serverRunning.value = result.running;
325 | serverStatus.value = result.running ? '运行中' : '已停止';
326 | connectedClients.value = result.clients || 0;
327 | httpUrl.value = result.running ? `http://localhost:${result.port}` : '';
328 | isProcessing.value = false;
329 | }
330 | } catch (error) {
331 | console.error('[Vue App] Failed to get server status:', error);
332 | }
333 | }, 2000);
334 | });
335 |
336 | return {
337 | // 数据
338 | activeTab,
339 | serverRunning,
340 | serverStatus,
341 | connectedClients,
342 | httpUrl,
343 | isProcessing,
344 | settings,
345 | availableTools,
346 | toolCategories,
347 | settingsChanged,
348 |
349 | // 计算属性
350 | statusClass,
351 | totalTools,
352 | enabledTools,
353 | disabledTools,
354 |
355 | // 方法
356 | switchTab,
357 | toggleServer,
358 | saveSettings,
359 | copyUrl,
360 | loadToolManagerState,
361 | updateToolStatus,
362 | selectAllTools,
363 | deselectAllTools,
364 | saveChanges,
365 | toggleCategoryTools,
366 | getToolsByCategory,
367 | getCategoryDisplayName
368 | };
369 | },
370 | template: readFileSync(join(__dirname, '../../../static/template/vue/mcp-server-app.html'), 'utf-8'),
371 | }));
372 |
373 | app.mount(this.$.app);
374 | panelDataMap.set(this, app);
375 |
376 | console.log('[MCP Panel] Vue3 app mounted successfully');
377 | }
378 | },
379 | beforeClose() { },
380 | close() {
381 | const app = panelDataMap.get(this);
382 | if (app) {
383 | app.unmount();
384 | }
385 | },
386 | });
```
--------------------------------------------------------------------------------
/source/tools/scene-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor, SceneInfo } from '../types';
2 |
3 | export class SceneTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'get_current_scene',
8 | description: 'Get current scene information',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {}
12 | }
13 | },
14 | {
15 | name: 'get_scene_list',
16 | description: 'Get all scenes in the project',
17 | inputSchema: {
18 | type: 'object',
19 | properties: {}
20 | }
21 | },
22 | {
23 | name: 'open_scene',
24 | description: 'Open a scene by path',
25 | inputSchema: {
26 | type: 'object',
27 | properties: {
28 | scenePath: {
29 | type: 'string',
30 | description: 'The scene file path'
31 | }
32 | },
33 | required: ['scenePath']
34 | }
35 | },
36 | {
37 | name: 'save_scene',
38 | description: 'Save current scene',
39 | inputSchema: {
40 | type: 'object',
41 | properties: {}
42 | }
43 | },
44 | {
45 | name: 'create_scene',
46 | description: 'Create a new scene asset',
47 | inputSchema: {
48 | type: 'object',
49 | properties: {
50 | sceneName: {
51 | type: 'string',
52 | description: 'Name of the new scene'
53 | },
54 | savePath: {
55 | type: 'string',
56 | description: 'Path to save the scene (e.g., db://assets/scenes/NewScene.scene)'
57 | }
58 | },
59 | required: ['sceneName', 'savePath']
60 | }
61 | },
62 | {
63 | name: 'save_scene_as',
64 | description: 'Save scene as new file',
65 | inputSchema: {
66 | type: 'object',
67 | properties: {
68 | path: {
69 | type: 'string',
70 | description: 'Path to save the scene'
71 | }
72 | },
73 | required: ['path']
74 | }
75 | },
76 | {
77 | name: 'close_scene',
78 | description: 'Close current scene',
79 | inputSchema: {
80 | type: 'object',
81 | properties: {}
82 | }
83 | },
84 | {
85 | name: 'get_scene_hierarchy',
86 | description: 'Get the complete hierarchy of current scene',
87 | inputSchema: {
88 | type: 'object',
89 | properties: {
90 | includeComponents: {
91 | type: 'boolean',
92 | description: 'Include component information',
93 | default: false
94 | }
95 | }
96 | }
97 | }
98 | ];
99 | }
100 |
101 | async execute(toolName: string, args: any): Promise<ToolResponse> {
102 | switch (toolName) {
103 | case 'get_current_scene':
104 | return await this.getCurrentScene();
105 | case 'get_scene_list':
106 | return await this.getSceneList();
107 | case 'open_scene':
108 | return await this.openScene(args.scenePath);
109 | case 'save_scene':
110 | return await this.saveScene();
111 | case 'create_scene':
112 | return await this.createScene(args.sceneName, args.savePath);
113 | case 'save_scene_as':
114 | return await this.saveSceneAs(args.path);
115 | case 'close_scene':
116 | return await this.closeScene();
117 | case 'get_scene_hierarchy':
118 | return await this.getSceneHierarchy(args.includeComponents);
119 | default:
120 | throw new Error(`Unknown tool: ${toolName}`);
121 | }
122 | }
123 |
124 | private async getCurrentScene(): Promise<ToolResponse> {
125 | return new Promise((resolve) => {
126 | // 直接使用 query-node-tree 来获取场景信息(这个方法已经验证可用)
127 | Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
128 | if (tree && tree.uuid) {
129 | resolve({
130 | success: true,
131 | data: {
132 | name: tree.name || 'Current Scene',
133 | uuid: tree.uuid,
134 | type: tree.type || 'cc.Scene',
135 | active: tree.active !== undefined ? tree.active : true,
136 | nodeCount: tree.children ? tree.children.length : 0
137 | }
138 | });
139 | } else {
140 | resolve({ success: false, error: 'No scene data available' });
141 | }
142 | }).catch((err: Error) => {
143 | // 备用方案:使用场景脚本
144 | const options = {
145 | name: 'cocos-mcp-server',
146 | method: 'getCurrentSceneInfo',
147 | args: []
148 | };
149 |
150 | Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
151 | resolve(result);
152 | }).catch((err2: Error) => {
153 | resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
154 | });
155 | });
156 | });
157 | }
158 |
159 | private async getSceneList(): Promise<ToolResponse> {
160 | return new Promise((resolve) => {
161 | // Note: query-assets API corrected with proper parameters
162 | Editor.Message.request('asset-db', 'query-assets', {
163 | pattern: 'db://assets/**/*.scene'
164 | }).then((results: any[]) => {
165 | const scenes: SceneInfo[] = results.map(asset => ({
166 | name: asset.name,
167 | path: asset.url,
168 | uuid: asset.uuid
169 | }));
170 | resolve({ success: true, data: scenes });
171 | }).catch((err: Error) => {
172 | resolve({ success: false, error: err.message });
173 | });
174 | });
175 | }
176 |
177 | private async openScene(scenePath: string): Promise<ToolResponse> {
178 | return new Promise((resolve) => {
179 | // 首先获取场景的UUID
180 | Editor.Message.request('asset-db', 'query-uuid', scenePath).then((uuid: string | null) => {
181 | if (!uuid) {
182 | throw new Error('Scene not found');
183 | }
184 |
185 | // 使用正确的 scene API 打开场景 (需要UUID)
186 | return Editor.Message.request('scene', 'open-scene', uuid);
187 | }).then(() => {
188 | resolve({ success: true, message: `Scene opened: ${scenePath}` });
189 | }).catch((err: Error) => {
190 | resolve({ success: false, error: err.message });
191 | });
192 | });
193 | }
194 |
195 | private async saveScene(): Promise<ToolResponse> {
196 | return new Promise((resolve) => {
197 | Editor.Message.request('scene', 'save-scene').then(() => {
198 | resolve({ success: true, message: 'Scene saved successfully' });
199 | }).catch((err: Error) => {
200 | resolve({ success: false, error: err.message });
201 | });
202 | });
203 | }
204 |
205 | private async createScene(sceneName: string, savePath: string): Promise<ToolResponse> {
206 | return new Promise((resolve) => {
207 | // 确保路径以.scene结尾
208 | const fullPath = savePath.endsWith('.scene') ? savePath : `${savePath}/${sceneName}.scene`;
209 |
210 | // 使用正确的Cocos Creator 3.8场景格式
211 | const sceneContent = JSON.stringify([
212 | {
213 | "__type__": "cc.SceneAsset",
214 | "_name": sceneName,
215 | "_objFlags": 0,
216 | "__editorExtras__": {},
217 | "_native": "",
218 | "scene": {
219 | "__id__": 1
220 | }
221 | },
222 | {
223 | "__type__": "cc.Scene",
224 | "_name": sceneName,
225 | "_objFlags": 0,
226 | "__editorExtras__": {},
227 | "_parent": null,
228 | "_children": [],
229 | "_active": true,
230 | "_components": [],
231 | "_prefab": null,
232 | "_lpos": {
233 | "__type__": "cc.Vec3",
234 | "x": 0,
235 | "y": 0,
236 | "z": 0
237 | },
238 | "_lrot": {
239 | "__type__": "cc.Quat",
240 | "x": 0,
241 | "y": 0,
242 | "z": 0,
243 | "w": 1
244 | },
245 | "_lscale": {
246 | "__type__": "cc.Vec3",
247 | "x": 1,
248 | "y": 1,
249 | "z": 1
250 | },
251 | "_mobility": 0,
252 | "_layer": 1073741824,
253 | "_euler": {
254 | "__type__": "cc.Vec3",
255 | "x": 0,
256 | "y": 0,
257 | "z": 0
258 | },
259 | "autoReleaseAssets": false,
260 | "_globals": {
261 | "__id__": 2
262 | },
263 | "_id": "scene"
264 | },
265 | {
266 | "__type__": "cc.SceneGlobals",
267 | "ambient": {
268 | "__id__": 3
269 | },
270 | "skybox": {
271 | "__id__": 4
272 | },
273 | "fog": {
274 | "__id__": 5
275 | },
276 | "octree": {
277 | "__id__": 6
278 | }
279 | },
280 | {
281 | "__type__": "cc.AmbientInfo",
282 | "_skyColorHDR": {
283 | "__type__": "cc.Vec4",
284 | "x": 0.2,
285 | "y": 0.5,
286 | "z": 0.8,
287 | "w": 0.520833
288 | },
289 | "_skyColor": {
290 | "__type__": "cc.Vec4",
291 | "x": 0.2,
292 | "y": 0.5,
293 | "z": 0.8,
294 | "w": 0.520833
295 | },
296 | "_skyIllumHDR": 20000,
297 | "_skyIllum": 20000,
298 | "_groundAlbedoHDR": {
299 | "__type__": "cc.Vec4",
300 | "x": 0.2,
301 | "y": 0.2,
302 | "z": 0.2,
303 | "w": 1
304 | },
305 | "_groundAlbedo": {
306 | "__type__": "cc.Vec4",
307 | "x": 0.2,
308 | "y": 0.2,
309 | "z": 0.2,
310 | "w": 1
311 | }
312 | },
313 | {
314 | "__type__": "cc.SkyboxInfo",
315 | "_envLightingType": 0,
316 | "_envmapHDR": null,
317 | "_envmap": null,
318 | "_envmapLodCount": 0,
319 | "_diffuseMapHDR": null,
320 | "_diffuseMap": null,
321 | "_enabled": false,
322 | "_useHDR": true,
323 | "_editableMaterial": null,
324 | "_reflectionHDR": null,
325 | "_reflectionMap": null,
326 | "_rotationAngle": 0
327 | },
328 | {
329 | "__type__": "cc.FogInfo",
330 | "_type": 0,
331 | "_fogColor": {
332 | "__type__": "cc.Color",
333 | "r": 200,
334 | "g": 200,
335 | "b": 200,
336 | "a": 255
337 | },
338 | "_enabled": false,
339 | "_fogDensity": 0.3,
340 | "_fogStart": 0.5,
341 | "_fogEnd": 300,
342 | "_fogAtten": 5,
343 | "_fogTop": 1.5,
344 | "_fogRange": 1.2,
345 | "_accurate": false
346 | },
347 | {
348 | "__type__": "cc.OctreeInfo",
349 | "_enabled": false,
350 | "_minPos": {
351 | "__type__": "cc.Vec3",
352 | "x": -1024,
353 | "y": -1024,
354 | "z": -1024
355 | },
356 | "_maxPos": {
357 | "__type__": "cc.Vec3",
358 | "x": 1024,
359 | "y": 1024,
360 | "z": 1024
361 | },
362 | "_depth": 8
363 | }
364 | ], null, 2);
365 |
366 | Editor.Message.request('asset-db', 'create-asset', fullPath, sceneContent).then((result: any) => {
367 | // Verify scene creation by checking if it exists
368 | this.getSceneList().then((sceneList) => {
369 | const createdScene = sceneList.data?.find((scene: any) => scene.uuid === result.uuid);
370 | resolve({
371 | success: true,
372 | data: {
373 | uuid: result.uuid,
374 | url: result.url,
375 | name: sceneName,
376 | message: `Scene '${sceneName}' created successfully`,
377 | sceneVerified: !!createdScene
378 | },
379 | verificationData: createdScene
380 | });
381 | }).catch(() => {
382 | resolve({
383 | success: true,
384 | data: {
385 | uuid: result.uuid,
386 | url: result.url,
387 | name: sceneName,
388 | message: `Scene '${sceneName}' created successfully (verification failed)`
389 | }
390 | });
391 | });
392 | }).catch((err: Error) => {
393 | resolve({ success: false, error: err.message });
394 | });
395 | });
396 | }
397 |
398 | private async getSceneHierarchy(includeComponents: boolean = false): Promise<ToolResponse> {
399 | return new Promise((resolve) => {
400 | // 优先尝试使用 Editor API 查询场景节点树
401 | Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
402 | if (tree) {
403 | const hierarchy = this.buildHierarchy(tree, includeComponents);
404 | resolve({
405 | success: true,
406 | data: hierarchy
407 | });
408 | } else {
409 | resolve({ success: false, error: 'No scene hierarchy available' });
410 | }
411 | }).catch((err: Error) => {
412 | // 备用方案:使用场景脚本
413 | const options = {
414 | name: 'cocos-mcp-server',
415 | method: 'getSceneHierarchy',
416 | args: [includeComponents]
417 | };
418 |
419 | Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
420 | resolve(result);
421 | }).catch((err2: Error) => {
422 | resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
423 | });
424 | });
425 | });
426 | }
427 |
428 | private buildHierarchy(node: any, includeComponents: boolean): any {
429 | const nodeInfo: any = {
430 | uuid: node.uuid,
431 | name: node.name,
432 | type: node.type,
433 | active: node.active,
434 | children: []
435 | };
436 |
437 | if (includeComponents && node.__comps__) {
438 | nodeInfo.components = node.__comps__.map((comp: any) => ({
439 | type: comp.__type__ || 'Unknown',
440 | enabled: comp.enabled !== undefined ? comp.enabled : true
441 | }));
442 | }
443 |
444 | if (node.children) {
445 | nodeInfo.children = node.children.map((child: any) =>
446 | this.buildHierarchy(child, includeComponents)
447 | );
448 | }
449 |
450 | return nodeInfo;
451 | }
452 |
453 | private async saveSceneAs(path: string): Promise<ToolResponse> {
454 | return new Promise((resolve) => {
455 | // save-as-scene API 不接受路径参数,会弹出对话框让用户选择
456 | (Editor.Message.request as any)('scene', 'save-as-scene').then(() => {
457 | resolve({
458 | success: true,
459 | data: {
460 | path: path,
461 | message: `Scene save-as dialog opened`
462 | }
463 | });
464 | }).catch((err: Error) => {
465 | resolve({ success: false, error: err.message });
466 | });
467 | });
468 | }
469 |
470 | private async closeScene(): Promise<ToolResponse> {
471 | return new Promise((resolve) => {
472 | Editor.Message.request('scene', 'close-scene').then(() => {
473 | resolve({
474 | success: true,
475 | message: 'Scene closed successfully'
476 | });
477 | }).catch((err: Error) => {
478 | resolve({ success: false, error: err.message });
479 | });
480 | });
481 | }
482 | }
```
--------------------------------------------------------------------------------
/source/tools/scene-view-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class SceneViewTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'change_gizmo_tool',
8 | description: 'Change Gizmo tool',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {
12 | name: {
13 | type: 'string',
14 | description: 'Tool name',
15 | enum: ['position', 'rotation', 'scale', 'rect']
16 | }
17 | },
18 | required: ['name']
19 | }
20 | },
21 | {
22 | name: 'query_gizmo_tool_name',
23 | description: 'Get current Gizmo tool name',
24 | inputSchema: {
25 | type: 'object',
26 | properties: {}
27 | }
28 | },
29 | {
30 | name: 'change_gizmo_pivot',
31 | description: 'Change transform pivot point',
32 | inputSchema: {
33 | type: 'object',
34 | properties: {
35 | name: {
36 | type: 'string',
37 | description: 'Pivot point',
38 | enum: ['pivot', 'center']
39 | }
40 | },
41 | required: ['name']
42 | }
43 | },
44 | {
45 | name: 'query_gizmo_pivot',
46 | description: 'Get current Gizmo pivot point',
47 | inputSchema: {
48 | type: 'object',
49 | properties: {}
50 | }
51 | },
52 | {
53 | name: 'query_gizmo_view_mode',
54 | description: 'Query view mode (view/select)',
55 | inputSchema: {
56 | type: 'object',
57 | properties: {}
58 | }
59 | },
60 | {
61 | name: 'change_gizmo_coordinate',
62 | description: 'Change coordinate system',
63 | inputSchema: {
64 | type: 'object',
65 | properties: {
66 | type: {
67 | type: 'string',
68 | description: 'Coordinate system',
69 | enum: ['local', 'global']
70 | }
71 | },
72 | required: ['type']
73 | }
74 | },
75 | {
76 | name: 'query_gizmo_coordinate',
77 | description: 'Get current coordinate system',
78 | inputSchema: {
79 | type: 'object',
80 | properties: {}
81 | }
82 | },
83 | {
84 | name: 'change_view_mode_2d_3d',
85 | description: 'Change 2D/3D view mode',
86 | inputSchema: {
87 | type: 'object',
88 | properties: {
89 | is2D: {
90 | type: 'boolean',
91 | description: '2D/3D view mode (true for 2D, false for 3D)'
92 | }
93 | },
94 | required: ['is2D']
95 | }
96 | },
97 | {
98 | name: 'query_view_mode_2d_3d',
99 | description: 'Get current view mode',
100 | inputSchema: {
101 | type: 'object',
102 | properties: {}
103 | }
104 | },
105 | {
106 | name: 'set_grid_visible',
107 | description: 'Show/hide grid',
108 | inputSchema: {
109 | type: 'object',
110 | properties: {
111 | visible: {
112 | type: 'boolean',
113 | description: 'Grid visibility'
114 | }
115 | },
116 | required: ['visible']
117 | }
118 | },
119 | {
120 | name: 'query_grid_visible',
121 | description: 'Query grid visibility status',
122 | inputSchema: {
123 | type: 'object',
124 | properties: {}
125 | }
126 | },
127 | {
128 | name: 'set_icon_gizmo_3d',
129 | description: 'Set IconGizmo to 3D or 2D mode',
130 | inputSchema: {
131 | type: 'object',
132 | properties: {
133 | is3D: {
134 | type: 'boolean',
135 | description: '3D/2D IconGizmo (true for 3D, false for 2D)'
136 | }
137 | },
138 | required: ['is3D']
139 | }
140 | },
141 | {
142 | name: 'query_icon_gizmo_3d',
143 | description: 'Query IconGizmo mode',
144 | inputSchema: {
145 | type: 'object',
146 | properties: {}
147 | }
148 | },
149 | {
150 | name: 'set_icon_gizmo_size',
151 | description: 'Set IconGizmo size',
152 | inputSchema: {
153 | type: 'object',
154 | properties: {
155 | size: {
156 | type: 'number',
157 | description: 'IconGizmo size',
158 | minimum: 10,
159 | maximum: 100
160 | }
161 | },
162 | required: ['size']
163 | }
164 | },
165 | {
166 | name: 'query_icon_gizmo_size',
167 | description: 'Query IconGizmo size',
168 | inputSchema: {
169 | type: 'object',
170 | properties: {}
171 | }
172 | },
173 | {
174 | name: 'focus_camera_on_nodes',
175 | description: 'Focus scene camera on nodes',
176 | inputSchema: {
177 | type: 'object',
178 | properties: {
179 | uuids: {
180 | oneOf: [
181 | { type: 'array', items: { type: 'string' } },
182 | { type: 'null' }
183 | ],
184 | description: 'Node UUIDs to focus on (null for all)'
185 | }
186 | },
187 | required: ['uuids']
188 | }
189 | },
190 | {
191 | name: 'align_camera_with_view',
192 | description: 'Apply scene camera position and angle to selected node',
193 | inputSchema: {
194 | type: 'object',
195 | properties: {}
196 | }
197 | },
198 | {
199 | name: 'align_view_with_node',
200 | description: 'Apply selected node position and angle to current view',
201 | inputSchema: {
202 | type: 'object',
203 | properties: {}
204 | }
205 | },
206 | {
207 | name: 'get_scene_view_status',
208 | description: 'Get comprehensive scene view status',
209 | inputSchema: {
210 | type: 'object',
211 | properties: {}
212 | }
213 | },
214 | {
215 | name: 'reset_scene_view',
216 | description: 'Reset scene view to default settings',
217 | inputSchema: {
218 | type: 'object',
219 | properties: {}
220 | }
221 | }
222 | ];
223 | }
224 |
225 | async execute(toolName: string, args: any): Promise<ToolResponse> {
226 | switch (toolName) {
227 | case 'change_gizmo_tool':
228 | return await this.changeGizmoTool(args.name);
229 | case 'query_gizmo_tool_name':
230 | return await this.queryGizmoToolName();
231 | case 'change_gizmo_pivot':
232 | return await this.changeGizmoPivot(args.name);
233 | case 'query_gizmo_pivot':
234 | return await this.queryGizmoPivot();
235 | case 'query_gizmo_view_mode':
236 | return await this.queryGizmoViewMode();
237 | case 'change_gizmo_coordinate':
238 | return await this.changeGizmoCoordinate(args.type);
239 | case 'query_gizmo_coordinate':
240 | return await this.queryGizmoCoordinate();
241 | case 'change_view_mode_2d_3d':
242 | return await this.changeViewMode2D3D(args.is2D);
243 | case 'query_view_mode_2d_3d':
244 | return await this.queryViewMode2D3D();
245 | case 'set_grid_visible':
246 | return await this.setGridVisible(args.visible);
247 | case 'query_grid_visible':
248 | return await this.queryGridVisible();
249 | case 'set_icon_gizmo_3d':
250 | return await this.setIconGizmo3D(args.is3D);
251 | case 'query_icon_gizmo_3d':
252 | return await this.queryIconGizmo3D();
253 | case 'set_icon_gizmo_size':
254 | return await this.setIconGizmoSize(args.size);
255 | case 'query_icon_gizmo_size':
256 | return await this.queryIconGizmoSize();
257 | case 'focus_camera_on_nodes':
258 | return await this.focusCameraOnNodes(args.uuids);
259 | case 'align_camera_with_view':
260 | return await this.alignCameraWithView();
261 | case 'align_view_with_node':
262 | return await this.alignViewWithNode();
263 | case 'get_scene_view_status':
264 | return await this.getSceneViewStatus();
265 | case 'reset_scene_view':
266 | return await this.resetSceneView();
267 | default:
268 | throw new Error(`Unknown tool: ${toolName}`);
269 | }
270 | }
271 |
272 | private async changeGizmoTool(name: string): Promise<ToolResponse> {
273 | return new Promise((resolve) => {
274 | Editor.Message.request('scene', 'change-gizmo-tool', name).then(() => {
275 | resolve({
276 | success: true,
277 | message: `Gizmo tool changed to '${name}'`
278 | });
279 | }).catch((err: Error) => {
280 | resolve({ success: false, error: err.message });
281 | });
282 | });
283 | }
284 |
285 | private async queryGizmoToolName(): Promise<ToolResponse> {
286 | return new Promise((resolve) => {
287 | Editor.Message.request('scene', 'query-gizmo-tool-name').then((toolName: string) => {
288 | resolve({
289 | success: true,
290 | data: {
291 | currentTool: toolName,
292 | message: `Current Gizmo tool: ${toolName}`
293 | }
294 | });
295 | }).catch((err: Error) => {
296 | resolve({ success: false, error: err.message });
297 | });
298 | });
299 | }
300 |
301 | private async changeGizmoPivot(name: string): Promise<ToolResponse> {
302 | return new Promise((resolve) => {
303 | Editor.Message.request('scene', 'change-gizmo-pivot', name).then(() => {
304 | resolve({
305 | success: true,
306 | message: `Gizmo pivot changed to '${name}'`
307 | });
308 | }).catch((err: Error) => {
309 | resolve({ success: false, error: err.message });
310 | });
311 | });
312 | }
313 |
314 | private async queryGizmoPivot(): Promise<ToolResponse> {
315 | return new Promise((resolve) => {
316 | Editor.Message.request('scene', 'query-gizmo-pivot').then((pivotName: string) => {
317 | resolve({
318 | success: true,
319 | data: {
320 | currentPivot: pivotName,
321 | message: `Current Gizmo pivot: ${pivotName}`
322 | }
323 | });
324 | }).catch((err: Error) => {
325 | resolve({ success: false, error: err.message });
326 | });
327 | });
328 | }
329 |
330 | private async queryGizmoViewMode(): Promise<ToolResponse> {
331 | return new Promise((resolve) => {
332 | Editor.Message.request('scene', 'query-gizmo-view-mode').then((viewMode: string) => {
333 | resolve({
334 | success: true,
335 | data: {
336 | viewMode: viewMode,
337 | message: `Current view mode: ${viewMode}`
338 | }
339 | });
340 | }).catch((err: Error) => {
341 | resolve({ success: false, error: err.message });
342 | });
343 | });
344 | }
345 |
346 | private async changeGizmoCoordinate(type: string): Promise<ToolResponse> {
347 | return new Promise((resolve) => {
348 | Editor.Message.request('scene', 'change-gizmo-coordinate', type).then(() => {
349 | resolve({
350 | success: true,
351 | message: `Coordinate system changed to '${type}'`
352 | });
353 | }).catch((err: Error) => {
354 | resolve({ success: false, error: err.message });
355 | });
356 | });
357 | }
358 |
359 | private async queryGizmoCoordinate(): Promise<ToolResponse> {
360 | return new Promise((resolve) => {
361 | Editor.Message.request('scene', 'query-gizmo-coordinate').then((coordinate: string) => {
362 | resolve({
363 | success: true,
364 | data: {
365 | coordinate: coordinate,
366 | message: `Current coordinate system: ${coordinate}`
367 | }
368 | });
369 | }).catch((err: Error) => {
370 | resolve({ success: false, error: err.message });
371 | });
372 | });
373 | }
374 |
375 | private async changeViewMode2D3D(is2D: boolean): Promise<ToolResponse> {
376 | return new Promise((resolve) => {
377 | Editor.Message.request('scene', 'change-is2D', is2D).then(() => {
378 | resolve({
379 | success: true,
380 | message: `View mode changed to ${is2D ? '2D' : '3D'}`
381 | });
382 | }).catch((err: Error) => {
383 | resolve({ success: false, error: err.message });
384 | });
385 | });
386 | }
387 |
388 | private async queryViewMode2D3D(): Promise<ToolResponse> {
389 | return new Promise((resolve) => {
390 | Editor.Message.request('scene', 'query-is2D').then((is2D: boolean) => {
391 | resolve({
392 | success: true,
393 | data: {
394 | is2D: is2D,
395 | viewMode: is2D ? '2D' : '3D',
396 | message: `Current view mode: ${is2D ? '2D' : '3D'}`
397 | }
398 | });
399 | }).catch((err: Error) => {
400 | resolve({ success: false, error: err.message });
401 | });
402 | });
403 | }
404 |
405 | private async setGridVisible(visible: boolean): Promise<ToolResponse> {
406 | return new Promise((resolve) => {
407 | Editor.Message.request('scene', 'set-grid-visible', visible).then(() => {
408 | resolve({
409 | success: true,
410 | message: `Grid ${visible ? 'shown' : 'hidden'}`
411 | });
412 | }).catch((err: Error) => {
413 | resolve({ success: false, error: err.message });
414 | });
415 | });
416 | }
417 |
418 | private async queryGridVisible(): Promise<ToolResponse> {
419 | return new Promise((resolve) => {
420 | Editor.Message.request('scene', 'query-is-grid-visible').then((visible: boolean) => {
421 | resolve({
422 | success: true,
423 | data: {
424 | visible: visible,
425 | message: `Grid is ${visible ? 'visible' : 'hidden'}`
426 | }
427 | });
428 | }).catch((err: Error) => {
429 | resolve({ success: false, error: err.message });
430 | });
431 | });
432 | }
433 |
434 | private async setIconGizmo3D(is3D: boolean): Promise<ToolResponse> {
435 | return new Promise((resolve) => {
436 | Editor.Message.request('scene', 'set-icon-gizmo-3d', is3D).then(() => {
437 | resolve({
438 | success: true,
439 | message: `IconGizmo set to ${is3D ? '3D' : '2D'} mode`
440 | });
441 | }).catch((err: Error) => {
442 | resolve({ success: false, error: err.message });
443 | });
444 | });
445 | }
446 |
447 | private async queryIconGizmo3D(): Promise<ToolResponse> {
448 | return new Promise((resolve) => {
449 | Editor.Message.request('scene', 'query-is-icon-gizmo-3d').then((is3D: boolean) => {
450 | resolve({
451 | success: true,
452 | data: {
453 | is3D: is3D,
454 | mode: is3D ? '3D' : '2D',
455 | message: `IconGizmo is in ${is3D ? '3D' : '2D'} mode`
456 | }
457 | });
458 | }).catch((err: Error) => {
459 | resolve({ success: false, error: err.message });
460 | });
461 | });
462 | }
463 |
464 | private async setIconGizmoSize(size: number): Promise<ToolResponse> {
465 | return new Promise((resolve) => {
466 | Editor.Message.request('scene', 'set-icon-gizmo-size', size).then(() => {
467 | resolve({
468 | success: true,
469 | message: `IconGizmo size set to ${size}`
470 | });
471 | }).catch((err: Error) => {
472 | resolve({ success: false, error: err.message });
473 | });
474 | });
475 | }
476 |
477 | private async queryIconGizmoSize(): Promise<ToolResponse> {
478 | return new Promise((resolve) => {
479 | Editor.Message.request('scene', 'query-icon-gizmo-size').then((size: number) => {
480 | resolve({
481 | success: true,
482 | data: {
483 | size: size,
484 | message: `IconGizmo size: ${size}`
485 | }
486 | });
487 | }).catch((err: Error) => {
488 | resolve({ success: false, error: err.message });
489 | });
490 | });
491 | }
492 |
493 | private async focusCameraOnNodes(uuids: string[] | null): Promise<ToolResponse> {
494 | return new Promise((resolve) => {
495 | Editor.Message.request('scene', 'focus-camera', uuids || []).then(() => {
496 | const message = uuids === null ?
497 | 'Camera focused on all nodes' :
498 | `Camera focused on ${uuids.length} node(s)`;
499 | resolve({
500 | success: true,
501 | message: message
502 | });
503 | }).catch((err: Error) => {
504 | resolve({ success: false, error: err.message });
505 | });
506 | });
507 | }
508 |
509 | private async alignCameraWithView(): Promise<ToolResponse> {
510 | return new Promise((resolve) => {
511 | Editor.Message.request('scene', 'align-with-view').then(() => {
512 | resolve({
513 | success: true,
514 | message: 'Scene camera aligned with current view'
515 | });
516 | }).catch((err: Error) => {
517 | resolve({ success: false, error: err.message });
518 | });
519 | });
520 | }
521 |
522 | private async alignViewWithNode(): Promise<ToolResponse> {
523 | return new Promise((resolve) => {
524 | Editor.Message.request('scene', 'align-with-view-node').then(() => {
525 | resolve({
526 | success: true,
527 | message: 'View aligned with selected node'
528 | });
529 | }).catch((err: Error) => {
530 | resolve({ success: false, error: err.message });
531 | });
532 | });
533 | }
534 |
535 | private async getSceneViewStatus(): Promise<ToolResponse> {
536 | return new Promise(async (resolve) => {
537 | try {
538 | // Gather all view status information
539 | const [
540 | gizmoTool,
541 | gizmoPivot,
542 | gizmoCoordinate,
543 | viewMode2D3D,
544 | gridVisible,
545 | iconGizmo3D,
546 | iconGizmoSize
547 | ] = await Promise.allSettled([
548 | this.queryGizmoToolName(),
549 | this.queryGizmoPivot(),
550 | this.queryGizmoCoordinate(),
551 | this.queryViewMode2D3D(),
552 | this.queryGridVisible(),
553 | this.queryIconGizmo3D(),
554 | this.queryIconGizmoSize()
555 | ]);
556 |
557 | const status: any = {
558 | timestamp: new Date().toISOString()
559 | };
560 |
561 | // Extract data from fulfilled promises
562 | if (gizmoTool.status === 'fulfilled' && gizmoTool.value.success) {
563 | status.gizmoTool = gizmoTool.value.data.currentTool;
564 | }
565 | if (gizmoPivot.status === 'fulfilled' && gizmoPivot.value.success) {
566 | status.gizmoPivot = gizmoPivot.value.data.currentPivot;
567 | }
568 | if (gizmoCoordinate.status === 'fulfilled' && gizmoCoordinate.value.success) {
569 | status.coordinate = gizmoCoordinate.value.data.coordinate;
570 | }
571 | if (viewMode2D3D.status === 'fulfilled' && viewMode2D3D.value.success) {
572 | status.is2D = viewMode2D3D.value.data.is2D;
573 | status.viewMode = viewMode2D3D.value.data.viewMode;
574 | }
575 | if (gridVisible.status === 'fulfilled' && gridVisible.value.success) {
576 | status.gridVisible = gridVisible.value.data.visible;
577 | }
578 | if (iconGizmo3D.status === 'fulfilled' && iconGizmo3D.value.success) {
579 | status.iconGizmo3D = iconGizmo3D.value.data.is3D;
580 | }
581 | if (iconGizmoSize.status === 'fulfilled' && iconGizmoSize.value.success) {
582 | status.iconGizmoSize = iconGizmoSize.value.data.size;
583 | }
584 |
585 | resolve({
586 | success: true,
587 | data: status
588 | });
589 |
590 | } catch (err: any) {
591 | resolve({
592 | success: false,
593 | error: `Failed to get scene view status: ${err.message}`
594 | });
595 | }
596 | });
597 | }
598 |
599 | private async resetSceneView(): Promise<ToolResponse> {
600 | return new Promise(async (resolve) => {
601 | try {
602 | // Reset scene view to default settings
603 | const resetActions = [
604 | this.changeGizmoTool('position'),
605 | this.changeGizmoPivot('pivot'),
606 | this.changeGizmoCoordinate('local'),
607 | this.changeViewMode2D3D(false), // 3D mode
608 | this.setGridVisible(true),
609 | this.setIconGizmo3D(true),
610 | this.setIconGizmoSize(60)
611 | ];
612 |
613 | await Promise.all(resetActions);
614 |
615 | resolve({
616 | success: true,
617 | message: 'Scene view reset to default settings'
618 | });
619 |
620 | } catch (err: any) {
621 | resolve({
622 | success: false,
623 | error: `Failed to reset scene view: ${err.message}`
624 | });
625 | }
626 | });
627 | }
628 | }
```
--------------------------------------------------------------------------------
/source/panels/tool-manager/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { readFileSync } from 'fs-extra';
2 | import { join } from 'path';
3 |
4 | module.exports = Editor.Panel.define({
5 | listeners: {
6 | show() { console.log('Tool Manager panel shown'); },
7 | hide() { console.log('Tool Manager panel hidden'); }
8 | },
9 | template: readFileSync(join(__dirname, '../../../static/template/default/tool-manager.html'), 'utf-8'),
10 | style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
11 | $: {
12 | panelTitle: '#panelTitle',
13 | createConfigBtn: '#createConfigBtn',
14 | importConfigBtn: '#importConfigBtn',
15 | exportConfigBtn: '#exportConfigBtn',
16 | configSelector: '#configSelector',
17 | applyConfigBtn: '#applyConfigBtn',
18 | editConfigBtn: '#editConfigBtn',
19 | deleteConfigBtn: '#deleteConfigBtn',
20 | toolsContainer: '#toolsContainer',
21 | selectAllBtn: '#selectAllBtn',
22 | deselectAllBtn: '#deselectAllBtn',
23 | saveChangesBtn: '#saveChangesBtn',
24 | totalToolsCount: '#totalToolsCount',
25 | enabledToolsCount: '#enabledToolsCount',
26 | disabledToolsCount: '#disabledToolsCount',
27 | configModal: '#configModal',
28 | modalTitle: '#modalTitle',
29 | configForm: '#configForm',
30 | configName: '#configName',
31 | configDescription: '#configDescription',
32 | closeModal: '#closeModal',
33 | cancelConfigBtn: '#cancelConfigBtn',
34 | saveConfigBtn: '#saveConfigBtn',
35 | importModal: '#importModal',
36 | importConfigJson: '#importConfigJson',
37 | closeImportModal: '#closeImportModal',
38 | cancelImportBtn: '#cancelImportBtn',
39 | confirmImportBtn: '#confirmImportBtn'
40 | },
41 | methods: {
42 | async loadToolManagerState(this: any) {
43 | try {
44 | this.toolManagerState = await Editor.Message.request('cocos-mcp-server', 'getToolManagerState');
45 | this.currentConfiguration = this.toolManagerState.currentConfiguration;
46 | this.configurations = this.toolManagerState.configurations;
47 | this.availableTools = this.toolManagerState.availableTools;
48 | this.updateUI();
49 | } catch (error) {
50 | console.error('Failed to load tool manager state:', error);
51 | this.showError('加载工具管理器状态失败');
52 | }
53 | },
54 |
55 | updateUI(this: any) {
56 | this.updateConfigSelector();
57 | this.updateToolsDisplay();
58 | this.updateStatusBar();
59 | this.updateButtons();
60 | },
61 |
62 | updateConfigSelector(this: any) {
63 | const selector = this.$.configSelector;
64 | selector.innerHTML = '<option value="">选择配置...</option>';
65 |
66 | this.configurations.forEach((config: any) => {
67 | const option = document.createElement('option');
68 | option.value = config.id;
69 | option.textContent = config.name;
70 | if (this.currentConfiguration && config.id === this.currentConfiguration.id) {
71 | option.selected = true;
72 | }
73 | selector.appendChild(option);
74 | });
75 | },
76 |
77 | updateToolsDisplay(this: any) {
78 | const container = this.$.toolsContainer;
79 |
80 | if (!this.currentConfiguration) {
81 | container.innerHTML = `
82 | <div class="empty-state">
83 | <h3>没有选择配置</h3>
84 | <p>请先选择一个配置或创建新配置</p>
85 | </div>
86 | `;
87 | return;
88 | }
89 |
90 | const toolsByCategory: any = {};
91 | this.currentConfiguration.tools.forEach((tool: any) => {
92 | if (!toolsByCategory[tool.category]) {
93 | toolsByCategory[tool.category] = [];
94 | }
95 | toolsByCategory[tool.category].push(tool);
96 | });
97 |
98 | container.innerHTML = '';
99 |
100 | Object.entries(toolsByCategory).forEach(([category, tools]: [string, any]) => {
101 | const categoryDiv = document.createElement('div');
102 | categoryDiv.className = 'tool-category';
103 |
104 | const enabledCount = tools.filter((t: any) => t.enabled).length;
105 | const totalCount = tools.length;
106 |
107 | categoryDiv.innerHTML = `
108 | <div class="category-header">
109 | <div class="category-name">${this.getCategoryDisplayName(category)}</div>
110 | <div class="category-toggle">
111 | <span>${enabledCount}/${totalCount}</span>
112 | <input type="checkbox" class="checkbox category-checkbox"
113 | data-category="${category}"
114 | ${enabledCount === totalCount ? 'checked' : ''}>
115 | </div>
116 | </div>
117 | <div class="tool-list">
118 | ${tools.map((tool: any) => `
119 | <div class="tool-item">
120 | <div class="tool-info">
121 | <div class="tool-name">${tool.name}</div>
122 | <div class="tool-description">${tool.description}</div>
123 | </div>
124 | <div class="tool-toggle">
125 | <input type="checkbox" class="checkbox tool-checkbox"
126 | data-category="${tool.category}"
127 | data-name="${tool.name}"
128 | ${tool.enabled ? 'checked' : ''}>
129 | </div>
130 | </div>
131 | `).join('')}
132 | </div>
133 | `;
134 |
135 | container.appendChild(categoryDiv);
136 | });
137 |
138 | this.bindToolEvents();
139 | },
140 |
141 | bindToolEvents(this: any) {
142 | document.querySelectorAll('.category-checkbox').forEach((checkbox: any) => {
143 | checkbox.addEventListener('change', (e: any) => {
144 | const category = e.target.dataset.category;
145 | const checked = e.target.checked;
146 | this.toggleCategoryTools(category, checked);
147 | });
148 | });
149 |
150 | document.querySelectorAll('.tool-checkbox').forEach((checkbox: any) => {
151 | checkbox.addEventListener('change', (e: any) => {
152 | const category = e.target.dataset.category;
153 | const name = e.target.dataset.name;
154 | const enabled = e.target.checked;
155 | this.updateToolStatus(category, name, enabled);
156 | });
157 | });
158 | },
159 |
160 | async toggleCategoryTools(this: any, category: string, enabled: boolean) {
161 | if (!this.currentConfiguration) return;
162 |
163 | console.log(`Toggling category tools: ${category} = ${enabled}`);
164 |
165 | const categoryTools = this.currentConfiguration.tools.filter((tool: any) => tool.category === category);
166 | if (categoryTools.length === 0) return;
167 |
168 | const updates = categoryTools.map((tool: any) => ({
169 | category: tool.category,
170 | name: tool.name,
171 | enabled: enabled
172 | }));
173 |
174 | try {
175 | // 先更新本地状态
176 | categoryTools.forEach((tool: any) => {
177 | tool.enabled = enabled;
178 | });
179 | console.log(`Updated local category state: ${category} = ${enabled}`);
180 |
181 | // 立即更新UI
182 | this.updateStatusBar();
183 | this.updateCategoryCounts();
184 | this.updateToolCheckboxes(category, enabled);
185 |
186 | // 然后发送到后端
187 | await Editor.Message.request('cocos-mcp-server', 'updateToolStatusBatch',
188 | this.currentConfiguration.id, updates);
189 |
190 | } catch (error) {
191 | console.error('Failed to toggle category tools:', error);
192 | this.showError('切换类别工具失败');
193 |
194 | // 如果后端更新失败,回滚本地状态
195 | categoryTools.forEach((tool: any) => {
196 | tool.enabled = !enabled;
197 | });
198 | this.updateStatusBar();
199 | this.updateCategoryCounts();
200 | this.updateToolCheckboxes(category, !enabled);
201 | }
202 | },
203 |
204 | async updateToolStatus(this: any, category: string, name: string, enabled: boolean) {
205 | if (!this.currentConfiguration) return;
206 |
207 | console.log(`Updating tool status: ${category}.${name} = ${enabled}`);
208 | console.log(`Current config ID: ${this.currentConfiguration.id}`);
209 |
210 | // 先更新本地状态
211 | const tool = this.currentConfiguration.tools.find((t: any) =>
212 | t.category === category && t.name === name);
213 | if (!tool) {
214 | console.error(`Tool not found: ${category}.${name}`);
215 | return;
216 | }
217 |
218 | try {
219 | tool.enabled = enabled;
220 | console.log(`Updated local tool state: ${tool.name} = ${tool.enabled}`);
221 |
222 | // 立即更新UI(只更新统计信息,不重新渲染工具列表)
223 | this.updateStatusBar();
224 | this.updateCategoryCounts();
225 |
226 | // 然后发送到后端
227 | console.log(`Sending to backend: configId=${this.currentConfiguration.id}, category=${category}, name=${name}, enabled=${enabled}`);
228 | const result = await Editor.Message.request('cocos-mcp-server', 'updateToolStatus',
229 | this.currentConfiguration.id, category, name, enabled);
230 | console.log('Backend response:', result);
231 |
232 | } catch (error) {
233 | console.error('Failed to update tool status:', error);
234 | this.showError('更新工具状态失败');
235 |
236 | // 如果后端更新失败,回滚本地状态
237 | tool.enabled = !enabled;
238 | this.updateStatusBar();
239 | this.updateCategoryCounts();
240 | }
241 | },
242 |
243 | updateStatusBar(this: any) {
244 | if (!this.currentConfiguration) {
245 | this.$.totalToolsCount.textContent = '0';
246 | this.$.enabledToolsCount.textContent = '0';
247 | this.$.disabledToolsCount.textContent = '0';
248 | return;
249 | }
250 |
251 | const total = this.currentConfiguration.tools.length;
252 | const enabled = this.currentConfiguration.tools.filter((t: any) => t.enabled).length;
253 | const disabled = total - enabled;
254 |
255 | console.log(`Status bar update: total=${total}, enabled=${enabled}, disabled=${disabled}`);
256 |
257 | this.$.totalToolsCount.textContent = total.toString();
258 | this.$.enabledToolsCount.textContent = enabled.toString();
259 | this.$.disabledToolsCount.textContent = disabled.toString();
260 | },
261 |
262 | updateCategoryCounts(this: any) {
263 | if (!this.currentConfiguration) return;
264 |
265 | // 更新每个类别的计数显示
266 | document.querySelectorAll('.category-checkbox').forEach((checkbox: any) => {
267 | const category = checkbox.dataset.category;
268 | const categoryTools = this.currentConfiguration.tools.filter((t: any) => t.category === category);
269 | const enabledCount = categoryTools.filter((t: any) => t.enabled).length;
270 | const totalCount = categoryTools.length;
271 |
272 | // 更新计数显示
273 | const countSpan = checkbox.parentElement.querySelector('span');
274 | if (countSpan) {
275 | countSpan.textContent = `${enabledCount}/${totalCount}`;
276 | }
277 |
278 | // 更新类别复选框状态
279 | checkbox.checked = enabledCount === totalCount;
280 | });
281 | },
282 |
283 | updateToolCheckboxes(this: any, category: string, enabled: boolean) {
284 | // 更新特定类别的所有工具复选框
285 | document.querySelectorAll(`.tool-checkbox[data-category="${category}"]`).forEach((checkbox: any) => {
286 | checkbox.checked = enabled;
287 | });
288 | },
289 |
290 | updateButtons(this: any) {
291 | const hasCurrentConfig = !!this.currentConfiguration;
292 | this.$.editConfigBtn.disabled = !hasCurrentConfig;
293 | this.$.deleteConfigBtn.disabled = !hasCurrentConfig;
294 | this.$.exportConfigBtn.disabled = !hasCurrentConfig;
295 | this.$.applyConfigBtn.disabled = !hasCurrentConfig;
296 | },
297 |
298 | async createConfiguration(this: any) {
299 | this.editingConfig = null;
300 | this.$.modalTitle.textContent = '新建配置';
301 | this.$.configName.value = '';
302 | this.$.configDescription.value = '';
303 | this.showModal('configModal');
304 | },
305 |
306 | async editConfiguration(this: any) {
307 | if (!this.currentConfiguration) return;
308 |
309 | this.editingConfig = this.currentConfiguration;
310 | this.$.modalTitle.textContent = '编辑配置';
311 | this.$.configName.value = this.currentConfiguration.name;
312 | this.$.configDescription.value = this.currentConfiguration.description || '';
313 | this.showModal('configModal');
314 | },
315 |
316 | async saveConfiguration(this: any) {
317 | const name = this.$.configName.value.trim();
318 | const description = this.$.configDescription.value.trim();
319 |
320 | if (!name) {
321 | this.showError('配置名称不能为空');
322 | return;
323 | }
324 |
325 | try {
326 | if (this.editingConfig) {
327 | await Editor.Message.request('cocos-mcp-server', 'updateToolConfiguration',
328 | this.editingConfig.id, { name, description });
329 | } else {
330 | await Editor.Message.request('cocos-mcp-server', 'createToolConfiguration', name, description);
331 | }
332 |
333 | this.hideModal('configModal');
334 | await this.loadToolManagerState();
335 | } catch (error) {
336 | console.error('Failed to save configuration:', error);
337 | this.showError('保存配置失败');
338 | }
339 | },
340 |
341 | async deleteConfiguration(this: any) {
342 | if (!this.currentConfiguration) return;
343 |
344 | const confirmed = await Editor.Dialog.warn('确认删除', {
345 | detail: `确定要删除配置 "${this.currentConfiguration.name}" 吗?此操作不可撤销。`
346 | });
347 |
348 | if (confirmed) {
349 | try {
350 | await Editor.Message.request('cocos-mcp-server', 'deleteToolConfiguration',
351 | this.currentConfiguration.id);
352 | await this.loadToolManagerState();
353 | } catch (error) {
354 | console.error('Failed to delete configuration:', error);
355 | this.showError('删除配置失败');
356 | }
357 | }
358 | },
359 |
360 | async applyConfiguration(this: any) {
361 | const configId = this.$.configSelector.value;
362 | if (!configId) return;
363 |
364 | try {
365 | await Editor.Message.request('cocos-mcp-server', 'setCurrentToolConfiguration', configId);
366 | await this.loadToolManagerState();
367 | } catch (error) {
368 | console.error('Failed to apply configuration:', error);
369 | this.showError('应用配置失败');
370 | }
371 | },
372 |
373 | async exportConfiguration(this: any) {
374 | if (!this.currentConfiguration) return;
375 |
376 | try {
377 | const result = await Editor.Message.request('cocos-mcp-server', 'exportToolConfiguration',
378 | this.currentConfiguration.id);
379 |
380 | Editor.Clipboard.write('text', result.configJson);
381 | Editor.Dialog.info('导出成功', { detail: '配置已复制到剪贴板' });
382 | } catch (error) {
383 | console.error('Failed to export configuration:', error);
384 | this.showError('导出配置失败');
385 | }
386 | },
387 |
388 | async importConfiguration(this: any) {
389 | this.$.importConfigJson.value = '';
390 | this.showModal('importModal');
391 | },
392 |
393 | async confirmImport(this: any) {
394 | const configJson = this.$.importConfigJson.value.trim();
395 | if (!configJson) {
396 | this.showError('请输入配置JSON');
397 | return;
398 | }
399 |
400 | try {
401 | await Editor.Message.request('cocos-mcp-server', 'importToolConfiguration', configJson);
402 | this.hideModal('importModal');
403 | await this.loadToolManagerState();
404 | Editor.Dialog.info('导入成功', { detail: '配置已成功导入' });
405 | } catch (error) {
406 | console.error('Failed to import configuration:', error);
407 | this.showError('导入配置失败');
408 | }
409 | },
410 |
411 | async selectAllTools(this: any) {
412 | if (!this.currentConfiguration) return;
413 |
414 | console.log('Selecting all tools');
415 |
416 | const updates = this.currentConfiguration.tools.map((tool: any) => ({
417 | category: tool.category,
418 | name: tool.name,
419 | enabled: true
420 | }));
421 |
422 | try {
423 | // 先更新本地状态
424 | this.currentConfiguration.tools.forEach((tool: any) => {
425 | tool.enabled = true;
426 | });
427 | console.log('Updated local state: all tools enabled');
428 |
429 | // 立即更新UI
430 | this.updateStatusBar();
431 | this.updateToolsDisplay();
432 |
433 | // 然后发送到后端
434 | await Editor.Message.request('cocos-mcp-server', 'updateToolStatusBatch',
435 | this.currentConfiguration.id, updates);
436 |
437 | } catch (error) {
438 | console.error('Failed to select all tools:', error);
439 | this.showError('全选工具失败');
440 |
441 | // 如果后端更新失败,回滚本地状态
442 | this.currentConfiguration.tools.forEach((tool: any) => {
443 | tool.enabled = false;
444 | });
445 | this.updateStatusBar();
446 | this.updateToolsDisplay();
447 | }
448 | },
449 |
450 | async deselectAllTools(this: any) {
451 | if (!this.currentConfiguration) return;
452 |
453 | console.log('Deselecting all tools');
454 |
455 | const updates = this.currentConfiguration.tools.map((tool: any) => ({
456 | category: tool.category,
457 | name: tool.name,
458 | enabled: false
459 | }));
460 |
461 | try {
462 | // 先更新本地状态
463 | this.currentConfiguration.tools.forEach((tool: any) => {
464 | tool.enabled = false;
465 | });
466 | console.log('Updated local state: all tools disabled');
467 |
468 | // 立即更新UI
469 | this.updateStatusBar();
470 | this.updateToolsDisplay();
471 |
472 | // 然后发送到后端
473 | await Editor.Message.request('cocos-mcp-server', 'updateToolStatusBatch',
474 | this.currentConfiguration.id, updates);
475 |
476 | } catch (error) {
477 | console.error('Failed to deselect all tools:', error);
478 | this.showError('取消全选工具失败');
479 |
480 | // 如果后端更新失败,回滚本地状态
481 | this.currentConfiguration.tools.forEach((tool: any) => {
482 | tool.enabled = true;
483 | });
484 | this.updateStatusBar();
485 | this.updateToolsDisplay();
486 | }
487 | },
488 |
489 | getCategoryDisplayName(this: any, category: string): string {
490 | const categoryNames: any = {
491 | 'scene': '场景工具',
492 | 'node': '节点工具',
493 | 'component': '组件工具',
494 | 'prefab': '预制体工具',
495 | 'project': '项目工具',
496 | 'debug': '调试工具',
497 | 'preferences': '偏好设置工具',
498 | 'server': '服务器工具',
499 | 'broadcast': '广播工具',
500 | 'sceneAdvanced': '高级场景工具',
501 | 'sceneView': '场景视图工具',
502 | 'referenceImage': '参考图片工具',
503 | 'assetAdvanced': '高级资源工具',
504 | 'validation': '验证工具'
505 | };
506 | return categoryNames[category] || category;
507 | },
508 |
509 | showModal(this: any, modalId: string) {
510 | this.$[modalId].style.display = 'block';
511 | },
512 |
513 | hideModal(this: any, modalId: string) {
514 | this.$[modalId].style.display = 'none';
515 | },
516 |
517 | showError(this: any, message: string) {
518 | Editor.Dialog.error('错误', { detail: message });
519 | },
520 |
521 | async saveChanges(this: any) {
522 | if (!this.currentConfiguration) {
523 | this.showError('没有选择配置');
524 | return;
525 | }
526 |
527 | try {
528 | // 确保当前配置已保存到后端
529 | await Editor.Message.request('cocos-mcp-server', 'updateToolConfiguration',
530 | this.currentConfiguration.id, {
531 | name: this.currentConfiguration.name,
532 | description: this.currentConfiguration.description,
533 | tools: this.currentConfiguration.tools
534 | });
535 |
536 | Editor.Dialog.info('保存成功', { detail: '配置更改已保存' });
537 | } catch (error) {
538 | console.error('Failed to save changes:', error);
539 | this.showError('保存更改失败');
540 | }
541 | },
542 |
543 | bindEvents(this: any) {
544 | this.$.createConfigBtn.addEventListener('click', this.createConfiguration.bind(this));
545 | this.$.editConfigBtn.addEventListener('click', this.editConfiguration.bind(this));
546 | this.$.deleteConfigBtn.addEventListener('click', this.deleteConfiguration.bind(this));
547 | this.$.applyConfigBtn.addEventListener('click', this.applyConfiguration.bind(this));
548 | this.$.exportConfigBtn.addEventListener('click', this.exportConfiguration.bind(this));
549 | this.$.importConfigBtn.addEventListener('click', this.importConfiguration.bind(this));
550 |
551 | this.$.selectAllBtn.addEventListener('click', this.selectAllTools.bind(this));
552 | this.$.deselectAllBtn.addEventListener('click', this.deselectAllTools.bind(this));
553 | this.$.saveChangesBtn.addEventListener('click', this.saveChanges.bind(this));
554 |
555 | this.$.closeModal.addEventListener('click', () => this.hideModal('configModal'));
556 | this.$.cancelConfigBtn.addEventListener('click', () => this.hideModal('configModal'));
557 | this.$.configForm.addEventListener('submit', (e: any) => {
558 | e.preventDefault();
559 | this.saveConfiguration();
560 | });
561 |
562 | this.$.closeImportModal.addEventListener('click', () => this.hideModal('importModal'));
563 | this.$.cancelImportBtn.addEventListener('click', () => this.hideModal('importModal'));
564 | this.$.confirmImportBtn.addEventListener('click', this.confirmImport.bind(this));
565 |
566 | this.$.configSelector.addEventListener('change', this.applyConfiguration.bind(this));
567 | }
568 | },
569 | ready() {
570 | (this as any).toolManagerState = null;
571 | (this as any).currentConfiguration = null;
572 | (this as any).configurations = [];
573 | (this as any).availableTools = [];
574 | (this as any).editingConfig = null;
575 |
576 | (this as any).bindEvents();
577 | (this as any).loadToolManagerState();
578 | },
579 | beforeClose() {
580 | // 清理工作
581 | },
582 | close() {
583 | // 面板关闭清理
584 | }
585 | } as any);
```
--------------------------------------------------------------------------------
/source/tools/debug-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor, ConsoleMessage, PerformanceStats, ValidationResult, ValidationIssue } from '../types';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 |
5 | export class DebugTools implements ToolExecutor {
6 | private consoleMessages: ConsoleMessage[] = [];
7 | private readonly maxMessages = 1000;
8 |
9 | constructor() {
10 | this.setupConsoleCapture();
11 | }
12 |
13 | private setupConsoleCapture(): void {
14 | // Intercept Editor console messages
15 | // Note: Editor.Message.addBroadcastListener may not be available in all versions
16 | // This is a placeholder for console capture implementation
17 | console.log('Console capture setup - implementation depends on Editor API availability');
18 | }
19 |
20 | private addConsoleMessage(message: any): void {
21 | this.consoleMessages.push({
22 | timestamp: new Date().toISOString(),
23 | ...message
24 | });
25 |
26 | // Keep only latest messages
27 | if (this.consoleMessages.length > this.maxMessages) {
28 | this.consoleMessages.shift();
29 | }
30 | }
31 |
32 | getTools(): ToolDefinition[] {
33 | return [
34 | {
35 | name: 'get_console_logs',
36 | description: 'Get editor console logs',
37 | inputSchema: {
38 | type: 'object',
39 | properties: {
40 | limit: {
41 | type: 'number',
42 | description: 'Number of recent logs to retrieve',
43 | default: 100
44 | },
45 | filter: {
46 | type: 'string',
47 | description: 'Filter logs by type',
48 | enum: ['all', 'log', 'warn', 'error', 'info'],
49 | default: 'all'
50 | }
51 | }
52 | }
53 | },
54 | {
55 | name: 'clear_console',
56 | description: 'Clear editor console',
57 | inputSchema: {
58 | type: 'object',
59 | properties: {}
60 | }
61 | },
62 | {
63 | name: 'execute_script',
64 | description: 'Execute JavaScript in scene context',
65 | inputSchema: {
66 | type: 'object',
67 | properties: {
68 | script: {
69 | type: 'string',
70 | description: 'JavaScript code to execute'
71 | }
72 | },
73 | required: ['script']
74 | }
75 | },
76 | {
77 | name: 'get_node_tree',
78 | description: 'Get detailed node tree for debugging',
79 | inputSchema: {
80 | type: 'object',
81 | properties: {
82 | rootUuid: {
83 | type: 'string',
84 | description: 'Root node UUID (optional, uses scene root if not provided)'
85 | },
86 | maxDepth: {
87 | type: 'number',
88 | description: 'Maximum tree depth',
89 | default: 10
90 | }
91 | }
92 | }
93 | },
94 | {
95 | name: 'get_performance_stats',
96 | description: 'Get performance statistics',
97 | inputSchema: {
98 | type: 'object',
99 | properties: {}
100 | }
101 | },
102 | {
103 | name: 'validate_scene',
104 | description: 'Validate current scene for issues',
105 | inputSchema: {
106 | type: 'object',
107 | properties: {
108 | checkMissingAssets: {
109 | type: 'boolean',
110 | description: 'Check for missing asset references',
111 | default: true
112 | },
113 | checkPerformance: {
114 | type: 'boolean',
115 | description: 'Check for performance issues',
116 | default: true
117 | }
118 | }
119 | }
120 | },
121 | {
122 | name: 'get_editor_info',
123 | description: 'Get editor and environment information',
124 | inputSchema: {
125 | type: 'object',
126 | properties: {}
127 | }
128 | },
129 | {
130 | name: 'get_project_logs',
131 | description: 'Get project logs from temp/logs/project.log file',
132 | inputSchema: {
133 | type: 'object',
134 | properties: {
135 | lines: {
136 | type: 'number',
137 | description: 'Number of lines to read from the end of the log file (default: 100)',
138 | default: 100,
139 | minimum: 1,
140 | maximum: 10000
141 | },
142 | filterKeyword: {
143 | type: 'string',
144 | description: 'Filter logs containing specific keyword (optional)'
145 | },
146 | logLevel: {
147 | type: 'string',
148 | description: 'Filter by log level',
149 | enum: ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL'],
150 | default: 'ALL'
151 | }
152 | }
153 | }
154 | },
155 | {
156 | name: 'get_log_file_info',
157 | description: 'Get information about the project log file',
158 | inputSchema: {
159 | type: 'object',
160 | properties: {}
161 | }
162 | },
163 | {
164 | name: 'search_project_logs',
165 | description: 'Search for specific patterns or errors in project logs',
166 | inputSchema: {
167 | type: 'object',
168 | properties: {
169 | pattern: {
170 | type: 'string',
171 | description: 'Search pattern (supports regex)'
172 | },
173 | maxResults: {
174 | type: 'number',
175 | description: 'Maximum number of matching results',
176 | default: 20,
177 | minimum: 1,
178 | maximum: 100
179 | },
180 | contextLines: {
181 | type: 'number',
182 | description: 'Number of context lines to show around each match',
183 | default: 2,
184 | minimum: 0,
185 | maximum: 10
186 | }
187 | },
188 | required: ['pattern']
189 | }
190 | }
191 | ];
192 | }
193 |
194 | async execute(toolName: string, args: any): Promise<ToolResponse> {
195 | switch (toolName) {
196 | case 'get_console_logs':
197 | return await this.getConsoleLogs(args.limit, args.filter);
198 | case 'clear_console':
199 | return await this.clearConsole();
200 | case 'execute_script':
201 | return await this.executeScript(args.script);
202 | case 'get_node_tree':
203 | return await this.getNodeTree(args.rootUuid, args.maxDepth);
204 | case 'get_performance_stats':
205 | return await this.getPerformanceStats();
206 | case 'validate_scene':
207 | return await this.validateScene(args);
208 | case 'get_editor_info':
209 | return await this.getEditorInfo();
210 | case 'get_project_logs':
211 | return await this.getProjectLogs(args.lines, args.filterKeyword, args.logLevel);
212 | case 'get_log_file_info':
213 | return await this.getLogFileInfo();
214 | case 'search_project_logs':
215 | return await this.searchProjectLogs(args.pattern, args.maxResults, args.contextLines);
216 | default:
217 | throw new Error(`Unknown tool: ${toolName}`);
218 | }
219 | }
220 |
221 | private async getConsoleLogs(limit: number = 100, filter: string = 'all'): Promise<ToolResponse> {
222 | let logs = this.consoleMessages;
223 |
224 | if (filter !== 'all') {
225 | logs = logs.filter(log => log.type === filter);
226 | }
227 |
228 | const recentLogs = logs.slice(-limit);
229 |
230 | return {
231 | success: true,
232 | data: {
233 | total: logs.length,
234 | returned: recentLogs.length,
235 | logs: recentLogs
236 | }
237 | };
238 | }
239 |
240 | private async clearConsole(): Promise<ToolResponse> {
241 | this.consoleMessages = [];
242 |
243 | try {
244 | // Note: Editor.Message.send may not return a promise in all versions
245 | Editor.Message.send('console', 'clear');
246 | return {
247 | success: true,
248 | message: 'Console cleared successfully'
249 | };
250 | } catch (err: any) {
251 | return { success: false, error: err.message };
252 | }
253 | }
254 |
255 | private async executeScript(script: string): Promise<ToolResponse> {
256 | return new Promise((resolve) => {
257 | Editor.Message.request('scene', 'execute-scene-script', {
258 | name: 'console',
259 | method: 'eval',
260 | args: [script]
261 | }).then((result: any) => {
262 | resolve({
263 | success: true,
264 | data: {
265 | result: result,
266 | message: 'Script executed successfully'
267 | }
268 | });
269 | }).catch((err: Error) => {
270 | resolve({ success: false, error: err.message });
271 | });
272 | });
273 | }
274 |
275 | private async getNodeTree(rootUuid?: string, maxDepth: number = 10): Promise<ToolResponse> {
276 | return new Promise((resolve) => {
277 | const buildTree = async (nodeUuid: string, depth: number = 0): Promise<any> => {
278 | if (depth >= maxDepth) {
279 | return { truncated: true };
280 | }
281 |
282 | try {
283 | const nodeData = await Editor.Message.request('scene', 'query-node', nodeUuid);
284 |
285 | const tree = {
286 | uuid: nodeData.uuid,
287 | name: nodeData.name,
288 | active: nodeData.active,
289 | components: (nodeData as any).components ? (nodeData as any).components.map((c: any) => c.__type__) : [],
290 | childCount: nodeData.children ? nodeData.children.length : 0,
291 | children: [] as any[]
292 | };
293 |
294 | if (nodeData.children && nodeData.children.length > 0) {
295 | for (const childId of nodeData.children) {
296 | const childTree = await buildTree(childId, depth + 1);
297 | tree.children.push(childTree);
298 | }
299 | }
300 |
301 | return tree;
302 | } catch (err: any) {
303 | return { error: err.message };
304 | }
305 | };
306 |
307 | if (rootUuid) {
308 | buildTree(rootUuid).then(tree => {
309 | resolve({ success: true, data: tree });
310 | });
311 | } else {
312 | Editor.Message.request('scene', 'query-hierarchy').then(async (hierarchy: any) => {
313 | const trees = [];
314 | for (const rootNode of hierarchy.children) {
315 | const tree = await buildTree(rootNode.uuid);
316 | trees.push(tree);
317 | }
318 | resolve({ success: true, data: trees });
319 | }).catch((err: Error) => {
320 | resolve({ success: false, error: err.message });
321 | });
322 | }
323 | });
324 | }
325 |
326 | private async getPerformanceStats(): Promise<ToolResponse> {
327 | return new Promise((resolve) => {
328 | Editor.Message.request('scene', 'query-performance').then((stats: any) => {
329 | const perfStats: PerformanceStats = {
330 | nodeCount: stats.nodeCount || 0,
331 | componentCount: stats.componentCount || 0,
332 | drawCalls: stats.drawCalls || 0,
333 | triangles: stats.triangles || 0,
334 | memory: stats.memory || {}
335 | };
336 | resolve({ success: true, data: perfStats });
337 | }).catch(() => {
338 | // Fallback to basic stats
339 | resolve({
340 | success: true,
341 | data: {
342 | message: 'Performance stats not available in edit mode'
343 | }
344 | });
345 | });
346 | });
347 | }
348 |
349 | private async validateScene(options: any): Promise<ToolResponse> {
350 | const issues: ValidationIssue[] = [];
351 |
352 | try {
353 | // Check for missing assets
354 | if (options.checkMissingAssets) {
355 | const assetCheck = await Editor.Message.request('scene', 'check-missing-assets');
356 | if (assetCheck && assetCheck.missing) {
357 | issues.push({
358 | type: 'error',
359 | category: 'assets',
360 | message: `Found ${assetCheck.missing.length} missing asset references`,
361 | details: assetCheck.missing
362 | });
363 | }
364 | }
365 |
366 | // Check for performance issues
367 | if (options.checkPerformance) {
368 | const hierarchy = await Editor.Message.request('scene', 'query-hierarchy');
369 | const nodeCount = this.countNodes(hierarchy.children);
370 |
371 | if (nodeCount > 1000) {
372 | issues.push({
373 | type: 'warning',
374 | category: 'performance',
375 | message: `High node count: ${nodeCount} nodes (recommended < 1000)`,
376 | suggestion: 'Consider using object pooling or scene optimization'
377 | });
378 | }
379 | }
380 |
381 | const result: ValidationResult = {
382 | valid: issues.length === 0,
383 | issueCount: issues.length,
384 | issues: issues
385 | };
386 |
387 | return { success: true, data: result };
388 | } catch (err: any) {
389 | return { success: false, error: err.message };
390 | }
391 | }
392 |
393 | private countNodes(nodes: any[]): number {
394 | let count = nodes.length;
395 | for (const node of nodes) {
396 | if (node.children) {
397 | count += this.countNodes(node.children);
398 | }
399 | }
400 | return count;
401 | }
402 |
403 | private async getEditorInfo(): Promise<ToolResponse> {
404 | const info = {
405 | editor: {
406 | version: (Editor as any).versions?.editor || 'Unknown',
407 | cocosVersion: (Editor as any).versions?.cocos || 'Unknown',
408 | platform: process.platform,
409 | arch: process.arch,
410 | nodeVersion: process.version
411 | },
412 | project: {
413 | name: Editor.Project.name,
414 | path: Editor.Project.path,
415 | uuid: Editor.Project.uuid
416 | },
417 | memory: process.memoryUsage(),
418 | uptime: process.uptime()
419 | };
420 |
421 | return { success: true, data: info };
422 | }
423 |
424 | private async getProjectLogs(lines: number = 100, filterKeyword?: string, logLevel: string = 'ALL'): Promise<ToolResponse> {
425 | try {
426 | // Try multiple possible project paths
427 | let logFilePath = '';
428 | const possiblePaths = [
429 | Editor.Project ? Editor.Project.path : null,
430 | '/Users/lizhiyong/NewProject_3',
431 | process.cwd(),
432 | ].filter(p => p !== null);
433 |
434 | for (const basePath of possiblePaths) {
435 | const testPath = path.join(basePath, 'temp/logs/project.log');
436 | if (fs.existsSync(testPath)) {
437 | logFilePath = testPath;
438 | break;
439 | }
440 | }
441 |
442 | if (!logFilePath) {
443 | return {
444 | success: false,
445 | error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
446 | };
447 | }
448 |
449 | // Read the file content
450 | const logContent = fs.readFileSync(logFilePath, 'utf8');
451 | const logLines = logContent.split('\n').filter(line => line.trim() !== '');
452 |
453 | // Get the last N lines
454 | const recentLines = logLines.slice(-lines);
455 |
456 | // Apply filters
457 | let filteredLines = recentLines;
458 |
459 | // Filter by log level if not 'ALL'
460 | if (logLevel !== 'ALL') {
461 | filteredLines = filteredLines.filter(line =>
462 | line.includes(`[${logLevel}]`) || line.includes(logLevel.toLowerCase())
463 | );
464 | }
465 |
466 | // Filter by keyword if provided
467 | if (filterKeyword) {
468 | filteredLines = filteredLines.filter(line =>
469 | line.toLowerCase().includes(filterKeyword.toLowerCase())
470 | );
471 | }
472 |
473 | return {
474 | success: true,
475 | data: {
476 | totalLines: logLines.length,
477 | requestedLines: lines,
478 | filteredLines: filteredLines.length,
479 | logLevel: logLevel,
480 | filterKeyword: filterKeyword || null,
481 | logs: filteredLines,
482 | logFilePath: logFilePath
483 | }
484 | };
485 | } catch (error: any) {
486 | return {
487 | success: false,
488 | error: `Failed to read project logs: ${error.message}`
489 | };
490 | }
491 | }
492 |
493 | private async getLogFileInfo(): Promise<ToolResponse> {
494 | try {
495 | // Try multiple possible project paths
496 | let logFilePath = '';
497 | const possiblePaths = [
498 | Editor.Project ? Editor.Project.path : null,
499 | '/Users/lizhiyong/NewProject_3',
500 | process.cwd(),
501 | ].filter(p => p !== null);
502 |
503 | for (const basePath of possiblePaths) {
504 | const testPath = path.join(basePath, 'temp/logs/project.log');
505 | if (fs.existsSync(testPath)) {
506 | logFilePath = testPath;
507 | break;
508 | }
509 | }
510 |
511 | if (!logFilePath) {
512 | return {
513 | success: false,
514 | error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
515 | };
516 | }
517 |
518 | const stats = fs.statSync(logFilePath);
519 | const logContent = fs.readFileSync(logFilePath, 'utf8');
520 | const lineCount = logContent.split('\n').filter(line => line.trim() !== '').length;
521 |
522 | return {
523 | success: true,
524 | data: {
525 | filePath: logFilePath,
526 | fileSize: stats.size,
527 | fileSizeFormatted: this.formatFileSize(stats.size),
528 | lastModified: stats.mtime.toISOString(),
529 | lineCount: lineCount,
530 | created: stats.birthtime.toISOString(),
531 | accessible: fs.constants.R_OK
532 | }
533 | };
534 | } catch (error: any) {
535 | return {
536 | success: false,
537 | error: `Failed to get log file info: ${error.message}`
538 | };
539 | }
540 | }
541 |
542 | private async searchProjectLogs(pattern: string, maxResults: number = 20, contextLines: number = 2): Promise<ToolResponse> {
543 | try {
544 | // Try multiple possible project paths
545 | let logFilePath = '';
546 | const possiblePaths = [
547 | Editor.Project ? Editor.Project.path : null,
548 | '/Users/lizhiyong/NewProject_3',
549 | process.cwd(),
550 | ].filter(p => p !== null);
551 |
552 | for (const basePath of possiblePaths) {
553 | const testPath = path.join(basePath, 'temp/logs/project.log');
554 | if (fs.existsSync(testPath)) {
555 | logFilePath = testPath;
556 | break;
557 | }
558 | }
559 |
560 | if (!logFilePath) {
561 | return {
562 | success: false,
563 | error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
564 | };
565 | }
566 |
567 | const logContent = fs.readFileSync(logFilePath, 'utf8');
568 | const logLines = logContent.split('\n');
569 |
570 | // Create regex pattern (support both string and regex patterns)
571 | let regex: RegExp;
572 | try {
573 | regex = new RegExp(pattern, 'gi');
574 | } catch {
575 | // If pattern is not valid regex, treat as literal string
576 | regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
577 | }
578 |
579 | const matches: any[] = [];
580 | let resultCount = 0;
581 |
582 | for (let i = 0; i < logLines.length && resultCount < maxResults; i++) {
583 | const line = logLines[i];
584 | if (regex.test(line)) {
585 | // Get context lines
586 | const contextStart = Math.max(0, i - contextLines);
587 | const contextEnd = Math.min(logLines.length - 1, i + contextLines);
588 |
589 | const contextLinesArray = [];
590 | for (let j = contextStart; j <= contextEnd; j++) {
591 | contextLinesArray.push({
592 | lineNumber: j + 1,
593 | content: logLines[j],
594 | isMatch: j === i
595 | });
596 | }
597 |
598 | matches.push({
599 | lineNumber: i + 1,
600 | matchedLine: line,
601 | context: contextLinesArray
602 | });
603 |
604 | resultCount++;
605 |
606 | // Reset regex lastIndex for global search
607 | regex.lastIndex = 0;
608 | }
609 | }
610 |
611 | return {
612 | success: true,
613 | data: {
614 | pattern: pattern,
615 | totalMatches: matches.length,
616 | maxResults: maxResults,
617 | contextLines: contextLines,
618 | logFilePath: logFilePath,
619 | matches: matches
620 | }
621 | };
622 | } catch (error: any) {
623 | return {
624 | success: false,
625 | error: `Failed to search project logs: ${error.message}`
626 | };
627 | }
628 | }
629 |
630 | private formatFileSize(bytes: number): string {
631 | const units = ['B', 'KB', 'MB', 'GB'];
632 | let size = bytes;
633 | let unitIndex = 0;
634 |
635 | while (size >= 1024 && unitIndex < units.length - 1) {
636 | size /= 1024;
637 | unitIndex++;
638 | }
639 |
640 | return `${size.toFixed(2)} ${units[unitIndex]}`;
641 | }
642 | }
```
--------------------------------------------------------------------------------
/source/tools/asset-advanced-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class AssetAdvancedTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'save_asset_meta',
8 | description: 'Save asset meta information',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {
12 | urlOrUUID: {
13 | type: 'string',
14 | description: 'Asset URL or UUID'
15 | },
16 | content: {
17 | type: 'string',
18 | description: 'Asset meta serialized content string'
19 | }
20 | },
21 | required: ['urlOrUUID', 'content']
22 | }
23 | },
24 | {
25 | name: 'generate_available_url',
26 | description: 'Generate an available URL based on input URL',
27 | inputSchema: {
28 | type: 'object',
29 | properties: {
30 | url: {
31 | type: 'string',
32 | description: 'Asset URL to generate available URL for'
33 | }
34 | },
35 | required: ['url']
36 | }
37 | },
38 | {
39 | name: 'query_asset_db_ready',
40 | description: 'Check if asset database is ready',
41 | inputSchema: {
42 | type: 'object',
43 | properties: {}
44 | }
45 | },
46 | {
47 | name: 'open_asset_external',
48 | description: 'Open asset with external program',
49 | inputSchema: {
50 | type: 'object',
51 | properties: {
52 | urlOrUUID: {
53 | type: 'string',
54 | description: 'Asset URL or UUID to open'
55 | }
56 | },
57 | required: ['urlOrUUID']
58 | }
59 | },
60 | {
61 | name: 'batch_import_assets',
62 | description: 'Import multiple assets in batch',
63 | inputSchema: {
64 | type: 'object',
65 | properties: {
66 | sourceDirectory: {
67 | type: 'string',
68 | description: 'Source directory path'
69 | },
70 | targetDirectory: {
71 | type: 'string',
72 | description: 'Target directory URL'
73 | },
74 | fileFilter: {
75 | type: 'array',
76 | items: { type: 'string' },
77 | description: 'File extensions to include (e.g., [".png", ".jpg"])',
78 | default: []
79 | },
80 | recursive: {
81 | type: 'boolean',
82 | description: 'Include subdirectories',
83 | default: false
84 | },
85 | overwrite: {
86 | type: 'boolean',
87 | description: 'Overwrite existing files',
88 | default: false
89 | }
90 | },
91 | required: ['sourceDirectory', 'targetDirectory']
92 | }
93 | },
94 | {
95 | name: 'batch_delete_assets',
96 | description: 'Delete multiple assets in batch',
97 | inputSchema: {
98 | type: 'object',
99 | properties: {
100 | urls: {
101 | type: 'array',
102 | items: { type: 'string' },
103 | description: 'Array of asset URLs to delete'
104 | }
105 | },
106 | required: ['urls']
107 | }
108 | },
109 | {
110 | name: 'validate_asset_references',
111 | description: 'Validate asset references and find broken links',
112 | inputSchema: {
113 | type: 'object',
114 | properties: {
115 | directory: {
116 | type: 'string',
117 | description: 'Directory to validate (default: entire project)',
118 | default: 'db://assets'
119 | }
120 | }
121 | }
122 | },
123 | {
124 | name: 'get_asset_dependencies',
125 | description: 'Get asset dependency tree',
126 | inputSchema: {
127 | type: 'object',
128 | properties: {
129 | urlOrUUID: {
130 | type: 'string',
131 | description: 'Asset URL or UUID'
132 | },
133 | direction: {
134 | type: 'string',
135 | description: 'Dependency direction',
136 | enum: ['dependents', 'dependencies', 'both'],
137 | default: 'dependencies'
138 | }
139 | },
140 | required: ['urlOrUUID']
141 | }
142 | },
143 | {
144 | name: 'get_unused_assets',
145 | description: 'Find unused assets in project',
146 | inputSchema: {
147 | type: 'object',
148 | properties: {
149 | directory: {
150 | type: 'string',
151 | description: 'Directory to scan (default: entire project)',
152 | default: 'db://assets'
153 | },
154 | excludeDirectories: {
155 | type: 'array',
156 | items: { type: 'string' },
157 | description: 'Directories to exclude from scan',
158 | default: []
159 | }
160 | }
161 | }
162 | },
163 | {
164 | name: 'compress_textures',
165 | description: 'Batch compress texture assets',
166 | inputSchema: {
167 | type: 'object',
168 | properties: {
169 | directory: {
170 | type: 'string',
171 | description: 'Directory containing textures',
172 | default: 'db://assets'
173 | },
174 | format: {
175 | type: 'string',
176 | description: 'Compression format',
177 | enum: ['auto', 'jpg', 'png', 'webp'],
178 | default: 'auto'
179 | },
180 | quality: {
181 | type: 'number',
182 | description: 'Compression quality (0.1-1.0)',
183 | minimum: 0.1,
184 | maximum: 1.0,
185 | default: 0.8
186 | }
187 | }
188 | }
189 | },
190 | {
191 | name: 'export_asset_manifest',
192 | description: 'Export asset manifest/inventory',
193 | inputSchema: {
194 | type: 'object',
195 | properties: {
196 | directory: {
197 | type: 'string',
198 | description: 'Directory to export manifest for',
199 | default: 'db://assets'
200 | },
201 | format: {
202 | type: 'string',
203 | description: 'Export format',
204 | enum: ['json', 'csv', 'xml'],
205 | default: 'json'
206 | },
207 | includeMetadata: {
208 | type: 'boolean',
209 | description: 'Include asset metadata',
210 | default: true
211 | }
212 | }
213 | }
214 | }
215 | ];
216 | }
217 |
218 | async execute(toolName: string, args: any): Promise<ToolResponse> {
219 | switch (toolName) {
220 | case 'save_asset_meta':
221 | return await this.saveAssetMeta(args.urlOrUUID, args.content);
222 | case 'generate_available_url':
223 | return await this.generateAvailableUrl(args.url);
224 | case 'query_asset_db_ready':
225 | return await this.queryAssetDbReady();
226 | case 'open_asset_external':
227 | return await this.openAssetExternal(args.urlOrUUID);
228 | case 'batch_import_assets':
229 | return await this.batchImportAssets(args);
230 | case 'batch_delete_assets':
231 | return await this.batchDeleteAssets(args.urls);
232 | case 'validate_asset_references':
233 | return await this.validateAssetReferences(args.directory);
234 | case 'get_asset_dependencies':
235 | return await this.getAssetDependencies(args.urlOrUUID, args.direction);
236 | case 'get_unused_assets':
237 | return await this.getUnusedAssets(args.directory, args.excludeDirectories);
238 | case 'compress_textures':
239 | return await this.compressTextures(args.directory, args.format, args.quality);
240 | case 'export_asset_manifest':
241 | return await this.exportAssetManifest(args.directory, args.format, args.includeMetadata);
242 | default:
243 | throw new Error(`Unknown tool: ${toolName}`);
244 | }
245 | }
246 |
247 | private async saveAssetMeta(urlOrUUID: string, content: string): Promise<ToolResponse> {
248 | return new Promise((resolve) => {
249 | Editor.Message.request('asset-db', 'save-asset-meta', urlOrUUID, content).then((result: any) => {
250 | resolve({
251 | success: true,
252 | data: {
253 | uuid: result?.uuid,
254 | url: result?.url,
255 | message: 'Asset meta saved successfully'
256 | }
257 | });
258 | }).catch((err: Error) => {
259 | resolve({ success: false, error: err.message });
260 | });
261 | });
262 | }
263 |
264 | private async generateAvailableUrl(url: string): Promise<ToolResponse> {
265 | return new Promise((resolve) => {
266 | Editor.Message.request('asset-db', 'generate-available-url', url).then((availableUrl: string) => {
267 | resolve({
268 | success: true,
269 | data: {
270 | originalUrl: url,
271 | availableUrl: availableUrl,
272 | message: availableUrl === url ?
273 | 'URL is available' :
274 | 'Generated new available URL'
275 | }
276 | });
277 | }).catch((err: Error) => {
278 | resolve({ success: false, error: err.message });
279 | });
280 | });
281 | }
282 |
283 | private async queryAssetDbReady(): Promise<ToolResponse> {
284 | return new Promise((resolve) => {
285 | Editor.Message.request('asset-db', 'query-ready').then((ready: boolean) => {
286 | resolve({
287 | success: true,
288 | data: {
289 | ready: ready,
290 | message: ready ? 'Asset database is ready' : 'Asset database is not ready'
291 | }
292 | });
293 | }).catch((err: Error) => {
294 | resolve({ success: false, error: err.message });
295 | });
296 | });
297 | }
298 |
299 | private async openAssetExternal(urlOrUUID: string): Promise<ToolResponse> {
300 | return new Promise((resolve) => {
301 | Editor.Message.request('asset-db', 'open-asset', urlOrUUID).then(() => {
302 | resolve({
303 | success: true,
304 | message: 'Asset opened with external program'
305 | });
306 | }).catch((err: Error) => {
307 | resolve({ success: false, error: err.message });
308 | });
309 | });
310 | }
311 |
312 | private async batchImportAssets(args: any): Promise<ToolResponse> {
313 | return new Promise(async (resolve) => {
314 | try {
315 | const fs = require('fs');
316 | const path = require('path');
317 |
318 | if (!fs.existsSync(args.sourceDirectory)) {
319 | resolve({ success: false, error: 'Source directory does not exist' });
320 | return;
321 | }
322 |
323 | const files = this.getFilesFromDirectory(
324 | args.sourceDirectory,
325 | args.fileFilter || [],
326 | args.recursive || false
327 | );
328 |
329 | const importResults: any[] = [];
330 | let successCount = 0;
331 | let errorCount = 0;
332 |
333 | for (const filePath of files) {
334 | try {
335 | const fileName = path.basename(filePath);
336 | const targetPath = `${args.targetDirectory}/${fileName}`;
337 |
338 | const result = await Editor.Message.request('asset-db', 'import-asset',
339 | filePath, targetPath, {
340 | overwrite: args.overwrite || false,
341 | rename: !(args.overwrite || false)
342 | });
343 |
344 | importResults.push({
345 | source: filePath,
346 | target: targetPath,
347 | success: true,
348 | uuid: result?.uuid
349 | });
350 | successCount++;
351 | } catch (err: any) {
352 | importResults.push({
353 | source: filePath,
354 | success: false,
355 | error: err.message
356 | });
357 | errorCount++;
358 | }
359 | }
360 |
361 | resolve({
362 | success: true,
363 | data: {
364 | totalFiles: files.length,
365 | successCount: successCount,
366 | errorCount: errorCount,
367 | results: importResults,
368 | message: `Batch import completed: ${successCount} success, ${errorCount} errors`
369 | }
370 | });
371 | } catch (err: any) {
372 | resolve({ success: false, error: err.message });
373 | }
374 | });
375 | }
376 |
377 | private getFilesFromDirectory(dirPath: string, fileFilter: string[], recursive: boolean): string[] {
378 | const fs = require('fs');
379 | const path = require('path');
380 | const files: string[] = [];
381 |
382 | const items = fs.readdirSync(dirPath);
383 |
384 | for (const item of items) {
385 | const fullPath = path.join(dirPath, item);
386 | const stat = fs.statSync(fullPath);
387 |
388 | if (stat.isFile()) {
389 | if (fileFilter.length === 0 || fileFilter.some(ext => item.toLowerCase().endsWith(ext.toLowerCase()))) {
390 | files.push(fullPath);
391 | }
392 | } else if (stat.isDirectory() && recursive) {
393 | files.push(...this.getFilesFromDirectory(fullPath, fileFilter, recursive));
394 | }
395 | }
396 |
397 | return files;
398 | }
399 |
400 | private async batchDeleteAssets(urls: string[]): Promise<ToolResponse> {
401 | return new Promise(async (resolve) => {
402 | try {
403 | const deleteResults: any[] = [];
404 | let successCount = 0;
405 | let errorCount = 0;
406 |
407 | for (const url of urls) {
408 | try {
409 | await Editor.Message.request('asset-db', 'delete-asset', url);
410 | deleteResults.push({
411 | url: url,
412 | success: true
413 | });
414 | successCount++;
415 | } catch (err: any) {
416 | deleteResults.push({
417 | url: url,
418 | success: false,
419 | error: err.message
420 | });
421 | errorCount++;
422 | }
423 | }
424 |
425 | resolve({
426 | success: true,
427 | data: {
428 | totalAssets: urls.length,
429 | successCount: successCount,
430 | errorCount: errorCount,
431 | results: deleteResults,
432 | message: `Batch delete completed: ${successCount} success, ${errorCount} errors`
433 | }
434 | });
435 | } catch (err: any) {
436 | resolve({ success: false, error: err.message });
437 | }
438 | });
439 | }
440 |
441 | private async validateAssetReferences(directory: string = 'db://assets'): Promise<ToolResponse> {
442 | return new Promise(async (resolve) => {
443 | try {
444 | // Get all assets in directory
445 | const assets = await Editor.Message.request('asset-db', 'query-assets', { pattern: `${directory}/**/*` });
446 |
447 | const brokenReferences: any[] = [];
448 | const validReferences: any[] = [];
449 |
450 | for (const asset of assets) {
451 | try {
452 | const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', asset.url);
453 | if (assetInfo) {
454 | validReferences.push({
455 | url: asset.url,
456 | uuid: asset.uuid,
457 | name: asset.name
458 | });
459 | }
460 | } catch (err) {
461 | brokenReferences.push({
462 | url: asset.url,
463 | uuid: asset.uuid,
464 | name: asset.name,
465 | error: (err as Error).message
466 | });
467 | }
468 | }
469 |
470 | resolve({
471 | success: true,
472 | data: {
473 | directory: directory,
474 | totalAssets: assets.length,
475 | validReferences: validReferences.length,
476 | brokenReferences: brokenReferences.length,
477 | brokenAssets: brokenReferences,
478 | message: `Validation completed: ${brokenReferences.length} broken references found`
479 | }
480 | });
481 | } catch (err: any) {
482 | resolve({ success: false, error: err.message });
483 | }
484 | });
485 | }
486 |
487 | private async getAssetDependencies(urlOrUUID: string, direction: string = 'dependencies'): Promise<ToolResponse> {
488 | return new Promise((resolve) => {
489 | // Note: This would require scene analysis or additional APIs not available in current documentation
490 | resolve({
491 | success: false,
492 | error: 'Asset dependency analysis requires additional APIs not available in current Cocos Creator MCP implementation. Consider using the Editor UI for dependency analysis.'
493 | });
494 | });
495 | }
496 |
497 | private async getUnusedAssets(directory: string = 'db://assets', excludeDirectories: string[] = []): Promise<ToolResponse> {
498 | return new Promise((resolve) => {
499 | // Note: This would require comprehensive project analysis
500 | resolve({
501 | success: false,
502 | error: 'Unused asset detection requires comprehensive project analysis not available in current Cocos Creator MCP implementation. Consider using the Editor UI or third-party tools for unused asset detection.'
503 | });
504 | });
505 | }
506 |
507 | private async compressTextures(directory: string = 'db://assets', format: string = 'auto', quality: number = 0.8): Promise<ToolResponse> {
508 | return new Promise((resolve) => {
509 | // Note: Texture compression would require image processing APIs
510 | resolve({
511 | success: false,
512 | error: 'Texture compression requires image processing capabilities not available in current Cocos Creator MCP implementation. Use the Editor\'s built-in texture compression settings or external tools.'
513 | });
514 | });
515 | }
516 |
517 | private async exportAssetManifest(directory: string = 'db://assets', format: string = 'json', includeMetadata: boolean = true): Promise<ToolResponse> {
518 | return new Promise(async (resolve) => {
519 | try {
520 | const assets = await Editor.Message.request('asset-db', 'query-assets', { pattern: `${directory}/**/*` });
521 |
522 | const manifest: any[] = [];
523 |
524 | for (const asset of assets) {
525 | const manifestEntry: any = {
526 | name: asset.name,
527 | url: asset.url,
528 | uuid: asset.uuid,
529 | type: asset.type,
530 | size: (asset as any).size || 0,
531 | isDirectory: asset.isDirectory || false
532 | };
533 |
534 | if (includeMetadata) {
535 | try {
536 | const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', asset.url);
537 | if (assetInfo && assetInfo.meta) {
538 | manifestEntry.meta = assetInfo.meta;
539 | }
540 | } catch (err) {
541 | // Skip metadata if not available
542 | }
543 | }
544 |
545 | manifest.push(manifestEntry);
546 | }
547 |
548 | let exportData: string;
549 | switch (format) {
550 | case 'json':
551 | exportData = JSON.stringify(manifest, null, 2);
552 | break;
553 | case 'csv':
554 | exportData = this.convertToCSV(manifest);
555 | break;
556 | case 'xml':
557 | exportData = this.convertToXML(manifest);
558 | break;
559 | default:
560 | exportData = JSON.stringify(manifest, null, 2);
561 | }
562 |
563 | resolve({
564 | success: true,
565 | data: {
566 | directory: directory,
567 | format: format,
568 | assetCount: manifest.length,
569 | includeMetadata: includeMetadata,
570 | manifest: exportData,
571 | message: `Asset manifest exported with ${manifest.length} assets`
572 | }
573 | });
574 | } catch (err: any) {
575 | resolve({ success: false, error: err.message });
576 | }
577 | });
578 | }
579 |
580 | private convertToCSV(data: any[]): string {
581 | if (data.length === 0) return '';
582 |
583 | const headers = Object.keys(data[0]);
584 | const csvRows = [headers.join(',')];
585 |
586 | for (const row of data) {
587 | const values = headers.map(header => {
588 | const value = row[header];
589 | return typeof value === 'object' ? JSON.stringify(value) : String(value);
590 | });
591 | csvRows.push(values.join(','));
592 | }
593 |
594 | return csvRows.join('\n');
595 | }
596 |
597 | private convertToXML(data: any[]): string {
598 | let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<assets>\n';
599 |
600 | for (const item of data) {
601 | xml += ' <asset>\n';
602 | for (const [key, value] of Object.entries(item)) {
603 | const xmlValue = typeof value === 'object' ?
604 | JSON.stringify(value) :
605 | String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
606 | xml += ` <${key}>${xmlValue}</${key}>\n`;
607 | }
608 | xml += ' </asset>\n';
609 | }
610 |
611 | xml += '</assets>';
612 | return xml;
613 | }
614 | }
```
--------------------------------------------------------------------------------
/source/tools/scene-advanced-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
2 |
3 | export class SceneAdvancedTools implements ToolExecutor {
4 | getTools(): ToolDefinition[] {
5 | return [
6 | {
7 | name: 'reset_node_property',
8 | description: 'Reset node property to default value',
9 | inputSchema: {
10 | type: 'object',
11 | properties: {
12 | uuid: {
13 | type: 'string',
14 | description: 'Node UUID'
15 | },
16 | path: {
17 | type: 'string',
18 | description: 'Property path (e.g., position, rotation, scale)'
19 | }
20 | },
21 | required: ['uuid', 'path']
22 | }
23 | },
24 | {
25 | name: 'move_array_element',
26 | description: 'Move array element position',
27 | inputSchema: {
28 | type: 'object',
29 | properties: {
30 | uuid: {
31 | type: 'string',
32 | description: 'Node UUID'
33 | },
34 | path: {
35 | type: 'string',
36 | description: 'Array property path (e.g., __comps__)'
37 | },
38 | target: {
39 | type: 'number',
40 | description: 'Target item original index'
41 | },
42 | offset: {
43 | type: 'number',
44 | description: 'Offset amount (positive or negative)'
45 | }
46 | },
47 | required: ['uuid', 'path', 'target', 'offset']
48 | }
49 | },
50 | {
51 | name: 'remove_array_element',
52 | description: 'Remove array element at specific index',
53 | inputSchema: {
54 | type: 'object',
55 | properties: {
56 | uuid: {
57 | type: 'string',
58 | description: 'Node UUID'
59 | },
60 | path: {
61 | type: 'string',
62 | description: 'Array property path'
63 | },
64 | index: {
65 | type: 'number',
66 | description: 'Target item index to remove'
67 | }
68 | },
69 | required: ['uuid', 'path', 'index']
70 | }
71 | },
72 | {
73 | name: 'copy_node',
74 | description: 'Copy node for later paste operation',
75 | inputSchema: {
76 | type: 'object',
77 | properties: {
78 | uuids: {
79 | oneOf: [
80 | { type: 'string' },
81 | { type: 'array', items: { type: 'string' } }
82 | ],
83 | description: 'Node UUID or array of UUIDs to copy'
84 | }
85 | },
86 | required: ['uuids']
87 | }
88 | },
89 | {
90 | name: 'paste_node',
91 | description: 'Paste previously copied nodes',
92 | inputSchema: {
93 | type: 'object',
94 | properties: {
95 | target: {
96 | type: 'string',
97 | description: 'Target parent node UUID'
98 | },
99 | uuids: {
100 | oneOf: [
101 | { type: 'string' },
102 | { type: 'array', items: { type: 'string' } }
103 | ],
104 | description: 'Node UUIDs to paste'
105 | },
106 | keepWorldTransform: {
107 | type: 'boolean',
108 | description: 'Keep world transform coordinates',
109 | default: false
110 | }
111 | },
112 | required: ['target', 'uuids']
113 | }
114 | },
115 | {
116 | name: 'cut_node',
117 | description: 'Cut node (copy + mark for move)',
118 | inputSchema: {
119 | type: 'object',
120 | properties: {
121 | uuids: {
122 | oneOf: [
123 | { type: 'string' },
124 | { type: 'array', items: { type: 'string' } }
125 | ],
126 | description: 'Node UUID or array of UUIDs to cut'
127 | }
128 | },
129 | required: ['uuids']
130 | }
131 | },
132 | {
133 | name: 'reset_node_transform',
134 | description: 'Reset node position, rotation and scale',
135 | inputSchema: {
136 | type: 'object',
137 | properties: {
138 | uuid: {
139 | type: 'string',
140 | description: 'Node UUID'
141 | }
142 | },
143 | required: ['uuid']
144 | }
145 | },
146 | {
147 | name: 'reset_component',
148 | description: 'Reset component to default values',
149 | inputSchema: {
150 | type: 'object',
151 | properties: {
152 | uuid: {
153 | type: 'string',
154 | description: 'Component UUID'
155 | }
156 | },
157 | required: ['uuid']
158 | }
159 | },
160 | {
161 | name: 'restore_prefab',
162 | description: 'Restore prefab instance from asset',
163 | inputSchema: {
164 | type: 'object',
165 | properties: {
166 | nodeUuid: {
167 | type: 'string',
168 | description: 'Node UUID'
169 | },
170 | assetUuid: {
171 | type: 'string',
172 | description: 'Prefab asset UUID'
173 | }
174 | },
175 | required: ['nodeUuid', 'assetUuid']
176 | }
177 | },
178 | {
179 | name: 'execute_component_method',
180 | description: 'Execute method on component',
181 | inputSchema: {
182 | type: 'object',
183 | properties: {
184 | uuid: {
185 | type: 'string',
186 | description: 'Component UUID'
187 | },
188 | name: {
189 | type: 'string',
190 | description: 'Method name'
191 | },
192 | args: {
193 | type: 'array',
194 | description: 'Method arguments',
195 | default: []
196 | }
197 | },
198 | required: ['uuid', 'name']
199 | }
200 | },
201 | {
202 | name: 'execute_scene_script',
203 | description: 'Execute scene script method',
204 | inputSchema: {
205 | type: 'object',
206 | properties: {
207 | name: {
208 | type: 'string',
209 | description: 'Plugin name'
210 | },
211 | method: {
212 | type: 'string',
213 | description: 'Method name'
214 | },
215 | args: {
216 | type: 'array',
217 | description: 'Method arguments',
218 | default: []
219 | }
220 | },
221 | required: ['name', 'method']
222 | }
223 | },
224 | {
225 | name: 'scene_snapshot',
226 | description: 'Create scene state snapshot',
227 | inputSchema: {
228 | type: 'object',
229 | properties: {}
230 | }
231 | },
232 | {
233 | name: 'scene_snapshot_abort',
234 | description: 'Abort scene snapshot creation',
235 | inputSchema: {
236 | type: 'object',
237 | properties: {}
238 | }
239 | },
240 | {
241 | name: 'begin_undo_recording',
242 | description: 'Begin recording undo data',
243 | inputSchema: {
244 | type: 'object',
245 | properties: {
246 | nodeUuid: {
247 | type: 'string',
248 | description: 'Node UUID to record'
249 | }
250 | },
251 | required: ['nodeUuid']
252 | }
253 | },
254 | {
255 | name: 'end_undo_recording',
256 | description: 'End recording undo data',
257 | inputSchema: {
258 | type: 'object',
259 | properties: {
260 | undoId: {
261 | type: 'string',
262 | description: 'Undo recording ID from begin_undo_recording'
263 | }
264 | },
265 | required: ['undoId']
266 | }
267 | },
268 | {
269 | name: 'cancel_undo_recording',
270 | description: 'Cancel undo recording',
271 | inputSchema: {
272 | type: 'object',
273 | properties: {
274 | undoId: {
275 | type: 'string',
276 | description: 'Undo recording ID to cancel'
277 | }
278 | },
279 | required: ['undoId']
280 | }
281 | },
282 | {
283 | name: 'soft_reload_scene',
284 | description: 'Soft reload current scene',
285 | inputSchema: {
286 | type: 'object',
287 | properties: {}
288 | }
289 | },
290 | {
291 | name: 'query_scene_ready',
292 | description: 'Check if scene is ready',
293 | inputSchema: {
294 | type: 'object',
295 | properties: {}
296 | }
297 | },
298 | {
299 | name: 'query_scene_dirty',
300 | description: 'Check if scene has unsaved changes',
301 | inputSchema: {
302 | type: 'object',
303 | properties: {}
304 | }
305 | },
306 | {
307 | name: 'query_scene_classes',
308 | description: 'Query all registered classes',
309 | inputSchema: {
310 | type: 'object',
311 | properties: {
312 | extends: {
313 | type: 'string',
314 | description: 'Filter classes that extend this base class'
315 | }
316 | }
317 | }
318 | },
319 | {
320 | name: 'query_scene_components',
321 | description: 'Query available scene components',
322 | inputSchema: {
323 | type: 'object',
324 | properties: {}
325 | }
326 | },
327 | {
328 | name: 'query_component_has_script',
329 | description: 'Check if component has script',
330 | inputSchema: {
331 | type: 'object',
332 | properties: {
333 | className: {
334 | type: 'string',
335 | description: 'Script class name to check'
336 | }
337 | },
338 | required: ['className']
339 | }
340 | },
341 | {
342 | name: 'query_nodes_by_asset_uuid',
343 | description: 'Find nodes that use specific asset UUID',
344 | inputSchema: {
345 | type: 'object',
346 | properties: {
347 | assetUuid: {
348 | type: 'string',
349 | description: 'Asset UUID to search for'
350 | }
351 | },
352 | required: ['assetUuid']
353 | }
354 | }
355 | ];
356 | }
357 |
358 | async execute(toolName: string, args: any): Promise<ToolResponse> {
359 | switch (toolName) {
360 | case 'reset_node_property':
361 | return await this.resetNodeProperty(args.uuid, args.path);
362 | case 'move_array_element':
363 | return await this.moveArrayElement(args.uuid, args.path, args.target, args.offset);
364 | case 'remove_array_element':
365 | return await this.removeArrayElement(args.uuid, args.path, args.index);
366 | case 'copy_node':
367 | return await this.copyNode(args.uuids);
368 | case 'paste_node':
369 | return await this.pasteNode(args.target, args.uuids, args.keepWorldTransform);
370 | case 'cut_node':
371 | return await this.cutNode(args.uuids);
372 | case 'reset_node_transform':
373 | return await this.resetNodeTransform(args.uuid);
374 | case 'reset_component':
375 | return await this.resetComponent(args.uuid);
376 | case 'restore_prefab':
377 | return await this.restorePrefab(args.nodeUuid, args.assetUuid);
378 | case 'execute_component_method':
379 | return await this.executeComponentMethod(args.uuid, args.name, args.args);
380 | case 'execute_scene_script':
381 | return await this.executeSceneScript(args.name, args.method, args.args);
382 | case 'scene_snapshot':
383 | return await this.sceneSnapshot();
384 | case 'scene_snapshot_abort':
385 | return await this.sceneSnapshotAbort();
386 | case 'begin_undo_recording':
387 | return await this.beginUndoRecording(args.nodeUuid);
388 | case 'end_undo_recording':
389 | return await this.endUndoRecording(args.undoId);
390 | case 'cancel_undo_recording':
391 | return await this.cancelUndoRecording(args.undoId);
392 | case 'soft_reload_scene':
393 | return await this.softReloadScene();
394 | case 'query_scene_ready':
395 | return await this.querySceneReady();
396 | case 'query_scene_dirty':
397 | return await this.querySceneDirty();
398 | case 'query_scene_classes':
399 | return await this.querySceneClasses(args.extends);
400 | case 'query_scene_components':
401 | return await this.querySceneComponents();
402 | case 'query_component_has_script':
403 | return await this.queryComponentHasScript(args.className);
404 | case 'query_nodes_by_asset_uuid':
405 | return await this.queryNodesByAssetUuid(args.assetUuid);
406 | default:
407 | throw new Error(`Unknown tool: ${toolName}`);
408 | }
409 | }
410 |
411 | private async resetNodeProperty(uuid: string, path: string): Promise<ToolResponse> {
412 | return new Promise((resolve) => {
413 | Editor.Message.request('scene', 'reset-property', {
414 | uuid,
415 | path,
416 | dump: { value: null }
417 | }).then(() => {
418 | resolve({
419 | success: true,
420 | message: `Property '${path}' reset to default value`
421 | });
422 | }).catch((err: Error) => {
423 | resolve({ success: false, error: err.message });
424 | });
425 | });
426 | }
427 |
428 | private async moveArrayElement(uuid: string, path: string, target: number, offset: number): Promise<ToolResponse> {
429 | return new Promise((resolve) => {
430 | Editor.Message.request('scene', 'move-array-element', {
431 | uuid,
432 | path,
433 | target,
434 | offset
435 | }).then(() => {
436 | resolve({
437 | success: true,
438 | message: `Array element at index ${target} moved by ${offset}`
439 | });
440 | }).catch((err: Error) => {
441 | resolve({ success: false, error: err.message });
442 | });
443 | });
444 | }
445 |
446 | private async removeArrayElement(uuid: string, path: string, index: number): Promise<ToolResponse> {
447 | return new Promise((resolve) => {
448 | Editor.Message.request('scene', 'remove-array-element', {
449 | uuid,
450 | path,
451 | index
452 | }).then(() => {
453 | resolve({
454 | success: true,
455 | message: `Array element at index ${index} removed`
456 | });
457 | }).catch((err: Error) => {
458 | resolve({ success: false, error: err.message });
459 | });
460 | });
461 | }
462 |
463 | private async copyNode(uuids: string | string[]): Promise<ToolResponse> {
464 | return new Promise((resolve) => {
465 | Editor.Message.request('scene', 'copy-node', uuids).then((result: string | string[]) => {
466 | resolve({
467 | success: true,
468 | data: {
469 | copiedUuids: result,
470 | message: 'Node(s) copied successfully'
471 | }
472 | });
473 | }).catch((err: Error) => {
474 | resolve({ success: false, error: err.message });
475 | });
476 | });
477 | }
478 |
479 | private async pasteNode(target: string, uuids: string | string[], keepWorldTransform: boolean = false): Promise<ToolResponse> {
480 | return new Promise((resolve) => {
481 | Editor.Message.request('scene', 'paste-node', {
482 | target,
483 | uuids,
484 | keepWorldTransform
485 | }).then((result: string | string[]) => {
486 | resolve({
487 | success: true,
488 | data: {
489 | newUuids: result,
490 | message: 'Node(s) pasted successfully'
491 | }
492 | });
493 | }).catch((err: Error) => {
494 | resolve({ success: false, error: err.message });
495 | });
496 | });
497 | }
498 |
499 | private async cutNode(uuids: string | string[]): Promise<ToolResponse> {
500 | return new Promise((resolve) => {
501 | Editor.Message.request('scene', 'cut-node', uuids).then((result: any) => {
502 | resolve({
503 | success: true,
504 | data: {
505 | cutUuids: result,
506 | message: 'Node(s) cut successfully'
507 | }
508 | });
509 | }).catch((err: Error) => {
510 | resolve({ success: false, error: err.message });
511 | });
512 | });
513 | }
514 |
515 | private async resetNodeTransform(uuid: string): Promise<ToolResponse> {
516 | return new Promise((resolve) => {
517 | Editor.Message.request('scene', 'reset-node', { uuid }).then(() => {
518 | resolve({
519 | success: true,
520 | message: 'Node transform reset to default'
521 | });
522 | }).catch((err: Error) => {
523 | resolve({ success: false, error: err.message });
524 | });
525 | });
526 | }
527 |
528 | private async resetComponent(uuid: string): Promise<ToolResponse> {
529 | return new Promise((resolve) => {
530 | Editor.Message.request('scene', 'reset-component', { uuid }).then(() => {
531 | resolve({
532 | success: true,
533 | message: 'Component reset to default values'
534 | });
535 | }).catch((err: Error) => {
536 | resolve({ success: false, error: err.message });
537 | });
538 | });
539 | }
540 |
541 | private async restorePrefab(nodeUuid: string, assetUuid: string): Promise<ToolResponse> {
542 | return new Promise((resolve) => {
543 | (Editor.Message.request as any)('scene', 'restore-prefab', nodeUuid, assetUuid).then(() => {
544 | resolve({
545 | success: true,
546 | message: 'Prefab restored successfully'
547 | });
548 | }).catch((err: Error) => {
549 | resolve({ success: false, error: err.message });
550 | });
551 | });
552 | }
553 |
554 | private async executeComponentMethod(uuid: string, name: string, args: any[] = []): Promise<ToolResponse> {
555 | return new Promise((resolve) => {
556 | Editor.Message.request('scene', 'execute-component-method', {
557 | uuid,
558 | name,
559 | args
560 | }).then((result: any) => {
561 | resolve({
562 | success: true,
563 | data: {
564 | result: result,
565 | message: `Method '${name}' executed successfully`
566 | }
567 | });
568 | }).catch((err: Error) => {
569 | resolve({ success: false, error: err.message });
570 | });
571 | });
572 | }
573 |
574 | private async executeSceneScript(name: string, method: string, args: any[] = []): Promise<ToolResponse> {
575 | return new Promise((resolve) => {
576 | Editor.Message.request('scene', 'execute-scene-script', {
577 | name,
578 | method,
579 | args
580 | }).then((result: any) => {
581 | resolve({
582 | success: true,
583 | data: result
584 | });
585 | }).catch((err: Error) => {
586 | resolve({ success: false, error: err.message });
587 | });
588 | });
589 | }
590 |
591 | private async sceneSnapshot(): Promise<ToolResponse> {
592 | return new Promise((resolve) => {
593 | Editor.Message.request('scene', 'snapshot').then(() => {
594 | resolve({
595 | success: true,
596 | message: 'Scene snapshot created'
597 | });
598 | }).catch((err: Error) => {
599 | resolve({ success: false, error: err.message });
600 | });
601 | });
602 | }
603 |
604 | private async sceneSnapshotAbort(): Promise<ToolResponse> {
605 | return new Promise((resolve) => {
606 | Editor.Message.request('scene', 'snapshot-abort').then(() => {
607 | resolve({
608 | success: true,
609 | message: 'Scene snapshot aborted'
610 | });
611 | }).catch((err: Error) => {
612 | resolve({ success: false, error: err.message });
613 | });
614 | });
615 | }
616 |
617 | private async beginUndoRecording(nodeUuid: string): Promise<ToolResponse> {
618 | return new Promise((resolve) => {
619 | Editor.Message.request('scene', 'begin-recording', nodeUuid).then((undoId: string) => {
620 | resolve({
621 | success: true,
622 | data: {
623 | undoId: undoId,
624 | message: 'Undo recording started'
625 | }
626 | });
627 | }).catch((err: Error) => {
628 | resolve({ success: false, error: err.message });
629 | });
630 | });
631 | }
632 |
633 | private async endUndoRecording(undoId: string): Promise<ToolResponse> {
634 | return new Promise((resolve) => {
635 | Editor.Message.request('scene', 'end-recording', undoId).then(() => {
636 | resolve({
637 | success: true,
638 | message: 'Undo recording ended'
639 | });
640 | }).catch((err: Error) => {
641 | resolve({ success: false, error: err.message });
642 | });
643 | });
644 | }
645 |
646 | private async cancelUndoRecording(undoId: string): Promise<ToolResponse> {
647 | return new Promise((resolve) => {
648 | Editor.Message.request('scene', 'cancel-recording', undoId).then(() => {
649 | resolve({
650 | success: true,
651 | message: 'Undo recording cancelled'
652 | });
653 | }).catch((err: Error) => {
654 | resolve({ success: false, error: err.message });
655 | });
656 | });
657 | }
658 |
659 | private async softReloadScene(): Promise<ToolResponse> {
660 | return new Promise((resolve) => {
661 | Editor.Message.request('scene', 'soft-reload').then(() => {
662 | resolve({
663 | success: true,
664 | message: 'Scene soft reloaded successfully'
665 | });
666 | }).catch((err: Error) => {
667 | resolve({ success: false, error: err.message });
668 | });
669 | });
670 | }
671 |
672 | private async querySceneReady(): Promise<ToolResponse> {
673 | return new Promise((resolve) => {
674 | Editor.Message.request('scene', 'query-is-ready').then((ready: boolean) => {
675 | resolve({
676 | success: true,
677 | data: {
678 | ready: ready,
679 | message: ready ? 'Scene is ready' : 'Scene is not ready'
680 | }
681 | });
682 | }).catch((err: Error) => {
683 | resolve({ success: false, error: err.message });
684 | });
685 | });
686 | }
687 |
688 | private async querySceneDirty(): Promise<ToolResponse> {
689 | return new Promise((resolve) => {
690 | Editor.Message.request('scene', 'query-dirty').then((dirty: boolean) => {
691 | resolve({
692 | success: true,
693 | data: {
694 | dirty: dirty,
695 | message: dirty ? 'Scene has unsaved changes' : 'Scene is clean'
696 | }
697 | });
698 | }).catch((err: Error) => {
699 | resolve({ success: false, error: err.message });
700 | });
701 | });
702 | }
703 |
704 | private async querySceneClasses(extendsClass?: string): Promise<ToolResponse> {
705 | return new Promise((resolve) => {
706 | const options: any = {};
707 | if (extendsClass) {
708 | options.extends = extendsClass;
709 | }
710 |
711 | Editor.Message.request('scene', 'query-classes', options).then((classes: any[]) => {
712 | resolve({
713 | success: true,
714 | data: {
715 | classes: classes,
716 | count: classes.length,
717 | extendsFilter: extendsClass
718 | }
719 | });
720 | }).catch((err: Error) => {
721 | resolve({ success: false, error: err.message });
722 | });
723 | });
724 | }
725 |
726 | private async querySceneComponents(): Promise<ToolResponse> {
727 | return new Promise((resolve) => {
728 | Editor.Message.request('scene', 'query-components').then((components: any[]) => {
729 | resolve({
730 | success: true,
731 | data: {
732 | components: components,
733 | count: components.length
734 | }
735 | });
736 | }).catch((err: Error) => {
737 | resolve({ success: false, error: err.message });
738 | });
739 | });
740 | }
741 |
742 | private async queryComponentHasScript(className: string): Promise<ToolResponse> {
743 | return new Promise((resolve) => {
744 | Editor.Message.request('scene', 'query-component-has-script', className).then((hasScript: boolean) => {
745 | resolve({
746 | success: true,
747 | data: {
748 | className: className,
749 | hasScript: hasScript,
750 | message: hasScript ? `Component '${className}' has script` : `Component '${className}' does not have script`
751 | }
752 | });
753 | }).catch((err: Error) => {
754 | resolve({ success: false, error: err.message });
755 | });
756 | });
757 | }
758 |
759 | private async queryNodesByAssetUuid(assetUuid: string): Promise<ToolResponse> {
760 | return new Promise((resolve) => {
761 | Editor.Message.request('scene', 'query-nodes-by-asset-uuid', assetUuid).then((nodeUuids: string[]) => {
762 | resolve({
763 | success: true,
764 | data: {
765 | assetUuid: assetUuid,
766 | nodeUuids: nodeUuids,
767 | count: nodeUuids.length,
768 | message: `Found ${nodeUuids.length} nodes using asset`
769 | }
770 | });
771 | }).catch((err: Error) => {
772 | resolve({ success: false, error: err.message });
773 | });
774 | });
775 | }
776 | }
```