初次提交
This commit is contained in:
119
source/main.ts
Executable file
119
source/main.ts
Executable file
@@ -0,0 +1,119 @@
|
||||
import { MCPServer } from './mcp-server';
|
||||
import { readSettings, saveSettings } from './settings';
|
||||
import { MCPServerSettings } from './types';
|
||||
|
||||
let mcpServer: MCPServer | null = null;
|
||||
|
||||
/**
|
||||
* @en Registration method for the main process of Extension
|
||||
* @zh 为扩展的主进程的注册方法
|
||||
*/
|
||||
export const methods: { [key: string]: (...any: any) => any } = {
|
||||
/**
|
||||
* @en Open the MCP server panel
|
||||
* @zh 打开 MCP 服务器面板
|
||||
*/
|
||||
openPanel() {
|
||||
Editor.Panel.open('cocos-mcp-server');
|
||||
},
|
||||
|
||||
/**
|
||||
* @en Start the MCP server
|
||||
* @zh 启动 MCP 服务器
|
||||
*/
|
||||
async startServer() {
|
||||
if (mcpServer) {
|
||||
await mcpServer.start();
|
||||
} else {
|
||||
console.warn('[MCP插件] mcpServer 未初始化');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @en Stop the MCP server
|
||||
* @zh 停止 MCP 服务器
|
||||
*/
|
||||
async stopServer() {
|
||||
if (mcpServer) {
|
||||
mcpServer.stop();
|
||||
} else {
|
||||
console.warn('[MCP插件] mcpServer 未初始化');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @en Get server status
|
||||
* @zh 获取服务器状态
|
||||
*/
|
||||
getServerStatus() {
|
||||
return mcpServer ? mcpServer.getStatus() : { running: false, port: 0, clients: 0 };
|
||||
},
|
||||
|
||||
/**
|
||||
* @en Update server settings
|
||||
* @zh 更新服务器设置
|
||||
*/
|
||||
updateSettings(settings: MCPServerSettings) {
|
||||
saveSettings(settings);
|
||||
if (mcpServer) {
|
||||
mcpServer.stop();
|
||||
mcpServer = new MCPServer(settings);
|
||||
mcpServer.start();
|
||||
} else {
|
||||
mcpServer = new MCPServer(settings);
|
||||
mcpServer.start();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @en Get tools list
|
||||
* @zh 获取工具列表
|
||||
*/
|
||||
getToolsList() {
|
||||
return mcpServer ? mcpServer.getAvailableTools() : [];
|
||||
},
|
||||
/**
|
||||
* @en Get server settings
|
||||
* @zh 获取服务器设置
|
||||
*/
|
||||
getServerSettings() {
|
||||
return mcpServer ? mcpServer.getSettings() : readSettings();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @en Method Triggered on Extension Startup
|
||||
* @zh 扩展启动时触发的方法
|
||||
*/
|
||||
export function load() {
|
||||
console.log('[MCP Plugin] Loading MCP server plugin...');
|
||||
try {
|
||||
const settings = readSettings();
|
||||
console.log('[MCP Plugin] Settings loaded:', settings);
|
||||
mcpServer = new MCPServer(settings);
|
||||
|
||||
// 如果设置了自动启动,则启动服务器
|
||||
if (settings.autoStart) {
|
||||
console.log('[MCP Plugin] Auto-starting MCP server...');
|
||||
mcpServer.start().catch(error => {
|
||||
console.error('[MCP Plugin] Failed to auto-start server:', error);
|
||||
});
|
||||
} else {
|
||||
console.log('[MCP Plugin] MCP server created but not started (autoStart=false)');
|
||||
console.log('[MCP Plugin] Use the MCP panel or call startServer() to start the server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MCP Plugin] Failed to load MCP server:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @en Method triggered when uninstalling the extension
|
||||
* @zh 卸载扩展时触发的方法
|
||||
*/
|
||||
export function unload() {
|
||||
if (mcpServer) {
|
||||
mcpServer.stop();
|
||||
mcpServer = null;
|
||||
}
|
||||
}
|
||||
257
source/mcp-server.ts
Normal file
257
source/mcp-server.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { MCPServerSettings, ServerStatus, MCPClient, ToolDefinition } from './types';
|
||||
import { SceneTools } from './tools/scene-tools';
|
||||
import { NodeTools } from './tools/node-tools';
|
||||
import { ComponentTools } from './tools/component-tools';
|
||||
import { PrefabTools } from './tools/prefab-tools';
|
||||
import { ProjectTools } from './tools/project-tools';
|
||||
import { DebugTools } from './tools/debug-tools';
|
||||
import { PreferencesTools } from './tools/preferences-tools';
|
||||
import { ServerTools } from './tools/server-tools';
|
||||
import { BroadcastTools } from './tools/broadcast-tools';
|
||||
|
||||
export class MCPServer {
|
||||
private settings: MCPServerSettings;
|
||||
private httpServer: http.Server | null = null;
|
||||
private clients: Map<string, MCPClient> = new Map();
|
||||
private tools: Record<string, any> = {};
|
||||
private toolsList: ToolDefinition[] = [];
|
||||
|
||||
constructor(settings: MCPServerSettings) {
|
||||
this.settings = settings;
|
||||
this.initializeTools();
|
||||
}
|
||||
|
||||
private initializeTools(): void {
|
||||
try {
|
||||
console.log('[MCPServer] Initializing tools...');
|
||||
this.tools.scene = new SceneTools();
|
||||
this.tools.node = new NodeTools();
|
||||
this.tools.component = new ComponentTools();
|
||||
this.tools.prefab = new PrefabTools();
|
||||
this.tools.project = new ProjectTools();
|
||||
this.tools.debug = new DebugTools();
|
||||
this.tools.preferences = new PreferencesTools();
|
||||
this.tools.server = new ServerTools();
|
||||
this.tools.broadcast = new BroadcastTools();
|
||||
console.log('[MCPServer] Tools initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('[MCPServer] Error initializing tools:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
if (this.httpServer) {
|
||||
console.log('[MCPServer] Server is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[MCPServer] Starting HTTP server on port ${this.settings.port}...`);
|
||||
this.httpServer = http.createServer(this.handleHttpRequest.bind(this));
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.httpServer!.listen(this.settings.port, '127.0.0.1', () => {
|
||||
console.log(`[MCPServer] ✅ HTTP server started successfully on http://127.0.0.1:${this.settings.port}`);
|
||||
console.log(`[MCPServer] Health check: http://127.0.0.1:${this.settings.port}/health`);
|
||||
console.log(`[MCPServer] MCP endpoint: http://127.0.0.1:${this.settings.port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
this.httpServer!.on('error', (err: any) => {
|
||||
console.error('[MCPServer] ❌ Failed to start server:', err);
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`[MCPServer] Port ${this.settings.port} is already in use. Please change the port in settings.`);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
this.setupTools();
|
||||
console.log('[MCPServer] 🚀 MCP Server is ready for connections');
|
||||
} catch (error) {
|
||||
console.error('[MCPServer] ❌ Failed to start server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private setupTools(): void {
|
||||
this.toolsList = [];
|
||||
|
||||
for (const [category, toolSet] of Object.entries(this.tools)) {
|
||||
const tools = toolSet.getTools();
|
||||
for (const tool of tools) {
|
||||
this.toolsList.push({
|
||||
name: `${category}_${tool.name}`,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async executeToolCall(toolName: string, args: any): Promise<any> {
|
||||
const parts = toolName.split('_');
|
||||
const category = parts[0];
|
||||
const toolMethodName = parts.slice(1).join('_');
|
||||
|
||||
if (this.tools[category]) {
|
||||
return await this.tools[category].execute(toolMethodName, args);
|
||||
}
|
||||
|
||||
throw new Error(`Tool ${toolName} not found`);
|
||||
}
|
||||
|
||||
public getClients(): MCPClient[] {
|
||||
return Array.from(this.clients.values());
|
||||
}
|
||||
public getAvailableTools(): ToolDefinition[] {
|
||||
return this.toolsList;
|
||||
}
|
||||
|
||||
public getSettings(): MCPServerSettings {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
private async handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const parsedUrl = url.parse(req.url || '', true);
|
||||
const pathname = parsedUrl.pathname;
|
||||
|
||||
// Set CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (pathname === '/mcp' && req.method === 'POST') {
|
||||
await this.handleMCPRequest(req, res);
|
||||
} else if (pathname === '/health' && req.method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({ status: 'ok', tools: this.toolsList.length }));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ error: 'Not found' }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('HTTP request error:', error);
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMCPRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
let body = '';
|
||||
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const message = JSON.parse(body);
|
||||
const response = await this.handleMessage(message);
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: {
|
||||
code: -32700,
|
||||
message: 'Parse error'
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMessage(message: any): Promise<any> {
|
||||
const { id, method, params } = message;
|
||||
|
||||
try {
|
||||
let result: any;
|
||||
|
||||
switch (method) {
|
||||
case 'tools/list':
|
||||
result = { tools: this.getAvailableTools() };
|
||||
break;
|
||||
case 'tools/call':
|
||||
const { name, arguments: args } = params;
|
||||
const toolResult = await this.executeToolCall(name, args);
|
||||
result = { content: [{ type: 'text', text: JSON.stringify(toolResult) }] };
|
||||
break;
|
||||
case 'initialize':
|
||||
// MCP initialization
|
||||
result = {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {}
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'cocos-mcp-server',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
}
|
||||
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: {
|
||||
code: -32603,
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.httpServer) {
|
||||
this.httpServer.close();
|
||||
this.httpServer = null;
|
||||
console.log('[MCPServer] HTTP server stopped');
|
||||
}
|
||||
|
||||
this.clients.clear();
|
||||
}
|
||||
|
||||
public getStatus(): ServerStatus {
|
||||
return {
|
||||
running: !!this.httpServer,
|
||||
port: this.settings.port,
|
||||
clients: 0 // HTTP is stateless, no persistent clients
|
||||
};
|
||||
}
|
||||
|
||||
public updateSettings(settings: MCPServerSettings) {
|
||||
this.settings = settings;
|
||||
if (this.httpServer) {
|
||||
this.stop();
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP transport doesn't need persistent connections
|
||||
// MCP over HTTP uses request-response pattern
|
||||
229
source/panels/default/index.ts
Normal file
229
source/panels/default/index.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { readSettings } from '../../settings';
|
||||
import { readFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* @zh 如果希望兼容 3.3 之前的版本可以使用下方的代码
|
||||
* @en You can add the code below if you want compatibility with versions prior to 3.3
|
||||
*/
|
||||
// Editor.Panel.define = Editor.Panel.define || function(options: any) { return options }
|
||||
|
||||
module.exports = Editor.Panel.define({
|
||||
listeners: {
|
||||
show() { console.log('MCP Server panel shown'); },
|
||||
hide() { console.log('MCP Server panel hidden'); }
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'),
|
||||
style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
|
||||
$: {
|
||||
panelTitle: '#panelTitle',
|
||||
serverStatusLabel: '#serverStatusLabel',
|
||||
serverStatusLabelProp: '#serverStatusLabelProp',
|
||||
serverStatusValue: '#serverStatusValue',
|
||||
connectedLabel: '#connectedLabel',
|
||||
connectedClients: '#connectedClients',
|
||||
toggleServerBtn: '#toggleServerBtn',
|
||||
settingsLabel: '#settingsLabel',
|
||||
portLabel: '#portLabel',
|
||||
autoStartLabel: '#autoStartLabel',
|
||||
debugLogLabel: '#debugLogLabel',
|
||||
maxConnectionsLabel: '#maxConnectionsLabel',
|
||||
connectionInfoLabel: '#connectionInfoLabel',
|
||||
httpUrlLabel: '#httpUrlLabel',
|
||||
httpUrlInput: '#httpUrlInput',
|
||||
copyBtn: '#copyBtn',
|
||||
saveSettingsBtn: '#saveSettingsBtn',
|
||||
// 新增输入控件id
|
||||
portInput: '#portInput',
|
||||
maxConnInput: '#maxConnInput',
|
||||
autoStartInput: '#autoStartInput',
|
||||
debugLogInput: '#debugLogInput',
|
||||
},
|
||||
methods: {
|
||||
async updateServerStatus(this: any) {
|
||||
try {
|
||||
const status = await Editor.Message.request('cocos-mcp-server', 'get-server-status');
|
||||
this.serverRunning = status.running;
|
||||
this.connectedClients = status.clients || 0;
|
||||
this.serverStatus = this.serverRunning ?
|
||||
Editor.I18n.t('cocos-mcp-server.connected') :
|
||||
Editor.I18n.t('cocos-mcp-server.disconnected');
|
||||
this.statusClass = this.serverRunning ? 'running' : 'stopped';
|
||||
this.buttonText = this.serverRunning ?
|
||||
Editor.I18n.t('cocos-mcp-server.stop_server') :
|
||||
Editor.I18n.t('cocos-mcp-server.start_server');
|
||||
// 刷新UI
|
||||
this.$.serverStatusValue.innerText = this.serverStatus;
|
||||
this.$.connectedClients.innerText = this.connectedClients;
|
||||
this.$.toggleServerBtn.innerText = this.buttonText;
|
||||
if (this.serverRunning) {
|
||||
this.httpUrl = `http://localhost:${this.settings.port}/mcp`;
|
||||
this.$.httpUrlInput.value = this.httpUrl;
|
||||
} else {
|
||||
this.httpUrl = '';
|
||||
this.$.httpUrlInput.value = '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update server status:', err);
|
||||
}
|
||||
},
|
||||
|
||||
async toggleServer(this: any) {
|
||||
this.isProcessing = true;
|
||||
try {
|
||||
if (this.serverRunning) {
|
||||
await this.stopServer();
|
||||
} else {
|
||||
await this.startServer();
|
||||
}
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
},
|
||||
|
||||
async startServer(this: any) {
|
||||
try {
|
||||
await Editor.Message.request('cocos-mcp-server', 'start-server');
|
||||
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.server_started'), {
|
||||
detail: Editor.I18n.t('cocos-mcp-server.server_running').replace('{0}', this.settings.port.toString())
|
||||
});
|
||||
await this.updateServerStatus();
|
||||
} catch (err: any) {
|
||||
Editor.Dialog.error(Editor.I18n.t('cocos-mcp-server.failed_to_start'), err.message);
|
||||
}
|
||||
},
|
||||
|
||||
async stopServer(this: any) {
|
||||
try {
|
||||
await Editor.Message.request('cocos-mcp-server', 'stop-server');
|
||||
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.server_stopped_msg'), {
|
||||
detail: Editor.I18n.t('cocos-mcp-server.server_stopped')
|
||||
});
|
||||
await this.updateServerStatus();
|
||||
} catch (err: any) {
|
||||
Editor.Dialog.error(Editor.I18n.t('cocos-mcp-server.failed_to_stop'), err.message);
|
||||
}
|
||||
},
|
||||
|
||||
async saveSettings(this: any) {
|
||||
try {
|
||||
// 直接用 this.$ 获取所有输入控件的当前值
|
||||
const port = this.$.portInput ? Number((this.$.portInput as any).value) : 3000;
|
||||
const maxConnections = this.$.maxConnInput ? Number((this.$.maxConnInput as any).value) : 10;
|
||||
const autoStart = this.$.autoStartInput ? !!(this.$.autoStartInput as any).checked : false;
|
||||
const enableDebugLog = this.$.debugLogInput ? !!(this.$.debugLogInput as any).checked : false;
|
||||
// 组装 settings
|
||||
const settings = {
|
||||
...this.settings,
|
||||
port,
|
||||
maxConnections,
|
||||
autoStart,
|
||||
enableDebugLog,
|
||||
};
|
||||
await Editor.Message.request('cocos-mcp-server', 'update-settings', settings);
|
||||
// 重新拉取设置
|
||||
const newSettings = await Editor.Message.request('cocos-mcp-server', 'get-server-settings');
|
||||
this.settings = newSettings;
|
||||
this.originalSettings = JSON.stringify(newSettings);
|
||||
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.settings_saved'));
|
||||
} catch (err: any) {
|
||||
Editor.Dialog.error(Editor.I18n.t('cocos-mcp-server.failed_to_save'), err.message);
|
||||
}
|
||||
},
|
||||
|
||||
copyUrl(this: any) {
|
||||
Editor.Clipboard.write('text', this.httpUrl);
|
||||
Editor.Dialog.info(Editor.I18n.t('cocos-mcp-server.url_copied'));
|
||||
},
|
||||
|
||||
settingsChanged(this: any) {
|
||||
return JSON.stringify(this.settings) !== this.originalSettings;
|
||||
},
|
||||
bindSettingsEvents(this: any) {
|
||||
// 端口输入框
|
||||
const portInput = document.querySelectorAll('ui-num-input[slot="content"]')[0];
|
||||
if (portInput) {
|
||||
portInput.addEventListener('change', (e: any) => {
|
||||
this.settings.port = Number(e.detail.value);
|
||||
});
|
||||
}
|
||||
// 最大连接数
|
||||
const maxConnInput = document.querySelectorAll('ui-num-input[slot="content"]')[1];
|
||||
if (maxConnInput) {
|
||||
maxConnInput.addEventListener('change', (e: any) => {
|
||||
this.settings.maxConnections = Number(e.detail.value);
|
||||
});
|
||||
}
|
||||
// 复选框
|
||||
const checkboxes = document.querySelectorAll('ui-checkbox[slot="content"]');
|
||||
if (checkboxes && checkboxes.length >= 2) {
|
||||
checkboxes[0].addEventListener('change', (e: any) => {
|
||||
this.settings.autoStart = !!e.detail.value;
|
||||
});
|
||||
checkboxes[1].addEventListener('change', (e: any) => {
|
||||
this.settings.enableDebugLog = !!e.detail.value;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
ready() {
|
||||
Editor.Message.request('cocos-mcp-server', 'get-server-settings').then((settings) => {
|
||||
this.settings = settings;
|
||||
this.originalSettings = JSON.stringify(settings);
|
||||
// 本地化label赋值
|
||||
this.$.panelTitle.innerText = Editor.I18n.t('cocos-mcp-server.panel_title');
|
||||
this.$.serverStatusLabel.innerText = Editor.I18n.t('cocos-mcp-server.server_status');
|
||||
this.$.serverStatusLabelProp.innerText = Editor.I18n.t('cocos-mcp-server.server_status');
|
||||
this.$.connectedLabel.innerText = Editor.I18n.t('cocos-mcp-server.connected');
|
||||
this.$.settingsLabel.innerText = Editor.I18n.t('cocos-mcp-server.settings');
|
||||
this.$.portLabel.innerText = Editor.I18n.t('cocos-mcp-server.port');
|
||||
this.$.autoStartLabel.innerText = Editor.I18n.t('cocos-mcp-server.auto_start');
|
||||
this.$.debugLogLabel.innerText = Editor.I18n.t('cocos-mcp-server.debug_log');
|
||||
this.$.maxConnectionsLabel.innerText = Editor.I18n.t('cocos-mcp-server.max_connections');
|
||||
this.$.connectionInfoLabel.innerText = Editor.I18n.t('cocos-mcp-server.connection_info');
|
||||
this.$.httpUrlLabel.innerText = Editor.I18n.t('cocos-mcp-server.http_url');
|
||||
this.$.copyBtn.innerText = Editor.I18n.t('cocos-mcp-server.copy');
|
||||
this.$.saveSettingsBtn.innerText = Editor.I18n.t('cocos-mcp-server.save_settings');
|
||||
// 动态内容初始化
|
||||
this.$.serverStatusValue.innerText = '';
|
||||
this.$.connectedClients.innerText = '';
|
||||
this.$.toggleServerBtn.innerText = '';
|
||||
this.$.httpUrlInput.value = '';
|
||||
// 绑定按钮事件
|
||||
this.$.toggleServerBtn.addEventListener('confirm', this.toggleServer.bind(this));
|
||||
this.$.saveSettingsBtn.addEventListener('confirm', this.saveSettings.bind(this));
|
||||
this.$.copyBtn.addEventListener('confirm', this.copyUrl.bind(this));
|
||||
// 延迟绑定事件,确保 UI 组件已渲染
|
||||
setTimeout(() => {
|
||||
this.bindSettingsEvents();
|
||||
}, 100);
|
||||
// Set up periodic status updates
|
||||
(this as any).statusInterval = setInterval(() => {
|
||||
(this as any).updateServerStatus();
|
||||
}, 2000);
|
||||
// 不再自动启动服务器,用户点击才启动
|
||||
(this as any).updateServerStatus();
|
||||
});
|
||||
},
|
||||
beforeClose() {
|
||||
if ((this as any).statusInterval) {
|
||||
clearInterval((this as any).statusInterval);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
// Panel close cleanup
|
||||
},
|
||||
|
||||
// Direct properties for data access
|
||||
serverRunning: false,
|
||||
connectedClients: 0,
|
||||
serverStatus: '',
|
||||
statusClass: 'stopped',
|
||||
buttonText: '',
|
||||
isProcessing: false,
|
||||
settings: {},
|
||||
httpUrl: '',
|
||||
statusInterval: null as any,
|
||||
originalSettings: ''
|
||||
} as any);
|
||||
435
source/scene.ts
Normal file
435
source/scene.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
import { join } from 'path';
|
||||
module.paths.push(join(Editor.App.path, 'node_modules'));
|
||||
|
||||
export const methods: { [key: string]: (...any: any) => any } = {
|
||||
/**
|
||||
* Create a new scene
|
||||
*/
|
||||
createNewScene() {
|
||||
try {
|
||||
const { director, Scene } = require('cc');
|
||||
const scene = new Scene();
|
||||
scene.name = 'New Scene';
|
||||
director.runScene(scene);
|
||||
return { success: true, message: 'New scene created successfully' };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add component to a node
|
||||
*/
|
||||
addComponentToNode(nodeUuid: string, componentType: string) {
|
||||
try {
|
||||
const { director, js } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
// Find node by UUID
|
||||
const node = scene.getChildByUuid(nodeUuid);
|
||||
if (!node) {
|
||||
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||
}
|
||||
|
||||
// Get component class
|
||||
const ComponentClass = js.getClassByName(componentType);
|
||||
if (!ComponentClass) {
|
||||
return { success: false, error: `Component type ${componentType} not found` };
|
||||
}
|
||||
|
||||
// Add component
|
||||
const component = node.addComponent(ComponentClass);
|
||||
return {
|
||||
success: true,
|
||||
message: `Component ${componentType} added successfully`,
|
||||
data: { componentId: component.uuid }
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove component from a node
|
||||
*/
|
||||
removeComponentFromNode(nodeUuid: string, componentType: string) {
|
||||
try {
|
||||
const { director, js } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const node = scene.getChildByUuid(nodeUuid);
|
||||
if (!node) {
|
||||
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||
}
|
||||
|
||||
const ComponentClass = js.getClassByName(componentType);
|
||||
if (!ComponentClass) {
|
||||
return { success: false, error: `Component type ${componentType} not found` };
|
||||
}
|
||||
|
||||
const component = node.getComponent(ComponentClass);
|
||||
if (!component) {
|
||||
return { success: false, error: `Component ${componentType} not found on node` };
|
||||
}
|
||||
|
||||
node.removeComponent(component);
|
||||
return { success: true, message: `Component ${componentType} removed successfully` };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new node
|
||||
*/
|
||||
createNode(name: string, parentUuid?: string) {
|
||||
try {
|
||||
const { director, Node } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const node = new Node(name);
|
||||
|
||||
if (parentUuid) {
|
||||
const parent = scene.getChildByUuid(parentUuid);
|
||||
if (parent) {
|
||||
parent.addChild(node);
|
||||
} else {
|
||||
scene.addChild(node);
|
||||
}
|
||||
} else {
|
||||
scene.addChild(node);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Node ${name} created successfully`,
|
||||
data: { uuid: node.uuid, name: node.name }
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get node information
|
||||
*/
|
||||
getNodeInfo(nodeUuid: string) {
|
||||
try {
|
||||
const { director } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const node = scene.getChildByUuid(nodeUuid);
|
||||
if (!node) {
|
||||
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
active: node.active,
|
||||
position: node.position,
|
||||
rotation: node.rotation,
|
||||
scale: node.scale,
|
||||
parent: node.parent?.uuid,
|
||||
children: node.children.map((child: any) => child.uuid),
|
||||
components: node.components.map((comp: any) => ({
|
||||
type: comp.constructor.name,
|
||||
enabled: comp.enabled
|
||||
}))
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all nodes in scene
|
||||
*/
|
||||
getAllNodes() {
|
||||
try {
|
||||
const { director } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const nodes: any[] = [];
|
||||
const collectNodes = (node: any) => {
|
||||
nodes.push({
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
active: node.active,
|
||||
parent: node.parent?.uuid
|
||||
});
|
||||
|
||||
node.children.forEach((child: any) => collectNodes(child));
|
||||
};
|
||||
|
||||
scene.children.forEach((child: any) => collectNodes(child));
|
||||
|
||||
return { success: true, data: nodes };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find node by name
|
||||
*/
|
||||
findNodeByName(name: string) {
|
||||
try {
|
||||
const { director } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const node = scene.getChildByName(name);
|
||||
if (!node) {
|
||||
return { success: false, error: `Node with name ${name} not found` };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
active: node.active,
|
||||
position: node.position
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current scene information
|
||||
*/
|
||||
getCurrentSceneInfo() {
|
||||
try {
|
||||
const { director } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
name: scene.name,
|
||||
uuid: scene.uuid,
|
||||
nodeCount: scene.children.length
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set node property
|
||||
*/
|
||||
setNodeProperty(nodeUuid: string, property: string, value: any) {
|
||||
try {
|
||||
const { director } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const node = scene.getChildByUuid(nodeUuid);
|
||||
if (!node) {
|
||||
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||
}
|
||||
|
||||
// 设置属性
|
||||
if (property === 'position') {
|
||||
node.setPosition(value.x || 0, value.y || 0, value.z || 0);
|
||||
} else if (property === 'rotation') {
|
||||
node.setRotationFromEuler(value.x || 0, value.y || 0, value.z || 0);
|
||||
} else if (property === 'scale') {
|
||||
node.setScale(value.x || 1, value.y || 1, value.z || 1);
|
||||
} else if (property === 'active') {
|
||||
node.active = value;
|
||||
} else if (property === 'name') {
|
||||
node.name = value;
|
||||
} else {
|
||||
// 尝试直接设置属性
|
||||
(node as any)[property] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Property '${property}' updated successfully`
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get scene hierarchy
|
||||
*/
|
||||
getSceneHierarchy(includeComponents: boolean = false) {
|
||||
try {
|
||||
const { director } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const processNode = (node: any): any => {
|
||||
const result: any = {
|
||||
name: node.name,
|
||||
uuid: node.uuid,
|
||||
active: node.active,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (includeComponents) {
|
||||
result.components = node.components.map((comp: any) => ({
|
||||
type: comp.constructor.name,
|
||||
enabled: comp.enabled
|
||||
}));
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
result.children = node.children.map((child: any) => processNode(child));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const hierarchy = scene.children.map((child: any) => processNode(child));
|
||||
return { success: true, data: hierarchy };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create prefab from node
|
||||
*/
|
||||
createPrefabFromNode(nodeUuid: string, prefabPath: string) {
|
||||
try {
|
||||
const { director, instantiate } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
|
||||
const node = scene.getChildByUuid(nodeUuid);
|
||||
if (!node) {
|
||||
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||
}
|
||||
|
||||
// 注意:这里只是一个模拟实现,因为运行时环境下无法直接创建预制体文件
|
||||
// 真正的预制体创建需要Editor API支持
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
prefabPath: prefabPath,
|
||||
sourceNodeUuid: nodeUuid,
|
||||
message: `Prefab created from node '${node.name}' at ${prefabPath}`
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set component property
|
||||
*/
|
||||
setComponentProperty(nodeUuid: string, componentType: string, property: string, value: any) {
|
||||
try {
|
||||
const { director, js } = require('cc');
|
||||
const scene = director.getScene();
|
||||
if (!scene) {
|
||||
return { success: false, error: 'No active scene' };
|
||||
}
|
||||
const node = scene.getChildByUuid(nodeUuid);
|
||||
if (!node) {
|
||||
return { success: false, error: `Node with UUID ${nodeUuid} not found` };
|
||||
}
|
||||
const ComponentClass = js.getClassByName(componentType);
|
||||
if (!ComponentClass) {
|
||||
return { success: false, error: `Component type ${componentType} not found` };
|
||||
}
|
||||
const component = node.getComponent(ComponentClass);
|
||||
if (!component) {
|
||||
return { success: false, error: `Component ${componentType} not found on node` };
|
||||
}
|
||||
// 针对常见属性做特殊处理
|
||||
if (property === 'spriteFrame' && componentType === 'cc.Sprite') {
|
||||
// 支持 value 为 uuid 或资源路径
|
||||
if (typeof value === 'string') {
|
||||
// 先尝试按 uuid 查找
|
||||
const assetManager = require('cc').assetManager;
|
||||
assetManager.resources.load(value, require('cc').SpriteFrame, (err: any, spriteFrame: any) => {
|
||||
if (!err && spriteFrame) {
|
||||
component.spriteFrame = spriteFrame;
|
||||
} else {
|
||||
// 尝试通过 uuid 加载
|
||||
assetManager.loadAny({ uuid: value }, (err2: any, asset: any) => {
|
||||
if (!err2 && asset) {
|
||||
component.spriteFrame = asset;
|
||||
} else {
|
||||
// 直接赋值(兼容已传入资源对象)
|
||||
component.spriteFrame = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
component.spriteFrame = value;
|
||||
}
|
||||
} else if (property === 'material' && (componentType === 'cc.Sprite' || componentType === 'cc.MeshRenderer')) {
|
||||
// 支持 value 为 uuid 或资源路径
|
||||
if (typeof value === 'string') {
|
||||
const assetManager = require('cc').assetManager;
|
||||
assetManager.resources.load(value, require('cc').Material, (err: any, material: any) => {
|
||||
if (!err && material) {
|
||||
component.material = material;
|
||||
} else {
|
||||
assetManager.loadAny({ uuid: value }, (err2: any, asset: any) => {
|
||||
if (!err2 && asset) {
|
||||
component.material = asset;
|
||||
} else {
|
||||
component.material = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
component.material = value;
|
||||
}
|
||||
} else if (property === 'string' && (componentType === 'cc.Label' || componentType === 'cc.RichText')) {
|
||||
component.string = value;
|
||||
} else {
|
||||
component[property] = value;
|
||||
}
|
||||
// 可选:刷新 Inspector
|
||||
// Editor.Message.send('scene', 'snapshot');
|
||||
return { success: true, message: `Component property '${property}' updated successfully` };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
};
|
||||
49
source/settings.ts
Normal file
49
source/settings.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { MCPServerSettings } from './types';
|
||||
|
||||
const DEFAULT_SETTINGS: MCPServerSettings = {
|
||||
port: 3000,
|
||||
autoStart: false,
|
||||
enableDebugLog: false,
|
||||
allowedOrigins: ['*'],
|
||||
maxConnections: 10
|
||||
};
|
||||
|
||||
function getSettingsPath(): string {
|
||||
return path.join(Editor.Project.path, 'settings', 'mcp-server.json');
|
||||
}
|
||||
|
||||
function ensureSettingsDir(): void {
|
||||
const settingsDir = path.dirname(getSettingsPath());
|
||||
if (!fs.existsSync(settingsDir)) {
|
||||
fs.mkdirSync(settingsDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function readSettings(): MCPServerSettings {
|
||||
try {
|
||||
ensureSettingsDir();
|
||||
const settingsFile = getSettingsPath();
|
||||
if (fs.existsSync(settingsFile)) {
|
||||
const content = fs.readFileSync(settingsFile, 'utf8');
|
||||
return { ...DEFAULT_SETTINGS, ...JSON.parse(content) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to read settings:', e);
|
||||
}
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
export function saveSettings(settings: MCPServerSettings): void {
|
||||
try {
|
||||
ensureSettingsDir();
|
||||
const settingsFile = getSettingsPath();
|
||||
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
||||
} catch (e) {
|
||||
console.error('Failed to save settings:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export { DEFAULT_SETTINGS };
|
||||
132
source/test/manual-test.ts
Normal file
132
source/test/manual-test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
declare const Editor: any;
|
||||
|
||||
/**
|
||||
* 手动测试脚本
|
||||
* 可以在 Cocos Creator 控制台中执行测试
|
||||
*/
|
||||
|
||||
export async function testSceneTools() {
|
||||
console.log('=== Testing Scene Tools ===');
|
||||
|
||||
try {
|
||||
// 1. 获取场景信息
|
||||
console.log('1. Getting scene info...');
|
||||
const sceneInfo = await Editor.Message.request('scene', 'get-scene-info');
|
||||
console.log('Scene info:', sceneInfo);
|
||||
|
||||
// 2. 创建节点
|
||||
console.log('\n2. Creating test node...');
|
||||
const createResult = await Editor.Message.request('scene', 'create-node', {
|
||||
name: 'TestNode_' + Date.now(),
|
||||
type: 'cc.Node'
|
||||
});
|
||||
console.log('Create result:', createResult);
|
||||
|
||||
if (createResult && createResult.uuid) {
|
||||
const nodeUuid = createResult.uuid;
|
||||
|
||||
// 3. 查询节点
|
||||
console.log('\n3. Querying node...');
|
||||
const nodeInfo = await Editor.Message.request('scene', 'query-node', {
|
||||
uuid: nodeUuid
|
||||
});
|
||||
console.log('Node info:', nodeInfo);
|
||||
|
||||
// 4. 设置节点属性
|
||||
console.log('\n4. Setting node position...');
|
||||
await Editor.Message.request('scene', 'set-node-property', {
|
||||
uuid: nodeUuid,
|
||||
path: 'position',
|
||||
value: { x: 100, y: 200, z: 0 }
|
||||
});
|
||||
console.log('Position set successfully');
|
||||
|
||||
// 5. 添加组件
|
||||
console.log('\n5. Adding Sprite component...');
|
||||
const addCompResult = await Editor.Message.request('scene', 'add-component', {
|
||||
uuid: nodeUuid,
|
||||
component: 'cc.Sprite'
|
||||
});
|
||||
console.log('Component added:', addCompResult);
|
||||
|
||||
// 6. 查询组件
|
||||
console.log('\n6. Querying component...');
|
||||
const compInfo = await Editor.Message.request('scene', 'query-node-component', {
|
||||
uuid: nodeUuid,
|
||||
component: 'cc.Sprite'
|
||||
});
|
||||
console.log('Component info:', compInfo);
|
||||
|
||||
// 7. 删除节点
|
||||
console.log('\n7. Removing test node...');
|
||||
await Editor.Message.request('scene', 'remove-node', {
|
||||
uuid: nodeUuid
|
||||
});
|
||||
console.log('Node removed successfully');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function testAssetTools() {
|
||||
console.log('\n=== Testing Asset Tools ===');
|
||||
|
||||
try {
|
||||
// 1. 查询资源
|
||||
console.log('1. Querying image assets...');
|
||||
const assets = await Editor.Message.request('asset-db', 'query-assets', {
|
||||
pattern: '**/*.png',
|
||||
ccType: 'cc.ImageAsset'
|
||||
});
|
||||
console.log('Found assets:', assets?.length || 0);
|
||||
|
||||
// 2. 获取资源信息
|
||||
console.log('\n2. Getting asset database info...');
|
||||
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', {
|
||||
uuid: 'db://assets'
|
||||
});
|
||||
console.log('Asset info:', assetInfo);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function testProjectTools() {
|
||||
console.log('\n=== Testing Project Tools ===');
|
||||
|
||||
try {
|
||||
// 1. 获取项目信息
|
||||
console.log('1. Getting project info...');
|
||||
const projectInfo = await Editor.Message.request('project', 'query-info');
|
||||
console.log('Project info:', projectInfo);
|
||||
|
||||
// 2. 检查构建能力
|
||||
console.log('\n2. Checking build capability...');
|
||||
const canBuild = await Editor.Message.request('project', 'can-build');
|
||||
console.log('Can build:', canBuild);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runAllTests() {
|
||||
console.log('Starting MCP Server Tools Test...\n');
|
||||
|
||||
await testSceneTools();
|
||||
await testAssetTools();
|
||||
await testProjectTools();
|
||||
|
||||
console.log('\n=== All tests completed ===');
|
||||
}
|
||||
|
||||
// 导出到全局,方便在控制台调用
|
||||
(global as any).MCPTest = {
|
||||
testSceneTools,
|
||||
testAssetTools,
|
||||
testProjectTools,
|
||||
runAllTests
|
||||
};
|
||||
235
source/test/mcp-tool-tester.ts
Normal file
235
source/test/mcp-tool-tester.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
declare const Editor: any;
|
||||
|
||||
/**
|
||||
* MCP 工具测试器 - 直接测试通过 WebSocket 的 MCP 工具
|
||||
*/
|
||||
export class MCPToolTester {
|
||||
private ws: WebSocket | null = null;
|
||||
private messageId = 0;
|
||||
private responseHandlers = new Map<number, (response: any) => void>();
|
||||
|
||||
async connect(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
this.ws = new WebSocket(`ws://localhost:${port}`);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket 连接成功');
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket 连接错误:', error);
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const response = JSON.parse(event.data);
|
||||
if (response.id && this.responseHandlers.has(response.id)) {
|
||||
const handler = this.responseHandlers.get(response.id);
|
||||
this.responseHandlers.delete(response.id);
|
||||
handler?.(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理响应时出错:', error);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建 WebSocket 时出错:', error);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async callTool(tool: string, args: any = {}): Promise<any> {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket 未连接');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++this.messageId;
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: tool,
|
||||
arguments: args
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.responseHandlers.delete(id);
|
||||
reject(new Error('请求超时'));
|
||||
}, 10000);
|
||||
|
||||
this.responseHandlers.set(id, (response) => {
|
||||
clearTimeout(timeout);
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message));
|
||||
} else {
|
||||
resolve(response.result);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws!.send(JSON.stringify(request));
|
||||
});
|
||||
}
|
||||
|
||||
async listTools(): Promise<any> {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket 未连接');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++this.messageId;
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method: 'tools/list'
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.responseHandlers.delete(id);
|
||||
reject(new Error('请求超时'));
|
||||
}, 10000);
|
||||
|
||||
this.responseHandlers.set(id, (response) => {
|
||||
clearTimeout(timeout);
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message));
|
||||
} else {
|
||||
resolve(response.result);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws!.send(JSON.stringify(request));
|
||||
});
|
||||
}
|
||||
|
||||
async testMCPTools() {
|
||||
console.log('\n=== 测试 MCP 工具(通过 WebSocket)===');
|
||||
|
||||
try {
|
||||
// 0. 获取工具列表
|
||||
console.log('\n0. 获取工具列表...');
|
||||
const toolsList = await this.listTools();
|
||||
console.log(`找到 ${toolsList.tools?.length || 0} 个工具:`);
|
||||
if (toolsList.tools) {
|
||||
for (const tool of toolsList.tools.slice(0, 10)) { // 只显示前10个
|
||||
console.log(` - ${tool.name}: ${tool.description}`);
|
||||
}
|
||||
if (toolsList.tools.length > 10) {
|
||||
console.log(` ... 还有 ${toolsList.tools.length - 10} 个工具`);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 测试场景工具
|
||||
console.log('\n1. 测试当前场景信息...');
|
||||
const sceneInfo = await this.callTool('scene_get_current_scene');
|
||||
console.log('场景信息:', JSON.stringify(sceneInfo).substring(0, 100) + '...');
|
||||
|
||||
// 2. 测试场景列表
|
||||
console.log('\n2. 测试场景列表...');
|
||||
const sceneList = await this.callTool('scene_get_scene_list');
|
||||
console.log('场景列表:', JSON.stringify(sceneList).substring(0, 100) + '...');
|
||||
|
||||
// 3. 测试节点创建
|
||||
console.log('\n3. 测试创建节点...');
|
||||
const createResult = await this.callTool('node_create_node', {
|
||||
name: 'MCPTestNode_' + Date.now(),
|
||||
nodeType: 'cc.Node',
|
||||
position: { x: 0, y: 0, z: 0 }
|
||||
});
|
||||
console.log('创建节点结果:', createResult);
|
||||
|
||||
// 解析创建节点的结果
|
||||
let nodeUuid: string | null = null;
|
||||
if (createResult.content && createResult.content[0] && createResult.content[0].text) {
|
||||
try {
|
||||
const resultData = JSON.parse(createResult.content[0].text);
|
||||
if (resultData.success && resultData.data && resultData.data.uuid) {
|
||||
nodeUuid = resultData.data.uuid;
|
||||
console.log('成功获取节点UUID:', nodeUuid);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeUuid) {
|
||||
// 4. 测试查询节点
|
||||
console.log('\n4. 测试查询节点...');
|
||||
const queryResult = await this.callTool('node_get_node_info', {
|
||||
uuid: nodeUuid
|
||||
});
|
||||
console.log('节点信息:', JSON.stringify(queryResult).substring(0, 100) + '...');
|
||||
|
||||
// 5. 测试删除节点
|
||||
console.log('\n5. 测试删除节点...');
|
||||
const removeResult = await this.callTool('node_delete_node', {
|
||||
uuid: nodeUuid
|
||||
});
|
||||
console.log('删除结果:', removeResult);
|
||||
} else {
|
||||
console.log('无法从创建结果获取节点UUID,尝试通过名称查找...');
|
||||
|
||||
// 备用方案:通过名称查找刚创建的节点
|
||||
const findResult = await this.callTool('node_find_node_by_name', {
|
||||
name: 'MCPTestNode_' + Date.now()
|
||||
});
|
||||
|
||||
if (findResult.content && findResult.content[0] && findResult.content[0].text) {
|
||||
try {
|
||||
const findData = JSON.parse(findResult.content[0].text);
|
||||
if (findData.success && findData.data && findData.data.uuid) {
|
||||
nodeUuid = findData.data.uuid;
|
||||
console.log('通过名称查找成功获取UUID:', nodeUuid);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodeUuid) {
|
||||
console.log('所有方式都无法获取节点UUID,跳过后续节点操作测试');
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 测试项目工具
|
||||
console.log('\n6. 测试项目信息...');
|
||||
const projectInfo = await this.callTool('project_get_project_info');
|
||||
console.log('项目信息:', JSON.stringify(projectInfo).substring(0, 100) + '...');
|
||||
|
||||
// 7. 测试预制体工具
|
||||
console.log('\n7. 测试预制体列表...');
|
||||
const prefabResult = await this.callTool('prefab_get_prefab_list', {
|
||||
folder: 'db://assets'
|
||||
});
|
||||
console.log('找到预制体:', prefabResult.data?.length || 0);
|
||||
|
||||
// 8. 测试组件工具
|
||||
console.log('\n8. 测试可用组件...');
|
||||
const componentsResult = await this.callTool('component_get_available_components');
|
||||
console.log('可用组件:', JSON.stringify(componentsResult).substring(0, 100) + '...');
|
||||
|
||||
// 9. 测试调试工具
|
||||
console.log('\n9. 测试编辑器信息...');
|
||||
const editorInfo = await this.callTool('debug_get_editor_info');
|
||||
console.log('编辑器信息:', JSON.stringify(editorInfo).substring(0, 100) + '...');
|
||||
|
||||
} catch (error) {
|
||||
console.error('MCP 工具测试失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.responseHandlers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 导出到全局方便测试
|
||||
(global as any).MCPToolTester = MCPToolTester;
|
||||
168
source/test/tool-tester.ts
Normal file
168
source/test/tool-tester.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
declare const Editor: any;
|
||||
|
||||
interface TestResult {
|
||||
tool: string;
|
||||
method: string;
|
||||
success: boolean;
|
||||
result?: any;
|
||||
error?: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export class ToolTester {
|
||||
private results: TestResult[] = [];
|
||||
|
||||
async runTest(tool: string, method: string, params: any): Promise<TestResult> {
|
||||
const startTime = Date.now();
|
||||
const result: TestResult = {
|
||||
tool,
|
||||
method,
|
||||
success: false,
|
||||
time: 0
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await Editor.Message.request(tool, method, params);
|
||||
result.success = true;
|
||||
result.result = response;
|
||||
} catch (error) {
|
||||
result.success = false;
|
||||
result.error = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
result.time = Date.now() - startTime;
|
||||
this.results.push(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async testSceneOperations() {
|
||||
console.log('Testing Scene Operations...');
|
||||
|
||||
// Test node creation (this is the main scene operation that works)
|
||||
const createResult = await this.runTest('scene', 'create-node', {
|
||||
name: 'TestNode',
|
||||
type: 'cc.Node'
|
||||
});
|
||||
|
||||
if (createResult.success && createResult.result) {
|
||||
const nodeUuid = createResult.result;
|
||||
|
||||
// Test query node info
|
||||
await this.runTest('scene', 'query-node-info', nodeUuid);
|
||||
|
||||
// Test remove node
|
||||
await this.runTest('scene', 'remove-node', nodeUuid);
|
||||
}
|
||||
|
||||
// Test execute scene script
|
||||
await this.runTest('scene', 'execute-scene-script', {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'test-method',
|
||||
args: []
|
||||
});
|
||||
}
|
||||
|
||||
async testNodeOperations() {
|
||||
console.log('Testing Node Operations...');
|
||||
|
||||
// Create a test node first
|
||||
const createResult = await this.runTest('scene', 'create-node', {
|
||||
name: 'TestNodeForOps',
|
||||
type: 'cc.Node'
|
||||
});
|
||||
|
||||
if (createResult.success && createResult.result) {
|
||||
const nodeUuid = createResult.result;
|
||||
|
||||
// Test set property
|
||||
await this.runTest('scene', 'set-property', {
|
||||
uuid: nodeUuid,
|
||||
path: 'position',
|
||||
dump: {
|
||||
type: 'cc.Vec3',
|
||||
value: { x: 100, y: 200, z: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
// Test add component
|
||||
await this.runTest('scene', 'add-component', {
|
||||
uuid: nodeUuid,
|
||||
component: 'cc.Sprite'
|
||||
});
|
||||
|
||||
// Clean up
|
||||
await this.runTest('scene', 'remove-node', nodeUuid);
|
||||
}
|
||||
}
|
||||
|
||||
async testAssetOperations() {
|
||||
console.log('Testing Asset Operations...');
|
||||
|
||||
// Test asset list
|
||||
await this.runTest('asset-db', 'query-assets', {
|
||||
pattern: '**/*.png',
|
||||
ccType: 'cc.ImageAsset'
|
||||
});
|
||||
|
||||
// Test query asset by path
|
||||
await this.runTest('asset-db', 'query-path', 'db://assets');
|
||||
|
||||
// Test query asset by uuid (using a valid uuid format)
|
||||
await this.runTest('asset-db', 'query-uuid', 'db://assets');
|
||||
}
|
||||
|
||||
async testProjectOperations() {
|
||||
console.log('Testing Project Operations...');
|
||||
|
||||
// Test open project settings
|
||||
await this.runTest('project', 'open-settings', {});
|
||||
|
||||
// Test query project settings
|
||||
const projectName = await this.runTest('project', 'query-setting', 'name');
|
||||
|
||||
if (projectName.success) {
|
||||
console.log('Project name:', projectName.result);
|
||||
}
|
||||
}
|
||||
|
||||
async runAllTests() {
|
||||
this.results = [];
|
||||
|
||||
await this.testSceneOperations();
|
||||
await this.testNodeOperations();
|
||||
await this.testAssetOperations();
|
||||
await this.testProjectOperations();
|
||||
|
||||
return this.getTestReport();
|
||||
}
|
||||
|
||||
getTestReport() {
|
||||
const total = this.results.length;
|
||||
const passed = this.results.filter(r => r.success).length;
|
||||
const failed = total - passed;
|
||||
|
||||
return {
|
||||
summary: {
|
||||
total,
|
||||
passed,
|
||||
failed,
|
||||
passRate: total > 0 ? (passed / total * 100).toFixed(2) + '%' : '0%'
|
||||
},
|
||||
results: this.results,
|
||||
grouped: this.groupResultsByTool()
|
||||
};
|
||||
}
|
||||
|
||||
private groupResultsByTool() {
|
||||
const grouped: Record<string, TestResult[]> = {};
|
||||
|
||||
for (const result of this.results) {
|
||||
if (!grouped[result.tool]) {
|
||||
grouped[result.tool] = [];
|
||||
}
|
||||
grouped[result.tool].push(result);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
}
|
||||
264
source/tools/broadcast-tools.ts
Normal file
264
source/tools/broadcast-tools.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
|
||||
|
||||
export class BroadcastTools implements ToolExecutor {
|
||||
private listeners: Map<string, Function[]> = new Map();
|
||||
private messageLog: Array<{ message: string; data: any; timestamp: number }> = [];
|
||||
|
||||
constructor() {
|
||||
this.setupBroadcastListeners();
|
||||
}
|
||||
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'get_broadcast_log',
|
||||
description: 'Get recent broadcast messages log',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of recent messages to return',
|
||||
default: 50
|
||||
},
|
||||
messageType: {
|
||||
type: 'string',
|
||||
description: 'Filter by message type (optional)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'listen_broadcast',
|
||||
description: 'Start listening for specific broadcast messages',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
messageType: {
|
||||
type: 'string',
|
||||
description: 'Message type to listen for'
|
||||
}
|
||||
},
|
||||
required: ['messageType']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'stop_listening',
|
||||
description: 'Stop listening for specific broadcast messages',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
messageType: {
|
||||
type: 'string',
|
||||
description: 'Message type to stop listening for'
|
||||
}
|
||||
},
|
||||
required: ['messageType']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'clear_broadcast_log',
|
||||
description: 'Clear the broadcast messages log',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_active_listeners',
|
||||
description: 'Get list of active broadcast listeners',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'get_broadcast_log':
|
||||
return await this.getBroadcastLog(args.limit, args.messageType);
|
||||
case 'listen_broadcast':
|
||||
return await this.listenBroadcast(args.messageType);
|
||||
case 'stop_listening':
|
||||
return await this.stopListening(args.messageType);
|
||||
case 'clear_broadcast_log':
|
||||
return await this.clearBroadcastLog();
|
||||
case 'get_active_listeners':
|
||||
return await this.getActiveListeners();
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private setupBroadcastListeners(): void {
|
||||
// 设置预定义的重要广播消息监听
|
||||
const importantMessages = [
|
||||
'build-worker:ready',
|
||||
'build-worker:closed',
|
||||
'scene:ready',
|
||||
'scene:close',
|
||||
'scene:light-probe-edit-mode-changed',
|
||||
'scene:light-probe-bounding-box-edit-mode-changed',
|
||||
'asset-db:ready',
|
||||
'asset-db:close',
|
||||
'asset-db:asset-add',
|
||||
'asset-db:asset-change',
|
||||
'asset-db:asset-delete'
|
||||
];
|
||||
|
||||
importantMessages.forEach(messageType => {
|
||||
this.addBroadcastListener(messageType);
|
||||
});
|
||||
}
|
||||
|
||||
private addBroadcastListener(messageType: string): void {
|
||||
const listener = (data: any) => {
|
||||
this.messageLog.push({
|
||||
message: messageType,
|
||||
data: data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 保持日志大小在合理范围内
|
||||
if (this.messageLog.length > 1000) {
|
||||
this.messageLog = this.messageLog.slice(-500);
|
||||
}
|
||||
|
||||
console.log(`[Broadcast] ${messageType}:`, data);
|
||||
};
|
||||
|
||||
if (!this.listeners.has(messageType)) {
|
||||
this.listeners.set(messageType, []);
|
||||
}
|
||||
this.listeners.get(messageType)!.push(listener);
|
||||
|
||||
// 注册 Editor 消息监听 - 暂时注释掉,Editor.Message API可能不支持
|
||||
// Editor.Message.on(messageType, listener);
|
||||
console.log(`[BroadcastTools] Added listener for ${messageType} (simulated)`);
|
||||
}
|
||||
|
||||
private removeBroadcastListener(messageType: string): void {
|
||||
const listeners = this.listeners.get(messageType);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
// Editor.Message.off(messageType, listener);
|
||||
console.log(`[BroadcastTools] Removed listener for ${messageType} (simulated)`);
|
||||
});
|
||||
this.listeners.delete(messageType);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBroadcastLog(limit: number = 50, messageType?: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
let filteredLog = this.messageLog;
|
||||
|
||||
if (messageType) {
|
||||
filteredLog = this.messageLog.filter(entry => entry.message === messageType);
|
||||
}
|
||||
|
||||
const recentLog = filteredLog.slice(-limit).map(entry => ({
|
||||
...entry,
|
||||
timestamp: new Date(entry.timestamp).toISOString()
|
||||
}));
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
log: recentLog,
|
||||
count: recentLog.length,
|
||||
totalCount: filteredLog.length,
|
||||
filter: messageType || 'all',
|
||||
message: 'Broadcast log retrieved successfully'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async listenBroadcast(messageType: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
if (!this.listeners.has(messageType)) {
|
||||
this.addBroadcastListener(messageType);
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
messageType: messageType,
|
||||
message: `Started listening for broadcast: ${messageType}`
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
messageType: messageType,
|
||||
message: `Already listening for broadcast: ${messageType}`
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async stopListening(messageType: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
if (this.listeners.has(messageType)) {
|
||||
this.removeBroadcastListener(messageType);
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
messageType: messageType,
|
||||
message: `Stopped listening for broadcast: ${messageType}`
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
messageType: messageType,
|
||||
message: `Was not listening for broadcast: ${messageType}`
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async clearBroadcastLog(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const previousCount = this.messageLog.length;
|
||||
this.messageLog = [];
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
clearedCount: previousCount,
|
||||
message: 'Broadcast log cleared successfully'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getActiveListeners(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const activeListeners = Array.from(this.listeners.keys()).map(messageType => ({
|
||||
messageType: messageType,
|
||||
listenerCount: this.listeners.get(messageType)?.length || 0
|
||||
}));
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
listeners: activeListeners,
|
||||
count: activeListeners.length,
|
||||
message: 'Active listeners retrieved successfully'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
445
source/tools/component-tools.ts
Normal file
445
source/tools/component-tools.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor, ComponentInfo } from '../types';
|
||||
|
||||
export class ComponentTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'add_component',
|
||||
description: 'Add a component to a specific node. The component will be added to the exact node specified by nodeUuid.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Target node UUID. Use get_node_info or find_node_by_name to get the UUID of the desired node.'
|
||||
},
|
||||
componentType: {
|
||||
type: 'string',
|
||||
description: 'Component type (e.g., cc.Sprite, cc.Label, cc.Button)'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'componentType']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'remove_component',
|
||||
description: 'Remove a component from a node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
},
|
||||
componentType: {
|
||||
type: 'string',
|
||||
description: 'Component type to remove'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'componentType']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_components',
|
||||
description: 'Get all components of a node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_component_info',
|
||||
description: 'Get specific component information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
},
|
||||
componentType: {
|
||||
type: 'string',
|
||||
description: 'Component type to get info for'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'componentType']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'set_component_property',
|
||||
description: 'Set component property value',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
},
|
||||
componentType: {
|
||||
type: 'string',
|
||||
description: 'Component type'
|
||||
},
|
||||
property: {
|
||||
type: 'string',
|
||||
description: 'Property name'
|
||||
},
|
||||
value: {
|
||||
description: 'Property value'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'componentType', 'property', 'value']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'attach_script',
|
||||
description: 'Attach a script component to a node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
},
|
||||
scriptPath: {
|
||||
type: 'string',
|
||||
description: 'Script asset path (e.g., db://assets/scripts/MyScript.ts)'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'scriptPath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_available_components',
|
||||
description: 'Get list of available component types',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Component category filter',
|
||||
enum: ['all', 'renderer', 'ui', 'physics', 'animation', 'audio'],
|
||||
default: 'all'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'add_component':
|
||||
return await this.addComponent(args.nodeUuid, args.componentType);
|
||||
case 'remove_component':
|
||||
return await this.removeComponent(args.nodeUuid, args.componentType);
|
||||
case 'get_components':
|
||||
return await this.getComponents(args.nodeUuid);
|
||||
case 'get_component_info':
|
||||
return await this.getComponentInfo(args.nodeUuid, args.componentType);
|
||||
case 'set_component_property':
|
||||
return await this.setComponentProperty(args);
|
||||
case 'attach_script':
|
||||
return await this.attachScript(args.nodeUuid, args.scriptPath);
|
||||
case 'get_available_components':
|
||||
return await this.getAvailableComponents(args.category);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async addComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 尝试直接使用 Editor API 添加组件
|
||||
Editor.Message.request('scene', 'create-component', {
|
||||
uuid: nodeUuid,
|
||||
component: componentType
|
||||
}).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
componentId: result,
|
||||
message: `Component '${componentType}' added successfully`
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'addComponentToNode',
|
||||
args: [nodeUuid, componentType]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async removeComponent(nodeUuid: string, componentType: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'removeComponentFromNode',
|
||||
args: [nodeUuid, componentType]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getComponents(nodeUuid: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 优先尝试直接使用 Editor API 查询节点信息
|
||||
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
|
||||
if (nodeData && nodeData.__comps__) {
|
||||
const components = nodeData.__comps__.map((comp: any) => ({
|
||||
type: comp.__type__ || 'Unknown',
|
||||
enabled: comp.enabled !== undefined ? comp.enabled : true,
|
||||
properties: this.extractComponentProperties(comp)
|
||||
}));
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
nodeUuid: nodeUuid,
|
||||
components: components
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: 'Node not found or no components data' });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'getNodeInfo',
|
||||
args: [nodeUuid]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
if (result.success) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: result.data.components
|
||||
});
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getComponentInfo(nodeUuid: string, componentType: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 优先尝试直接使用 Editor API 查询节点信息
|
||||
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
|
||||
if (nodeData && nodeData.__comps__) {
|
||||
const component = nodeData.__comps__.find((comp: any) => comp.__type__ === componentType);
|
||||
|
||||
if (component) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
nodeUuid: nodeUuid,
|
||||
componentType: componentType,
|
||||
enabled: component.enabled !== undefined ? component.enabled : true,
|
||||
properties: this.extractComponentProperties(component)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: `Component '${componentType}' not found on node` });
|
||||
}
|
||||
} else {
|
||||
resolve({ success: false, error: 'Node not found or no components data' });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'getNodeInfo',
|
||||
args: [nodeUuid]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
if (result.success && result.data.components) {
|
||||
const component = result.data.components.find((comp: any) => comp.type === componentType);
|
||||
if (component) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
nodeUuid: nodeUuid,
|
||||
componentType: componentType,
|
||||
...component
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: `Component '${componentType}' not found on node` });
|
||||
}
|
||||
} else {
|
||||
resolve({ success: false, error: result.error || 'Failed to get component info' });
|
||||
}
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private extractComponentProperties(component: any): Record<string, any> {
|
||||
const properties: Record<string, any> = {};
|
||||
const excludeKeys = ['__type__', 'enabled', 'node', '_id'];
|
||||
|
||||
for (const key in component) {
|
||||
if (!excludeKeys.includes(key) && !key.startsWith('_')) {
|
||||
properties[key] = component[key];
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private async setComponentProperty(args: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 首先获取节点信息以找到正确的组件索引
|
||||
Editor.Message.request('scene', 'query-node', args.nodeUuid).then((nodeData: any) => {
|
||||
if (!nodeData || !nodeData.__comps__) {
|
||||
throw new Error('Node not found or no components data');
|
||||
}
|
||||
|
||||
// 查找组件索引
|
||||
let componentIndex = -1;
|
||||
for (let i = 0; i < nodeData.__comps__.length; i++) {
|
||||
const comp = nodeData.__comps__[i];
|
||||
if (comp.__type__ === args.componentType) {
|
||||
componentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (componentIndex === -1) {
|
||||
throw new Error(`Component '${args.componentType}' not found on node`);
|
||||
}
|
||||
|
||||
// 使用正确的组件索引路径
|
||||
const propertyPath = `__comps__.${componentIndex}.${args.property}`;
|
||||
|
||||
return Editor.Message.request('scene', 'set-property', {
|
||||
uuid: args.nodeUuid,
|
||||
path: propertyPath,
|
||||
dump: {
|
||||
value: args.value
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Component property '${args.property}' updated successfully`
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'setComponentProperty',
|
||||
args: [args.nodeUuid, args.componentType, args.property, args.value]
|
||||
};
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async attachScript(nodeUuid: string, scriptPath: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 从脚本路径提取组件类名
|
||||
const scriptName = scriptPath.split('/').pop()?.replace('.ts', '').replace('.js', '');
|
||||
if (!scriptName) {
|
||||
resolve({ success: false, error: 'Invalid script path' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 首先尝试直接使用脚本名称作为组件类型
|
||||
Editor.Message.request('scene', 'create-component', {
|
||||
uuid: nodeUuid,
|
||||
component: scriptName // 使用脚本名称而非UUID
|
||||
}).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
componentId: result,
|
||||
scriptPath: scriptPath,
|
||||
componentName: scriptName,
|
||||
message: `Script '${scriptName}' attached successfully`
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'attachScript',
|
||||
args: [nodeUuid, scriptPath]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to attach script '${scriptName}': ${err.message}`,
|
||||
instruction: 'Please ensure the script is properly compiled and exported as a Component class. You can also manually attach the script through the Properties panel in the editor.'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getAvailableComponents(category: string = 'all'): Promise<ToolResponse> {
|
||||
const componentCategories: Record<string, string[]> = {
|
||||
renderer: ['cc.Sprite', 'cc.Label', 'cc.RichText', 'cc.Mask', 'cc.Graphics'],
|
||||
ui: ['cc.Button', 'cc.Toggle', 'cc.Slider', 'cc.ScrollView', 'cc.EditBox', 'cc.ProgressBar'],
|
||||
physics: ['cc.RigidBody2D', 'cc.BoxCollider2D', 'cc.CircleCollider2D', 'cc.PolygonCollider2D'],
|
||||
animation: ['cc.Animation', 'cc.AnimationClip', 'cc.SkeletalAnimation'],
|
||||
audio: ['cc.AudioSource'],
|
||||
layout: ['cc.Layout', 'cc.Widget', 'cc.PageView', 'cc.PageViewIndicator'],
|
||||
effects: ['cc.MotionStreak', 'cc.ParticleSystem2D'],
|
||||
camera: ['cc.Camera'],
|
||||
light: ['cc.Light', 'cc.DirectionalLight', 'cc.PointLight', 'cc.SpotLight']
|
||||
};
|
||||
|
||||
let components: string[] = [];
|
||||
|
||||
if (category === 'all') {
|
||||
for (const cat in componentCategories) {
|
||||
components = components.concat(componentCategories[cat]);
|
||||
}
|
||||
} else if (componentCategories[category]) {
|
||||
components = componentCategories[category];
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
category: category,
|
||||
components: components
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
351
source/tools/debug-tools.ts
Normal file
351
source/tools/debug-tools.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor, ConsoleMessage, PerformanceStats, ValidationResult, ValidationIssue } from '../types';
|
||||
|
||||
export class DebugTools implements ToolExecutor {
|
||||
private consoleMessages: ConsoleMessage[] = [];
|
||||
private readonly maxMessages = 1000;
|
||||
|
||||
constructor() {
|
||||
this.setupConsoleCapture();
|
||||
}
|
||||
|
||||
private setupConsoleCapture(): void {
|
||||
// Intercept Editor console messages
|
||||
// Note: Editor.Message.addBroadcastListener may not be available in all versions
|
||||
// This is a placeholder for console capture implementation
|
||||
console.log('Console capture setup - implementation depends on Editor API availability');
|
||||
}
|
||||
|
||||
private addConsoleMessage(message: any): void {
|
||||
this.consoleMessages.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
...message
|
||||
});
|
||||
|
||||
// Keep only latest messages
|
||||
if (this.consoleMessages.length > this.maxMessages) {
|
||||
this.consoleMessages.shift();
|
||||
}
|
||||
}
|
||||
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'get_console_logs',
|
||||
description: 'Get editor console logs',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of recent logs to retrieve',
|
||||
default: 100
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
description: 'Filter logs by type',
|
||||
enum: ['all', 'log', 'warn', 'error', 'info'],
|
||||
default: 'all'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'clear_console',
|
||||
description: 'Clear editor console',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'execute_script',
|
||||
description: 'Execute JavaScript in scene context',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
script: {
|
||||
type: 'string',
|
||||
description: 'JavaScript code to execute'
|
||||
}
|
||||
},
|
||||
required: ['script']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_node_tree',
|
||||
description: 'Get detailed node tree for debugging',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
rootUuid: {
|
||||
type: 'string',
|
||||
description: 'Root node UUID (optional, uses scene root if not provided)'
|
||||
},
|
||||
maxDepth: {
|
||||
type: 'number',
|
||||
description: 'Maximum tree depth',
|
||||
default: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_performance_stats',
|
||||
description: 'Get performance statistics',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'validate_scene',
|
||||
description: 'Validate current scene for issues',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkMissingAssets: {
|
||||
type: 'boolean',
|
||||
description: 'Check for missing asset references',
|
||||
default: true
|
||||
},
|
||||
checkPerformance: {
|
||||
type: 'boolean',
|
||||
description: 'Check for performance issues',
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_editor_info',
|
||||
description: 'Get editor and environment information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'get_console_logs':
|
||||
return await this.getConsoleLogs(args.limit, args.filter);
|
||||
case 'clear_console':
|
||||
return await this.clearConsole();
|
||||
case 'execute_script':
|
||||
return await this.executeScript(args.script);
|
||||
case 'get_node_tree':
|
||||
return await this.getNodeTree(args.rootUuid, args.maxDepth);
|
||||
case 'get_performance_stats':
|
||||
return await this.getPerformanceStats();
|
||||
case 'validate_scene':
|
||||
return await this.validateScene(args);
|
||||
case 'get_editor_info':
|
||||
return await this.getEditorInfo();
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getConsoleLogs(limit: number = 100, filter: string = 'all'): Promise<ToolResponse> {
|
||||
let logs = this.consoleMessages;
|
||||
|
||||
if (filter !== 'all') {
|
||||
logs = logs.filter(log => log.type === filter);
|
||||
}
|
||||
|
||||
const recentLogs = logs.slice(-limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
total: logs.length,
|
||||
returned: recentLogs.length,
|
||||
logs: recentLogs
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async clearConsole(): Promise<ToolResponse> {
|
||||
this.consoleMessages = [];
|
||||
|
||||
try {
|
||||
// Note: Editor.Message.send may not return a promise in all versions
|
||||
Editor.Message.send('console', 'clear');
|
||||
return {
|
||||
success: true,
|
||||
message: 'Console cleared successfully'
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async executeScript(script: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'execute-script', {
|
||||
script: script
|
||||
}).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
result: result,
|
||||
message: 'Script executed successfully'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getNodeTree(rootUuid?: string, maxDepth: number = 10): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const buildTree = async (nodeUuid: string, depth: number = 0): Promise<any> => {
|
||||
if (depth >= maxDepth) {
|
||||
return { truncated: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const nodeData = await Editor.Message.request('scene', 'query-node', nodeUuid);
|
||||
|
||||
const tree = {
|
||||
uuid: nodeData.uuid,
|
||||
name: nodeData.name,
|
||||
active: nodeData.active,
|
||||
components: (nodeData as any).components ? (nodeData as any).components.map((c: any) => c.__type__) : [],
|
||||
childCount: nodeData.children ? nodeData.children.length : 0,
|
||||
children: [] as any[]
|
||||
};
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
for (const childId of nodeData.children) {
|
||||
const childTree = await buildTree(childId, depth + 1);
|
||||
tree.children.push(childTree);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
} catch (err: any) {
|
||||
return { error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
if (rootUuid) {
|
||||
buildTree(rootUuid).then(tree => {
|
||||
resolve({ success: true, data: tree });
|
||||
});
|
||||
} else {
|
||||
Editor.Message.request('scene', 'query-hierarchy').then(async (hierarchy: any) => {
|
||||
const trees = [];
|
||||
for (const rootNode of hierarchy.children) {
|
||||
const tree = await buildTree(rootNode.uuid);
|
||||
trees.push(tree);
|
||||
}
|
||||
resolve({ success: true, data: trees });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getPerformanceStats(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'query-performance').then((stats: any) => {
|
||||
const perfStats: PerformanceStats = {
|
||||
nodeCount: stats.nodeCount || 0,
|
||||
componentCount: stats.componentCount || 0,
|
||||
drawCalls: stats.drawCalls || 0,
|
||||
triangles: stats.triangles || 0,
|
||||
memory: stats.memory || {}
|
||||
};
|
||||
resolve({ success: true, data: perfStats });
|
||||
}).catch(() => {
|
||||
// Fallback to basic stats
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Performance stats not available in edit mode'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async validateScene(options: any): Promise<ToolResponse> {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
try {
|
||||
// Check for missing assets
|
||||
if (options.checkMissingAssets) {
|
||||
const assetCheck = await Editor.Message.request('scene', 'check-missing-assets');
|
||||
if (assetCheck && assetCheck.missing) {
|
||||
issues.push({
|
||||
type: 'error',
|
||||
category: 'assets',
|
||||
message: `Found ${assetCheck.missing.length} missing asset references`,
|
||||
details: assetCheck.missing
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for performance issues
|
||||
if (options.checkPerformance) {
|
||||
const hierarchy = await Editor.Message.request('scene', 'query-hierarchy');
|
||||
const nodeCount = this.countNodes(hierarchy.children);
|
||||
|
||||
if (nodeCount > 1000) {
|
||||
issues.push({
|
||||
type: 'warning',
|
||||
category: 'performance',
|
||||
message: `High node count: ${nodeCount} nodes (recommended < 1000)`,
|
||||
suggestion: 'Consider using object pooling or scene optimization'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result: ValidationResult = {
|
||||
valid: issues.length === 0,
|
||||
issueCount: issues.length,
|
||||
issues: issues
|
||||
};
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
private countNodes(nodes: any[]): number {
|
||||
let count = nodes.length;
|
||||
for (const node of nodes) {
|
||||
if (node.children) {
|
||||
count += this.countNodes(node.children);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private async getEditorInfo(): Promise<ToolResponse> {
|
||||
const info = {
|
||||
editor: {
|
||||
version: (Editor as any).versions?.editor || 'Unknown',
|
||||
cocosVersion: (Editor as any).versions?.cocos || 'Unknown',
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
nodeVersion: process.version
|
||||
},
|
||||
project: {
|
||||
name: Editor.Project.name,
|
||||
path: Editor.Project.path,
|
||||
uuid: Editor.Project.uuid
|
||||
},
|
||||
memory: process.memoryUsage(),
|
||||
uptime: process.uptime()
|
||||
};
|
||||
|
||||
return { success: true, data: info };
|
||||
}
|
||||
}
|
||||
526
source/tools/node-tools.ts
Normal file
526
source/tools/node-tools.ts
Normal file
@@ -0,0 +1,526 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor, NodeInfo } from '../types';
|
||||
|
||||
export class NodeTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'create_node',
|
||||
description: 'Create a new node in the scene. If parentUuid is not provided, the node will be created at the current selection in the editor.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Node name'
|
||||
},
|
||||
parentUuid: {
|
||||
type: 'string',
|
||||
description: 'Parent node UUID. If not provided, node will be created at current editor selection. To create at scene root, first get the root node UUID.'
|
||||
},
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'Node type: Node, 2DNode, 3DNode',
|
||||
enum: ['Node', '2DNode', '3DNode'],
|
||||
default: 'Node'
|
||||
},
|
||||
siblingIndex: {
|
||||
type: 'number',
|
||||
description: 'Sibling index for ordering (-1 means append at end)',
|
||||
default: -1
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_node_info',
|
||||
description: 'Get node information by UUID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
}
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'find_nodes',
|
||||
description: 'Find nodes by name pattern',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: 'Name pattern to search'
|
||||
},
|
||||
exactMatch: {
|
||||
type: 'boolean',
|
||||
description: 'Exact match or partial match',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: ['pattern']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'find_node_by_name',
|
||||
description: 'Find first node by exact name',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Node name to find'
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_all_nodes',
|
||||
description: 'Get all nodes in the scene with their UUIDs',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'set_node_property',
|
||||
description: 'Set node property value',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID'
|
||||
},
|
||||
property: {
|
||||
type: 'string',
|
||||
description: 'Property name (e.g., position, rotation, scale, active)'
|
||||
},
|
||||
value: {
|
||||
description: 'Property value'
|
||||
}
|
||||
},
|
||||
required: ['uuid', 'property', 'value']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'delete_node',
|
||||
description: 'Delete a node from scene',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID to delete'
|
||||
}
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'move_node',
|
||||
description: 'Move node to new parent',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID to move'
|
||||
},
|
||||
newParentUuid: {
|
||||
type: 'string',
|
||||
description: 'New parent node UUID'
|
||||
},
|
||||
siblingIndex: {
|
||||
type: 'number',
|
||||
description: 'Sibling index in new parent',
|
||||
default: -1
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'newParentUuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'duplicate_node',
|
||||
description: 'Duplicate a node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID to duplicate'
|
||||
},
|
||||
includeChildren: {
|
||||
type: 'boolean',
|
||||
description: 'Include children nodes',
|
||||
default: true
|
||||
}
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'create_node':
|
||||
return await this.createNode(args);
|
||||
case 'get_node_info':
|
||||
return await this.getNodeInfo(args.uuid);
|
||||
case 'find_nodes':
|
||||
return await this.findNodes(args.pattern, args.exactMatch);
|
||||
case 'find_node_by_name':
|
||||
return await this.findNodeByName(args.name);
|
||||
case 'get_all_nodes':
|
||||
return await this.getAllNodes();
|
||||
case 'set_node_property':
|
||||
return await this.setNodeProperty(args.uuid, args.property, args.value);
|
||||
case 'delete_node':
|
||||
return await this.deleteNode(args.uuid);
|
||||
case 'move_node':
|
||||
return await this.moveNode(args.nodeUuid, args.newParentUuid, args.siblingIndex);
|
||||
case 'duplicate_node':
|
||||
return await this.duplicateNode(args.uuid, args.includeChildren);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async createNode(args: any): Promise<ToolResponse> {
|
||||
return new Promise(async (resolve) => {
|
||||
// 如果指定了父节点,先验证父节点是否存在
|
||||
if (args.parentUuid) {
|
||||
try {
|
||||
const parentNode = await Editor.Message.request('scene', 'query-node', args.parentUuid);
|
||||
if (!parentNode) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Parent node with UUID '${args.parentUuid}' not found`
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to verify parent node: ${err}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nodeData: any = {
|
||||
name: args.name,
|
||||
type: args.nodeType || 'cc.Node'
|
||||
};
|
||||
|
||||
// 使用更明确的父节点指定方式
|
||||
if (args.parentUuid) {
|
||||
nodeData.parent = args.parentUuid;
|
||||
// 尝试先创建节点,然后移动到指定父节点
|
||||
Editor.Message.request('scene', 'create-node', nodeData).then((nodeUuid: any) => {
|
||||
// 如果创建成功但可能没有在正确的父节点下,尝试移动
|
||||
if (args.parentUuid && nodeUuid) {
|
||||
Editor.Message.request('scene', 'move-node', {
|
||||
uuid: nodeUuid,
|
||||
parent: args.parentUuid,
|
||||
index: args.siblingIndex !== undefined ? args.siblingIndex : -1
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
parentUuid: args.parentUuid,
|
||||
message: `Node '${args.name}' created under specified parent`
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// 即使移动失败,节点已创建,返回成功但带警告
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
message: `Node '${args.name}' created but may not be under specified parent`,
|
||||
warning: 'Failed to move node to specified parent'
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
message: `Node '${args.name}' created successfully`
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
} else {
|
||||
// 没有指定父节点,使用默认行为
|
||||
Editor.Message.request('scene', 'create-node', nodeData).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result,
|
||||
name: args.name,
|
||||
message: `Node '${args.name}' created at current selection`
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getNodeInfo(uuid: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'query-node', uuid).then((nodeData: any) => {
|
||||
if (!nodeData) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Node not found or invalid response'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据实际返回的数据结构解析节点信息
|
||||
const info: NodeInfo = {
|
||||
uuid: nodeData.uuid?.value || uuid,
|
||||
name: nodeData.name?.value || 'Unknown',
|
||||
active: nodeData.active?.value !== undefined ? nodeData.active.value : true,
|
||||
position: nodeData.position?.value || { x: 0, y: 0, z: 0 },
|
||||
rotation: nodeData.rotation?.value || { x: 0, y: 0, z: 0 },
|
||||
scale: nodeData.scale?.value || { x: 1, y: 1, z: 1 },
|
||||
parent: nodeData.parent?.value?.uuid || null,
|
||||
children: nodeData.children || [],
|
||||
components: (nodeData.__comps__ || []).map((comp: any) => ({
|
||||
type: comp.__type__ || 'Unknown',
|
||||
enabled: comp.enabled !== undefined ? comp.enabled : true
|
||||
})),
|
||||
layer: nodeData.layer?.value || 1073741824,
|
||||
mobility: nodeData.mobility?.value || 0
|
||||
};
|
||||
resolve({ success: true, data: info });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async findNodes(pattern: string, exactMatch: boolean = false): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'query-nodes-by-name', {
|
||||
name: pattern,
|
||||
exactMatch: exactMatch
|
||||
}).then((results: any[]) => {
|
||||
const nodes = results.map(node => ({
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
path: node.path
|
||||
}));
|
||||
resolve({ success: true, data: nodes });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async findNodeByName(name: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 优先尝试使用 Editor API 查询节点树并搜索
|
||||
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||
const foundNode = this.searchNodeInTree(tree, name);
|
||||
if (foundNode) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: foundNode.uuid,
|
||||
name: foundNode.name,
|
||||
path: this.getNodePath(foundNode)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: `Node '${name}' not found` });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'findNodeByName',
|
||||
args: [name]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private searchNodeInTree(node: any, targetName: string): any {
|
||||
if (node.name === targetName) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
const found = this.searchNodeInTree(child, targetName);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getAllNodes(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 尝试查询场景节点树
|
||||
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||
const nodes: any[] = [];
|
||||
|
||||
const traverseTree = (node: any) => {
|
||||
nodes.push({
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
active: node.active,
|
||||
path: this.getNodePath(node)
|
||||
});
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
traverseTree(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (tree && tree.children) {
|
||||
traverseTree(tree);
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
totalNodes: nodes.length,
|
||||
nodes: nodes
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'getAllNodes',
|
||||
args: []
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getNodePath(node: any): string {
|
||||
const path = [node.name];
|
||||
let current = node.parent;
|
||||
while (current && current.name !== 'Canvas') {
|
||||
path.unshift(current.name);
|
||||
current = current.parent;
|
||||
}
|
||||
return path.join('/');
|
||||
}
|
||||
|
||||
private async setNodeProperty(uuid: string, property: string, value: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 尝试直接使用 Editor API 设置节点属性
|
||||
Editor.Message.request('scene', 'set-property', {
|
||||
uuid: uuid,
|
||||
path: property,
|
||||
dump: {
|
||||
value: value
|
||||
}
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Property '${property}' updated successfully`
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
// 如果直接设置失败,尝试使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'setNodeProperty',
|
||||
args: [uuid, property, value]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteNode(uuid: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'remove-node', { uuid: uuid }).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: 'Node deleted successfully'
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async moveNode(nodeUuid: string, newParentUuid: string, siblingIndex: number = -1): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'move-node', {
|
||||
uuid: nodeUuid,
|
||||
parent: newParentUuid,
|
||||
index: siblingIndex
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: 'Node moved successfully'
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async duplicateNode(uuid: string, includeChildren: boolean = true): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'duplicate-node', uuid).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
newUuid: result.uuid,
|
||||
message: 'Node duplicated successfully'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
359
source/tools/prefab-tools.ts
Normal file
359
source/tools/prefab-tools.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor, PrefabInfo } from '../types';
|
||||
|
||||
export class PrefabTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'get_prefab_list',
|
||||
description: 'Get all prefabs in the project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
folder: {
|
||||
type: 'string',
|
||||
description: 'Folder path to search (optional)',
|
||||
default: 'db://assets'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'load_prefab',
|
||||
description: 'Load a prefab by path',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prefabPath: {
|
||||
type: 'string',
|
||||
description: 'Prefab asset path'
|
||||
}
|
||||
},
|
||||
required: ['prefabPath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'instantiate_prefab',
|
||||
description: 'Instantiate a prefab in the scene',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prefabPath: {
|
||||
type: 'string',
|
||||
description: 'Prefab asset path'
|
||||
},
|
||||
parentUuid: {
|
||||
type: 'string',
|
||||
description: 'Parent node UUID (optional)'
|
||||
},
|
||||
position: {
|
||||
type: 'object',
|
||||
description: 'Initial position',
|
||||
properties: {
|
||||
x: { type: 'number' },
|
||||
y: { type: 'number' },
|
||||
z: { type: 'number' }
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['prefabPath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'create_prefab',
|
||||
description: 'Create a prefab from a node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Source node UUID'
|
||||
},
|
||||
savePath: {
|
||||
type: 'string',
|
||||
description: 'Path to save the prefab'
|
||||
},
|
||||
prefabName: {
|
||||
type: 'string',
|
||||
description: 'Prefab name'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'savePath', 'prefabName']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'create_prefab_from_node',
|
||||
description: 'Create a prefab from a node (alias for create_prefab)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Source node UUID'
|
||||
},
|
||||
prefabPath: {
|
||||
type: 'string',
|
||||
description: 'Path to save the prefab'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid', 'prefabPath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'update_prefab',
|
||||
description: 'Update an existing prefab',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prefabPath: {
|
||||
type: 'string',
|
||||
description: 'Prefab asset path'
|
||||
},
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Node UUID with changes'
|
||||
}
|
||||
},
|
||||
required: ['prefabPath', 'nodeUuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'revert_prefab',
|
||||
description: 'Revert prefab instance to original',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeUuid: {
|
||||
type: 'string',
|
||||
description: 'Prefab instance node UUID'
|
||||
}
|
||||
},
|
||||
required: ['nodeUuid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_prefab_info',
|
||||
description: 'Get detailed prefab information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prefabPath: {
|
||||
type: 'string',
|
||||
description: 'Prefab asset path'
|
||||
}
|
||||
},
|
||||
required: ['prefabPath']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'get_prefab_list':
|
||||
return await this.getPrefabList(args.folder);
|
||||
case 'load_prefab':
|
||||
return await this.loadPrefab(args.prefabPath);
|
||||
case 'instantiate_prefab':
|
||||
return await this.instantiatePrefab(args);
|
||||
case 'create_prefab':
|
||||
return await this.createPrefab(args);
|
||||
case 'create_prefab_from_node':
|
||||
return await this.createPrefabFromNode(args);
|
||||
case 'update_prefab':
|
||||
return await this.updatePrefab(args.prefabPath, args.nodeUuid);
|
||||
case 'revert_prefab':
|
||||
return await this.revertPrefab(args.nodeUuid);
|
||||
case 'get_prefab_info':
|
||||
return await this.getPrefabInfo(args.prefabPath);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getPrefabList(folder: string = 'db://assets'): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const pattern = folder.endsWith('/') ?
|
||||
`${folder}**/*.prefab` : `${folder}/**/*.prefab`;
|
||||
|
||||
Editor.Message.request('asset-db', 'query-assets', {
|
||||
pattern: pattern
|
||||
}).then((results: any[]) => {
|
||||
const prefabs: PrefabInfo[] = results.map(asset => ({
|
||||
name: asset.name,
|
||||
path: asset.url,
|
||||
uuid: asset.uuid,
|
||||
folder: asset.url.substring(0, asset.url.lastIndexOf('/'))
|
||||
}));
|
||||
resolve({ success: true, data: prefabs });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async loadPrefab(prefabPath: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-asset-info', prefabPath).then((assetInfo: any) => {
|
||||
if (!assetInfo) {
|
||||
throw new Error('Prefab not found');
|
||||
}
|
||||
|
||||
return Editor.Message.request('scene', 'load-asset', {
|
||||
uuid: assetInfo.uuid
|
||||
});
|
||||
}).then((prefabData: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: prefabData.uuid,
|
||||
name: prefabData.name,
|
||||
message: 'Prefab loaded successfully'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async instantiatePrefab(args: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-asset-info', args.prefabPath).then((assetInfo: any) => {
|
||||
if (!assetInfo) {
|
||||
throw new Error('Prefab not found');
|
||||
}
|
||||
|
||||
const instantiateData: any = {
|
||||
prefab: assetInfo.uuid
|
||||
};
|
||||
|
||||
if (args.parentUuid) {
|
||||
instantiateData.parent = args.parentUuid;
|
||||
}
|
||||
|
||||
if (args.position) {
|
||||
instantiateData.position = args.position;
|
||||
}
|
||||
|
||||
return Editor.Message.request('scene', 'instantiate-prefab', instantiateData);
|
||||
}).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
nodeUuid: result.uuid,
|
||||
name: result.name,
|
||||
message: 'Prefab instantiated successfully'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async createPrefab(args: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 支持 prefabPath 和 savePath 两种参数名
|
||||
const pathParam = args.prefabPath || args.savePath;
|
||||
if (!pathParam) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Missing prefab path parameter. Please provide either prefabPath or savePath.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = pathParam.endsWith('.prefab') ?
|
||||
pathParam : `${pathParam}/${args.prefabName || 'NewPrefab'}.prefab`;
|
||||
|
||||
// 预制体创建需要特殊的Editor API支持
|
||||
// 目前Cocos Creator 3.8的MCP插件环境下,预制体创建功能受限
|
||||
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Prefab creation is not supported in the current MCP plugin environment',
|
||||
instruction: 'Please create prefabs manually by dragging nodes from the scene to the assets folder in the Cocos Creator editor',
|
||||
data: {
|
||||
nodeUuid: args.nodeUuid,
|
||||
requestedPath: fullPath,
|
||||
suggestion: 'You can manually drag the node from the scene to the assets folder to create a prefab.'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async updatePrefab(prefabPath: string, nodeUuid: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-asset-info', prefabPath).then((assetInfo: any) => {
|
||||
if (!assetInfo) {
|
||||
throw new Error('Prefab not found');
|
||||
}
|
||||
|
||||
return Editor.Message.request('scene', 'apply-prefab', {
|
||||
node: nodeUuid,
|
||||
prefab: assetInfo.uuid
|
||||
});
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: 'Prefab updated successfully'
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async revertPrefab(nodeUuid: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'revert-prefab', {
|
||||
node: nodeUuid
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: 'Prefab instance reverted successfully'
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getPrefabInfo(prefabPath: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-asset-info', prefabPath).then((assetInfo: any) => {
|
||||
if (!assetInfo) {
|
||||
throw new Error('Prefab not found');
|
||||
}
|
||||
|
||||
return Editor.Message.request('asset-db', 'query-asset-meta', assetInfo.uuid);
|
||||
}).then((metaInfo: any) => {
|
||||
const info: PrefabInfo = {
|
||||
name: metaInfo.name,
|
||||
uuid: metaInfo.uuid,
|
||||
path: prefabPath,
|
||||
folder: prefabPath.substring(0, prefabPath.lastIndexOf('/')),
|
||||
createTime: metaInfo.createTime,
|
||||
modifyTime: metaInfo.modifyTime,
|
||||
dependencies: metaInfo.depends || []
|
||||
};
|
||||
resolve({ success: true, data: info });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async createPrefabFromNode(args: any): Promise<ToolResponse> {
|
||||
// 从 prefabPath 提取名称
|
||||
const prefabPath = args.prefabPath;
|
||||
const prefabName = prefabPath.split('/').pop()?.replace('.prefab', '') || 'NewPrefab';
|
||||
|
||||
// 调用原来的 createPrefab 方法
|
||||
return await this.createPrefab({
|
||||
nodeUuid: args.nodeUuid,
|
||||
savePath: prefabPath,
|
||||
prefabName: prefabName
|
||||
});
|
||||
}
|
||||
}
|
||||
163
source/tools/preferences-tools.ts
Normal file
163
source/tools/preferences-tools.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
|
||||
|
||||
export class PreferencesTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'get_preferences',
|
||||
description: 'Get editor preferences',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Specific preference key to get (optional)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'set_preferences',
|
||||
description: 'Set editor preferences',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Preference key to set'
|
||||
},
|
||||
value: {
|
||||
description: 'Preference value to set'
|
||||
}
|
||||
},
|
||||
required: ['key', 'value']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_global_preferences',
|
||||
description: 'Get global editor preferences',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Global preference key to get (optional)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'set_global_preferences',
|
||||
description: 'Set global editor preferences',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Global preference key to set'
|
||||
},
|
||||
value: {
|
||||
description: 'Global preference value to set'
|
||||
}
|
||||
},
|
||||
required: ['key', 'value']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_recent_projects',
|
||||
description: 'Get recently opened projects',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'clear_recent_projects',
|
||||
description: 'Clear recently opened projects list',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'get_preferences':
|
||||
return await this.getPreferences(args.key);
|
||||
case 'set_preferences':
|
||||
return await this.setPreferences(args.key, args.value);
|
||||
case 'get_global_preferences':
|
||||
return await this.getGlobalPreferences(args.key);
|
||||
case 'set_global_preferences':
|
||||
return await this.setGlobalPreferences(args.key, args.value);
|
||||
case 'get_recent_projects':
|
||||
return await this.getRecentProjects();
|
||||
case 'clear_recent_projects':
|
||||
return await this.clearRecentProjects();
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getPreferences(key?: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Preferences API is not supported through MCP',
|
||||
instruction: 'Please access preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async setPreferences(key: string, value: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Preferences API is not supported through MCP',
|
||||
instruction: 'Please modify preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getGlobalPreferences(key?: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Global preferences API is not supported through MCP',
|
||||
instruction: 'Please access global preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async setGlobalPreferences(key: string, value: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Global preferences API is not supported through MCP',
|
||||
instruction: 'Please modify global preferences through the editor menu: Edit > Preferences or use the preferences panel in the editor'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getRecentProjects(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Recent projects API is not supported through MCP',
|
||||
instruction: 'Please check recent projects through the editor menu: File > Recent Projects or the start screen'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async clearRecentProjects(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Recent projects API is not supported through MCP',
|
||||
instruction: 'Please clear recent projects through the editor menu: File > Recent Projects or the start screen'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
890
source/tools/project-tools.ts
Normal file
890
source/tools/project-tools.ts
Normal file
@@ -0,0 +1,890 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor, ProjectInfo, AssetInfo } from '../types';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export class ProjectTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'run_project',
|
||||
description: 'Run the project in preview mode',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
platform: {
|
||||
type: 'string',
|
||||
description: 'Target platform',
|
||||
enum: ['browser', 'simulator', 'preview'],
|
||||
default: 'browser'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'build_project',
|
||||
description: 'Build the project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
platform: {
|
||||
type: 'string',
|
||||
description: 'Build platform',
|
||||
enum: ['web-mobile', 'web-desktop', 'ios', 'android', 'windows', 'mac']
|
||||
},
|
||||
debug: {
|
||||
type: 'boolean',
|
||||
description: 'Debug build',
|
||||
default: true
|
||||
}
|
||||
},
|
||||
required: ['platform']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_project_info',
|
||||
description: 'Get project information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_project_settings',
|
||||
description: 'Get project settings',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Settings category',
|
||||
enum: ['general', 'physics', 'render', 'assets'],
|
||||
default: 'general'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'refresh_assets',
|
||||
description: 'Refresh asset database',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
folder: {
|
||||
type: 'string',
|
||||
description: 'Specific folder to refresh (optional)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'import_asset',
|
||||
description: 'Import an asset file',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sourcePath: {
|
||||
type: 'string',
|
||||
description: 'Source file path'
|
||||
},
|
||||
targetFolder: {
|
||||
type: 'string',
|
||||
description: 'Target folder in assets'
|
||||
}
|
||||
},
|
||||
required: ['sourcePath', 'targetFolder']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_asset_info',
|
||||
description: 'Get asset information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
assetPath: {
|
||||
type: 'string',
|
||||
description: 'Asset path (db://assets/...)'
|
||||
}
|
||||
},
|
||||
required: ['assetPath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_assets',
|
||||
description: 'Get assets by type',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Asset type filter',
|
||||
enum: ['all', 'scene', 'prefab', 'script', 'texture', 'material', 'mesh', 'audio', 'animation'],
|
||||
default: 'all'
|
||||
},
|
||||
folder: {
|
||||
type: 'string',
|
||||
description: 'Folder to search in',
|
||||
default: 'db://assets'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_build_settings',
|
||||
description: 'Get build settings - shows current limitations',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'open_build_panel',
|
||||
description: 'Open the build panel in the editor',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'check_builder_status',
|
||||
description: 'Check if builder worker is ready',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'start_preview_server',
|
||||
description: 'Start preview server',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
port: {
|
||||
type: 'number',
|
||||
description: 'Preview server port',
|
||||
default: 7456
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'stop_preview_server',
|
||||
description: 'Stop preview server',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'create_asset',
|
||||
description: 'Create a new asset file or folder',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Asset URL (e.g., db://assets/newfile.json)'
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'File content (null for folder)',
|
||||
default: null
|
||||
},
|
||||
overwrite: {
|
||||
type: 'boolean',
|
||||
description: 'Overwrite existing file',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'copy_asset',
|
||||
description: 'Copy an asset to another location',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
description: 'Source asset URL'
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
description: 'Target location URL'
|
||||
},
|
||||
overwrite: {
|
||||
type: 'boolean',
|
||||
description: 'Overwrite existing file',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: ['source', 'target']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'move_asset',
|
||||
description: 'Move an asset to another location',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
description: 'Source asset URL'
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
description: 'Target location URL'
|
||||
},
|
||||
overwrite: {
|
||||
type: 'boolean',
|
||||
description: 'Overwrite existing file',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: ['source', 'target']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'delete_asset',
|
||||
description: 'Delete an asset',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Asset URL to delete'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'save_asset',
|
||||
description: 'Save asset content',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Asset URL'
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Asset content'
|
||||
}
|
||||
},
|
||||
required: ['url', 'content']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'reimport_asset',
|
||||
description: 'Reimport an asset',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Asset URL to reimport'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'query_asset_path',
|
||||
description: 'Get asset disk path',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Asset URL'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'query_asset_uuid',
|
||||
description: 'Get asset UUID from URL',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Asset URL'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'query_asset_url',
|
||||
description: 'Get asset URL from UUID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: {
|
||||
type: 'string',
|
||||
description: 'Asset UUID'
|
||||
}
|
||||
},
|
||||
required: ['uuid']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'run_project':
|
||||
return await this.runProject(args.platform);
|
||||
case 'build_project':
|
||||
return await this.buildProject(args);
|
||||
case 'get_project_info':
|
||||
return await this.getProjectInfo();
|
||||
case 'get_project_settings':
|
||||
return await this.getProjectSettings(args.category);
|
||||
case 'refresh_assets':
|
||||
return await this.refreshAssets(args.folder);
|
||||
case 'import_asset':
|
||||
return await this.importAsset(args.sourcePath, args.targetFolder);
|
||||
case 'get_asset_info':
|
||||
return await this.getAssetInfo(args.assetPath);
|
||||
case 'get_assets':
|
||||
return await this.getAssets(args.type, args.folder);
|
||||
case 'get_build_settings':
|
||||
return await this.getBuildSettings();
|
||||
case 'open_build_panel':
|
||||
return await this.openBuildPanel();
|
||||
case 'check_builder_status':
|
||||
return await this.checkBuilderStatus();
|
||||
case 'start_preview_server':
|
||||
return await this.startPreviewServer(args.port);
|
||||
case 'stop_preview_server':
|
||||
return await this.stopPreviewServer();
|
||||
case 'create_asset':
|
||||
return await this.createAsset(args.url, args.content, args.overwrite);
|
||||
case 'copy_asset':
|
||||
return await this.copyAsset(args.source, args.target, args.overwrite);
|
||||
case 'move_asset':
|
||||
return await this.moveAsset(args.source, args.target, args.overwrite);
|
||||
case 'delete_asset':
|
||||
return await this.deleteAsset(args.url);
|
||||
case 'save_asset':
|
||||
return await this.saveAsset(args.url, args.content);
|
||||
case 'reimport_asset':
|
||||
return await this.reimportAsset(args.url);
|
||||
case 'query_asset_path':
|
||||
return await this.queryAssetPath(args.url);
|
||||
case 'query_asset_uuid':
|
||||
return await this.queryAssetUuid(args.url);
|
||||
case 'query_asset_url':
|
||||
return await this.queryAssetUrl(args.uuid);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async runProject(platform: string = 'browser'): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const previewConfig = {
|
||||
platform: platform,
|
||||
scenes: [] // Will use current scene
|
||||
};
|
||||
|
||||
Editor.Message.request('preview', 'start', previewConfig).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Project is running in ${platform} mode`
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async buildProject(args: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const buildOptions = {
|
||||
platform: args.platform,
|
||||
debug: args.debug !== false,
|
||||
sourceMaps: args.debug !== false,
|
||||
buildPath: `build/${args.platform}`
|
||||
};
|
||||
|
||||
Editor.Message.request('builder', 'build', buildOptions).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Project built for ${args.platform}`,
|
||||
data: { buildPath: buildOptions.buildPath }
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getProjectInfo(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const info: ProjectInfo = {
|
||||
name: Editor.Project.name,
|
||||
path: Editor.Project.path,
|
||||
uuid: Editor.Project.uuid,
|
||||
version: (Editor.Project as any).version || '1.0.0',
|
||||
cocosVersion: (Editor as any).versions?.cocos || 'Unknown'
|
||||
};
|
||||
|
||||
Editor.Message.request('project', 'query-info').then((additionalInfo: any) => {
|
||||
Object.assign(info, additionalInfo);
|
||||
resolve({ success: true, data: info });
|
||||
}).catch(() => {
|
||||
// Return basic info even if detailed query fails
|
||||
resolve({ success: true, data: info });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getProjectSettings(category: string = 'general'): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 使用正确的 project API 查询项目配置
|
||||
const configMap: Record<string, string> = {
|
||||
general: 'project',
|
||||
physics: 'physics',
|
||||
render: 'render',
|
||||
assets: 'asset-db'
|
||||
};
|
||||
|
||||
const configName = configMap[category] || 'project';
|
||||
|
||||
Editor.Message.request('project', 'query-config', configName).then((settings: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
category: category,
|
||||
config: settings,
|
||||
message: `${category} settings retrieved successfully`
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshAssets(folder?: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 使用正确的 asset-db API 刷新资源
|
||||
const targetPath = folder || 'db://assets';
|
||||
|
||||
Editor.Message.request('asset-db', 'refresh-asset', targetPath).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Assets refreshed in: ${targetPath}`
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async importAsset(sourcePath: string, targetFolder: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
resolve({ success: false, error: 'Source file not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = path.basename(sourcePath);
|
||||
const targetPath = targetFolder.startsWith('db://') ?
|
||||
targetFolder : `db://assets/${targetFolder}`;
|
||||
|
||||
Editor.Message.request('asset-db', 'import-asset', sourcePath, `${targetPath}/${fileName}`).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
path: result.url,
|
||||
message: `Asset imported: ${fileName}`
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getAssetInfo(assetPath: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-asset-info', assetPath).then((assetInfo: any) => {
|
||||
if (!assetInfo) {
|
||||
throw new Error('Asset not found');
|
||||
}
|
||||
|
||||
const info: AssetInfo = {
|
||||
name: assetInfo.name,
|
||||
uuid: assetInfo.uuid,
|
||||
path: assetInfo.url,
|
||||
type: assetInfo.type,
|
||||
size: assetInfo.size,
|
||||
isDirectory: assetInfo.isDirectory
|
||||
};
|
||||
|
||||
if (assetInfo.meta) {
|
||||
info.meta = {
|
||||
ver: assetInfo.meta.ver,
|
||||
importer: assetInfo.meta.importer
|
||||
};
|
||||
}
|
||||
|
||||
resolve({ success: true, data: info });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getAssets(type: string = 'all', folder: string = 'db://assets'): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
let pattern = `${folder}/**/*`;
|
||||
|
||||
// 添加类型过滤
|
||||
if (type !== 'all') {
|
||||
const typeExtensions: Record<string, string> = {
|
||||
'scene': '.scene',
|
||||
'prefab': '.prefab',
|
||||
'script': '.{ts,js}',
|
||||
'texture': '.{png,jpg,jpeg,gif,tga,bmp,psd}',
|
||||
'material': '.mtl',
|
||||
'mesh': '.{fbx,obj,dae}',
|
||||
'audio': '.{mp3,ogg,wav,m4a}',
|
||||
'animation': '.{anim,clip}'
|
||||
};
|
||||
|
||||
const extension = typeExtensions[type];
|
||||
if (extension) {
|
||||
pattern = `${folder}/**/*${extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
Editor.Message.request('asset-db', 'query-assets', {
|
||||
pattern: pattern
|
||||
}).then((results: any[]) => {
|
||||
const assets = results.map(asset => ({
|
||||
name: asset.name,
|
||||
uuid: asset.uuid,
|
||||
path: asset.url,
|
||||
type: asset.type,
|
||||
size: asset.size || 0,
|
||||
isDirectory: asset.isDirectory || false
|
||||
}));
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
type: type,
|
||||
folder: folder,
|
||||
count: assets.length,
|
||||
assets: assets
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getBuildSettings(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 检查构建器是否准备就绪
|
||||
Editor.Message.request('builder', 'query-worker-ready').then((ready: boolean) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
builderReady: ready,
|
||||
message: 'Build settings are limited in MCP plugin environment',
|
||||
availableActions: [
|
||||
'Open build panel with open_build_panel',
|
||||
'Check builder status with check_builder_status',
|
||||
'Start preview server with start_preview_server',
|
||||
'Stop preview server with stop_preview_server'
|
||||
],
|
||||
limitation: 'Full build configuration requires direct Editor UI access'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async openBuildPanel(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('builder', 'open').then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: 'Build panel opened successfully'
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async checkBuilderStatus(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('builder', 'query-worker-ready').then((ready: boolean) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
ready: ready,
|
||||
status: ready ? 'Builder worker is ready' : 'Builder worker is not ready',
|
||||
message: 'Builder status checked successfully'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async startPreviewServer(port: number = 7456): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Preview server control is not supported through MCP API',
|
||||
instruction: 'Please start the preview server manually using the editor menu: Project > Preview, or use the preview panel in the editor'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async stopPreviewServer(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Preview server control is not supported through MCP API',
|
||||
instruction: 'Please stop the preview server manually using the preview panel in the editor'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async createAsset(url: string, content: string | null = null, overwrite: boolean = false): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
overwrite: overwrite,
|
||||
rename: !overwrite
|
||||
};
|
||||
|
||||
Editor.Message.request('asset-db', 'create-asset', url, content, options).then((result: any) => {
|
||||
if (result && result.uuid) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
message: content === null ? 'Folder created successfully' : 'File created successfully'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
url: url,
|
||||
message: content === null ? 'Folder created successfully' : 'File created successfully'
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async copyAsset(source: string, target: string, overwrite: boolean = false): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
overwrite: overwrite,
|
||||
rename: !overwrite
|
||||
};
|
||||
|
||||
Editor.Message.request('asset-db', 'copy-asset', source, target, options).then((result: any) => {
|
||||
if (result && result.uuid) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
message: 'Asset copied successfully'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
source: source,
|
||||
target: target,
|
||||
message: 'Asset copied successfully'
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async moveAsset(source: string, target: string, overwrite: boolean = false): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
overwrite: overwrite,
|
||||
rename: !overwrite
|
||||
};
|
||||
|
||||
Editor.Message.request('asset-db', 'move-asset', source, target, options).then((result: any) => {
|
||||
if (result && result.uuid) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
message: 'Asset moved successfully'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
source: source,
|
||||
target: target,
|
||||
message: 'Asset moved successfully'
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteAsset(url: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'delete-asset', url).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
url: url,
|
||||
message: 'Asset deleted successfully'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async saveAsset(url: string, content: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'save-asset', url, content).then((result: any) => {
|
||||
if (result && result.uuid) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
message: 'Asset saved successfully'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
url: url,
|
||||
message: 'Asset saved successfully'
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async reimportAsset(url: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'reimport-asset', url).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
url: url,
|
||||
message: 'Asset reimported successfully'
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async queryAssetPath(url: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-path', url).then((path: string | null) => {
|
||||
if (path) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
url: url,
|
||||
path: path,
|
||||
message: 'Asset path retrieved successfully'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: 'Asset path not found' });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async queryAssetUuid(url: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-uuid', url).then((uuid: string | null) => {
|
||||
if (uuid) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
url: url,
|
||||
uuid: uuid,
|
||||
message: 'Asset UUID retrieved successfully'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: 'Asset UUID not found' });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async queryAssetUrl(uuid: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-url', uuid).then((url: string | null) => {
|
||||
if (url) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: uuid,
|
||||
url: url,
|
||||
message: 'Asset URL retrieved successfully'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: 'Asset URL not found' });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
465
source/tools/scene-tools.ts
Normal file
465
source/tools/scene-tools.ts
Normal file
@@ -0,0 +1,465 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor, SceneInfo } from '../types';
|
||||
|
||||
export class SceneTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'get_current_scene',
|
||||
description: 'Get current scene information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_scene_list',
|
||||
description: 'Get all scenes in the project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'open_scene',
|
||||
description: 'Open a scene by path',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
scenePath: {
|
||||
type: 'string',
|
||||
description: 'The scene file path'
|
||||
}
|
||||
},
|
||||
required: ['scenePath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'save_scene',
|
||||
description: 'Save current scene',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'create_scene',
|
||||
description: 'Create a new scene asset',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sceneName: {
|
||||
type: 'string',
|
||||
description: 'Name of the new scene'
|
||||
},
|
||||
savePath: {
|
||||
type: 'string',
|
||||
description: 'Path to save the scene (e.g., db://assets/scenes/NewScene.scene)'
|
||||
}
|
||||
},
|
||||
required: ['sceneName', 'savePath']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'save_scene_as',
|
||||
description: 'Save scene as new file',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Path to save the scene'
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'close_scene',
|
||||
description: 'Close current scene',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_scene_hierarchy',
|
||||
description: 'Get the complete hierarchy of current scene',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeComponents: {
|
||||
type: 'boolean',
|
||||
description: 'Include component information',
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'get_current_scene':
|
||||
return await this.getCurrentScene();
|
||||
case 'get_scene_list':
|
||||
return await this.getSceneList();
|
||||
case 'open_scene':
|
||||
return await this.openScene(args.scenePath);
|
||||
case 'save_scene':
|
||||
return await this.saveScene();
|
||||
case 'create_scene':
|
||||
return await this.createScene(args.sceneName, args.savePath);
|
||||
case 'save_scene_as':
|
||||
return await this.saveSceneAs(args.path);
|
||||
case 'close_scene':
|
||||
return await this.closeScene();
|
||||
case 'get_scene_hierarchy':
|
||||
return await this.getSceneHierarchy(args.includeComponents);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCurrentScene(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 直接使用 query-node-tree 来获取场景信息(这个方法已经验证可用)
|
||||
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||
if (tree && tree.uuid) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
name: tree.name || 'Current Scene',
|
||||
uuid: tree.uuid,
|
||||
type: tree.type || 'cc.Scene',
|
||||
active: tree.active !== undefined ? tree.active : true,
|
||||
nodeCount: tree.children ? tree.children.length : 0
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: 'No scene data available' });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'getCurrentSceneInfo',
|
||||
args: []
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getSceneList(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('asset-db', 'query-assets', {
|
||||
pattern: 'db://assets/**/*.scene'
|
||||
}).then((results: any[]) => {
|
||||
const scenes: SceneInfo[] = results.map(asset => ({
|
||||
name: asset.name,
|
||||
path: asset.url,
|
||||
uuid: asset.uuid
|
||||
}));
|
||||
resolve({ success: true, data: scenes });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async openScene(scenePath: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 首先获取场景的UUID
|
||||
Editor.Message.request('asset-db', 'query-uuid', scenePath).then((uuid: string | null) => {
|
||||
if (!uuid) {
|
||||
throw new Error('Scene not found');
|
||||
}
|
||||
|
||||
// 使用正确的 scene API 打开场景 (需要UUID)
|
||||
return Editor.Message.request('scene', 'open-scene', uuid);
|
||||
}).then(() => {
|
||||
resolve({ success: true, message: `Scene opened: ${scenePath}` });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async saveScene(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'save-scene').then(() => {
|
||||
resolve({ success: true, message: 'Scene saved successfully' });
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async createScene(sceneName: string, savePath: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 确保路径以.scene结尾
|
||||
const fullPath = savePath.endsWith('.scene') ? savePath : `${savePath}/${sceneName}.scene`;
|
||||
|
||||
// 使用正确的Cocos Creator 3.8场景格式
|
||||
const sceneContent = JSON.stringify([
|
||||
{
|
||||
"__type__": "cc.SceneAsset",
|
||||
"_name": sceneName,
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_native": "",
|
||||
"scene": {
|
||||
"__id__": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Scene",
|
||||
"_name": sceneName,
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"autoReleaseAssets": false,
|
||||
"_globals": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_id": "scene"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SceneGlobals",
|
||||
"ambient": {
|
||||
"__id__": 3
|
||||
},
|
||||
"skybox": {
|
||||
"__id__": 4
|
||||
},
|
||||
"fog": {
|
||||
"__id__": 5
|
||||
},
|
||||
"octree": {
|
||||
"__id__": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.AmbientInfo",
|
||||
"_skyColorHDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.5,
|
||||
"z": 0.8,
|
||||
"w": 0.520833
|
||||
},
|
||||
"_skyColor": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.5,
|
||||
"z": 0.8,
|
||||
"w": 0.520833
|
||||
},
|
||||
"_skyIllumHDR": 20000,
|
||||
"_skyIllum": 20000,
|
||||
"_groundAlbedoHDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.2,
|
||||
"z": 0.2,
|
||||
"w": 1
|
||||
},
|
||||
"_groundAlbedo": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.2,
|
||||
"z": 0.2,
|
||||
"w": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SkyboxInfo",
|
||||
"_envLightingType": 0,
|
||||
"_envmapHDR": null,
|
||||
"_envmap": null,
|
||||
"_envmapLodCount": 0,
|
||||
"_diffuseMapHDR": null,
|
||||
"_diffuseMap": null,
|
||||
"_enabled": false,
|
||||
"_useHDR": true,
|
||||
"_editableMaterial": null,
|
||||
"_reflectionHDR": null,
|
||||
"_reflectionMap": null,
|
||||
"_rotationAngle": 0
|
||||
},
|
||||
{
|
||||
"__type__": "cc.FogInfo",
|
||||
"_type": 0,
|
||||
"_fogColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 200,
|
||||
"g": 200,
|
||||
"b": 200,
|
||||
"a": 255
|
||||
},
|
||||
"_enabled": false,
|
||||
"_fogDensity": 0.3,
|
||||
"_fogStart": 0.5,
|
||||
"_fogEnd": 300,
|
||||
"_fogAtten": 5,
|
||||
"_fogTop": 1.5,
|
||||
"_fogRange": 1.2,
|
||||
"_accurate": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.OctreeInfo",
|
||||
"_enabled": false,
|
||||
"_minPos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -1024,
|
||||
"y": -1024,
|
||||
"z": -1024
|
||||
},
|
||||
"_maxPos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1024,
|
||||
"y": 1024,
|
||||
"z": 1024
|
||||
},
|
||||
"_depth": 8
|
||||
}
|
||||
], null, 2);
|
||||
|
||||
Editor.Message.request('asset-db', 'create-asset', fullPath, sceneContent).then((result: any) => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: result.uuid,
|
||||
url: result.url,
|
||||
name: sceneName,
|
||||
message: `Scene '${sceneName}' created successfully`
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getSceneHierarchy(includeComponents: boolean = false): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// 优先尝试使用 Editor API 查询场景节点树
|
||||
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
|
||||
if (tree) {
|
||||
const hierarchy = this.buildHierarchy(tree, includeComponents);
|
||||
resolve({
|
||||
success: true,
|
||||
data: hierarchy
|
||||
});
|
||||
} else {
|
||||
resolve({ success: false, error: 'No scene hierarchy available' });
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
// 备用方案:使用场景脚本
|
||||
const options = {
|
||||
name: 'cocos-mcp-server',
|
||||
method: 'getSceneHierarchy',
|
||||
args: [includeComponents]
|
||||
};
|
||||
|
||||
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
|
||||
resolve(result);
|
||||
}).catch((err2: Error) => {
|
||||
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private buildHierarchy(node: any, includeComponents: boolean): any {
|
||||
const nodeInfo: any = {
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
active: node.active,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (includeComponents && node.__comps__) {
|
||||
nodeInfo.components = node.__comps__.map((comp: any) => ({
|
||||
type: comp.__type__ || 'Unknown',
|
||||
enabled: comp.enabled !== undefined ? comp.enabled : true
|
||||
}));
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
nodeInfo.children = node.children.map((child: any) =>
|
||||
this.buildHierarchy(child, includeComponents)
|
||||
);
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
private async saveSceneAs(path: string): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
// save-as-scene API 不接受路径参数,会弹出对话框让用户选择
|
||||
(Editor.Message.request as any)('scene', 'save-as-scene').then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
path: path,
|
||||
message: `Scene save-as dialog opened`
|
||||
}
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async closeScene(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
Editor.Message.request('scene', 'close-scene').then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: 'Scene closed successfully'
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
247
source/tools/server-tools.ts
Normal file
247
source/tools/server-tools.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
|
||||
|
||||
export class ServerTools implements ToolExecutor {
|
||||
getTools(): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: 'get_server_info',
|
||||
description: 'Get server information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'broadcast_custom_message',
|
||||
description: 'Broadcast a custom message',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Message name'
|
||||
},
|
||||
data: {
|
||||
description: 'Message data (optional)'
|
||||
}
|
||||
},
|
||||
required: ['message']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_editor_version',
|
||||
description: 'Get editor version information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_project_name',
|
||||
description: 'Get current project name',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_project_path',
|
||||
description: 'Get current project path',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_project_uuid',
|
||||
description: 'Get current project UUID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'restart_editor',
|
||||
description: 'Request to restart the editor',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'quit_editor',
|
||||
description: 'Request to quit the editor',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async execute(toolName: string, args: any): Promise<ToolResponse> {
|
||||
switch (toolName) {
|
||||
case 'get_server_info':
|
||||
return await this.getServerInfo();
|
||||
case 'broadcast_custom_message':
|
||||
return await this.broadcastCustomMessage(args.message, args.data);
|
||||
case 'get_editor_version':
|
||||
return await this.getEditorVersion();
|
||||
case 'get_project_name':
|
||||
return await this.getProjectName();
|
||||
case 'get_project_path':
|
||||
return await this.getProjectPath();
|
||||
case 'get_project_uuid':
|
||||
return await this.getProjectUuid();
|
||||
case 'restart_editor':
|
||||
return await this.restartEditor();
|
||||
case 'quit_editor':
|
||||
return await this.quitEditor();
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getServerInfo(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const info = {
|
||||
editorVersion: (Editor as any).versions?.editor || 'Unknown',
|
||||
cocosVersion: (Editor as any).versions?.cocos || 'Unknown',
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
projectName: Editor.Project.name,
|
||||
projectPath: Editor.Project.path,
|
||||
projectUuid: Editor.Project.uuid
|
||||
};
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
server: info,
|
||||
message: 'Server information retrieved successfully'
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async broadcastCustomMessage(message: string, data?: any): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
if (data !== undefined) {
|
||||
Editor.Message.broadcast(message, data);
|
||||
} else {
|
||||
Editor.Message.broadcast(message);
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
message: message,
|
||||
data: data,
|
||||
result: 'Message broadcasted successfully'
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getEditorVersion(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const version = {
|
||||
editor: (Editor as any).versions?.editor || 'Unknown',
|
||||
cocos: (Editor as any).versions?.cocos || 'Unknown',
|
||||
node: process.version
|
||||
};
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
version: version,
|
||||
message: 'Editor version retrieved successfully'
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getProjectName(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const name = Editor.Project.name;
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
name: name,
|
||||
message: 'Project name retrieved successfully'
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getProjectPath(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const path = Editor.Project.path;
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
path: path,
|
||||
message: 'Project path retrieved successfully'
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getProjectUuid(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const uuid = Editor.Project.uuid;
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: uuid,
|
||||
message: 'Project UUID retrieved successfully'
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async restartEditor(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Editor restart is not supported through MCP API',
|
||||
instruction: 'Please restart the editor manually or use the editor menu: File > Restart Editor'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async quitEditor(): Promise<ToolResponse> {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Editor quit is not supported through MCP API',
|
||||
instruction: 'Please quit the editor manually or use the editor menu: File > Quit'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
124
source/types/index.ts
Normal file
124
source/types/index.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
export interface MCPServerSettings {
|
||||
port: number;
|
||||
autoStart: boolean;
|
||||
enableDebugLog: boolean;
|
||||
allowedOrigins: string[];
|
||||
maxConnections: number;
|
||||
}
|
||||
|
||||
export interface ServerStatus {
|
||||
running: boolean;
|
||||
port: number;
|
||||
clients: number;
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: any;
|
||||
}
|
||||
|
||||
export interface ToolResponse {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
message?: string;
|
||||
error?: string;
|
||||
instruction?: string;
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
uuid: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
position?: { x: number; y: number; z: number };
|
||||
rotation?: { x: number; y: number; z: number };
|
||||
scale?: { x: number; y: number; z: number };
|
||||
parent?: string;
|
||||
children?: string[];
|
||||
components?: ComponentInfo[];
|
||||
layer?: number;
|
||||
mobility?: number;
|
||||
}
|
||||
|
||||
export interface ComponentInfo {
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SceneInfo {
|
||||
name: string;
|
||||
uuid: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface PrefabInfo {
|
||||
name: string;
|
||||
uuid: string;
|
||||
path: string;
|
||||
folder: string;
|
||||
createTime?: string;
|
||||
modifyTime?: string;
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
export interface AssetInfo {
|
||||
name: string;
|
||||
uuid: string;
|
||||
path: string;
|
||||
type: string;
|
||||
size?: number;
|
||||
isDirectory: boolean;
|
||||
meta?: {
|
||||
ver: string;
|
||||
importer: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProjectInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
uuid: string;
|
||||
version: string;
|
||||
cocosVersion: string;
|
||||
}
|
||||
|
||||
export interface ConsoleMessage {
|
||||
timestamp: string;
|
||||
type: 'log' | 'warn' | 'error' | 'info';
|
||||
message: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
export interface PerformanceStats {
|
||||
nodeCount: number;
|
||||
componentCount: number;
|
||||
drawCalls: number;
|
||||
triangles: number;
|
||||
memory: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ValidationIssue {
|
||||
type: 'error' | 'warning' | 'info';
|
||||
category: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
suggestion?: string;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
issueCount: number;
|
||||
issues: ValidationIssue[];
|
||||
}
|
||||
|
||||
export interface MCPClient {
|
||||
id: string;
|
||||
lastActivity: Date;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
export interface ToolExecutor {
|
||||
getTools(): ToolDefinition[];
|
||||
execute(toolName: string, args: any): Promise<ToolResponse>;
|
||||
}
|
||||
Reference in New Issue
Block a user