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. IMPORTANT: You should always provide parentUuid to specify where to create the node. If parentUuid is not provided, the node will be created at the scene root.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Node name' }, parentUuid: { type: 'string', description: 'Parent node UUID. STRONGLY RECOMMENDED: Always provide this parameter. Use get_current_scene or get_all_nodes to find parent UUIDs. If not provided, node will be created at scene root.' }, 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 { 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 { return new Promise(async (resolve) => { let targetParentUuid = args.parentUuid; // 如果没有提供父节点UUID,获取场景根节点 if (!targetParentUuid) { try { const sceneInfo = await Editor.Message.request('scene', 'query-node-tree'); if (sceneInfo && typeof sceneInfo === 'object' && 'uuid' in sceneInfo) { targetParentUuid = sceneInfo.uuid; console.log(`No parent specified, using scene root: ${targetParentUuid}`); } else if (Array.isArray(sceneInfo) && sceneInfo.length > 0 && sceneInfo[0].uuid) { // 如果返回的是数组,使用第一个元素(通常是场景根节点) targetParentUuid = sceneInfo[0].uuid; console.log(`No parent specified, using scene root: ${targetParentUuid}`); } else { // 备用方案:尝试获取当前场景 const currentScene = await Editor.Message.request('scene', 'query-current-scene'); if (currentScene && currentScene.uuid) { targetParentUuid = currentScene.uuid; } } } catch (err) { console.warn('Failed to get scene root, will use default behavior'); } } // 如果指定了父节点,先验证父节点是否存在 if (targetParentUuid) { try { const parentNode = await Editor.Message.request('scene', 'query-node', targetParentUuid); if (!parentNode) { resolve({ success: false, error: `Parent node with UUID '${targetParentUuid}' 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' }; // 使用正确的create-node API参数结构 if (targetParentUuid) { const createNodeOptions = { parent: targetParentUuid, name: args.name, components: args.nodeType && args.nodeType !== 'Node' ? [args.nodeType] : undefined }; Editor.Message.request('scene', 'create-node', createNodeOptions).then((nodeUuid: any) => { // 如果需要设置特定的兄弟索引,使用set-parent API if (args.siblingIndex !== undefined && args.siblingIndex >= 0 && nodeUuid) { Editor.Message.request('scene', 'set-parent', { parent: targetParentUuid, uuids: [nodeUuid], keepWorldTransform: false }).then(() => { resolve({ success: true, data: { uuid: nodeUuid, name: args.name, parentUuid: targetParentUuid, message: args.parentUuid ? `Node '${args.name}' created under specified parent` : `Node '${args.name}' created at scene root (no parent specified)` } }); }).catch(() => { // 即使移动失败,节点已创建,返回成功但带警告 resolve({ success: true, data: { uuid: nodeUuid, name: args.name, message: `Node '${args.name}' created but may not be under intended 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 { // 没有找到场景根节点,使用默认行为(创建在场景根节点) const createNodeOptions = { name: args.name, components: args.nodeType && args.nodeType !== 'Node' ? [args.nodeType] : undefined }; Editor.Message.request('scene', 'create-node', createNodeOptions).then((result: any) => { resolve({ success: true, data: { uuid: result, name: args.name, message: `Node '${args.name}' created at default location (scene root not found)`, warning: 'Could not determine scene root, node created at default location' } }); }).catch((err: Error) => { resolve({ success: false, error: err.message }); }); } }); } private async getNodeInfo(uuid: string): Promise { 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 { return new Promise((resolve) => { // Note: 'query-nodes-by-name' API doesn't exist in official documentation // Using tree traversal as primary approach Editor.Message.request('scene', 'query-node-tree').then((tree: any) => { const nodes: any[] = []; const searchTree = (node: any, currentPath: string = '') => { const nodePath = currentPath ? `${currentPath}/${node.name}` : node.name; const matches = exactMatch ? node.name === pattern : node.name.toLowerCase().includes(pattern.toLowerCase()); if (matches) { nodes.push({ uuid: node.uuid, name: node.name, path: nodePath }); } if (node.children) { for (const child of node.children) { searchTree(child, nodePath); } } }; if (tree) { searchTree(tree); } resolve({ success: true, data: nodes }); }).catch((err: Error) => { // 备用方案:使用场景脚本 const options = { name: 'cocos-mcp-server', method: 'findNodes', args: [pattern, exactMatch] }; Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => { resolve(result); }).catch((err2: Error) => { resolve({ success: false, error: `Tree search failed: ${err.message}, Scene script failed: ${err2.message}` }); }); }); }); } private async findNodeByName(name: string): Promise { 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 { 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 { 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 { 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 { return new Promise((resolve) => { // Use correct set-parent API instead of move-node Editor.Message.request('scene', 'set-parent', { parent: newParentUuid, uuids: [nodeUuid], keepWorldTransform: false }).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 { return new Promise((resolve) => { // Note: includeChildren parameter is accepted for future use but not currently implemented 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 }); }); }); } }