初次提交

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

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