初次提交

This commit is contained in:
root
2025-07-17 18:12:56 +08:00
commit 8781bbf0f5
57 changed files with 15162 additions and 0 deletions

119
source/main.ts Executable file
View 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
View 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

View 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
View 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
View 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
View 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
};

View 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
View 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;
}
}

View 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'
}
});
});
}
}

View 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
View 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
View 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 });
});
});
}
}

View 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
});
}
}

View 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'
});
});
}
}

View 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
View 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 });
});
});
}
}

View 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
View 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>;
}