This is page 2 of 3. Use http://codebase.md/daxianlee/cocos-mcp-server?page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── @types
│ └── schema
│ └── package
│ ├── base
│ │ └── panels.json
│ ├── contributions
│ │ └── index.json
│ └── index.json
├── base.tsconfig.json
├── dist
│ ├── examples
│ │ └── prefab-instantiation-example.js
│ ├── main.js
│ ├── mcp-server.js
│ ├── panels
│ │ ├── default
│ │ │ └── index.js
│ │ └── tool-manager
│ │ └── index.js
│ ├── scene.js
│ ├── settings.js
│ ├── test
│ │ ├── manual-test.js
│ │ ├── mcp-tool-tester.js
│ │ ├── prefab-tools-test.js
│ │ └── tool-tester.js
│ ├── tools
│ │ ├── asset-advanced-tools.js
│ │ ├── broadcast-tools.js
│ │ ├── component-tools.js
│ │ ├── debug-tools.js
│ │ ├── node-tools.js
│ │ ├── prefab-tools.js
│ │ ├── preferences-tools.js
│ │ ├── project-tools.js
│ │ ├── reference-image-tools.js
│ │ ├── scene-advanced-tools.js
│ │ ├── scene-tools.js
│ │ ├── scene-view-tools.js
│ │ ├── server-tools.js
│ │ ├── tool-manager.js
│ │ └── validation-tools.js
│ └── types
│ └── index.js
├── FEATURE_GUIDE_CN.md
├── FEATURE_GUIDE_EN.md
├── i18n
│ ├── en.js
│ └── zh.js
├── image
│ ├── iamge2.png
│ └── image-20250717174157957.png
├── package-lock.json
├── package.json
├── README.EN.md
├── README.md
├── scripts
│ └── preinstall.js
├── source
│ ├── main.ts
│ ├── mcp-server.ts
│ ├── panels
│ │ ├── default
│ │ │ └── index.ts
│ │ └── tool-manager
│ │ └── index.ts
│ ├── scene.ts
│ ├── settings.ts
│ ├── test
│ │ ├── manual-test.ts
│ │ ├── mcp-tool-tester.ts
│ │ ├── prefab-tools-test.ts
│ │ └── tool-tester.ts
│ ├── tools
│ │ ├── asset-advanced-tools.ts
│ │ ├── broadcast-tools.ts
│ │ ├── component-tools.ts
│ │ ├── debug-tools.ts
│ │ ├── node-tools.ts
│ │ ├── prefab-tools.ts
│ │ ├── preferences-tools.ts
│ │ ├── project-tools.ts
│ │ ├── reference-image-tools.ts
│ │ ├── scene-advanced-tools.ts
│ │ ├── scene-tools.ts
│ │ ├── scene-view-tools.ts
│ │ ├── server-tools.ts
│ │ ├── tool-manager.ts
│ │ └── validation-tools.ts
│ └── types
│ └── index.ts
├── static
│ ├── icon.png
│ ├── style
│ │ └── default
│ │ └── index.css
│ └── template
│ ├── default
│ │ ├── index.html
│ │ └── tool-manager.html
│ └── vue
│ └── mcp-server-app.html
├── TestScript.js
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/source/tools/scene-view-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
export class SceneViewTools implements ToolExecutor {
getTools(): ToolDefinition[] {
return [
{
name: 'change_gizmo_tool',
description: 'Change Gizmo tool',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Tool name',
enum: ['position', 'rotation', 'scale', 'rect']
}
},
required: ['name']
}
},
{
name: 'query_gizmo_tool_name',
description: 'Get current Gizmo tool name',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'change_gizmo_pivot',
description: 'Change transform pivot point',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Pivot point',
enum: ['pivot', 'center']
}
},
required: ['name']
}
},
{
name: 'query_gizmo_pivot',
description: 'Get current Gizmo pivot point',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'query_gizmo_view_mode',
description: 'Query view mode (view/select)',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'change_gizmo_coordinate',
description: 'Change coordinate system',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Coordinate system',
enum: ['local', 'global']
}
},
required: ['type']
}
},
{
name: 'query_gizmo_coordinate',
description: 'Get current coordinate system',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'change_view_mode_2d_3d',
description: 'Change 2D/3D view mode',
inputSchema: {
type: 'object',
properties: {
is2D: {
type: 'boolean',
description: '2D/3D view mode (true for 2D, false for 3D)'
}
},
required: ['is2D']
}
},
{
name: 'query_view_mode_2d_3d',
description: 'Get current view mode',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'set_grid_visible',
description: 'Show/hide grid',
inputSchema: {
type: 'object',
properties: {
visible: {
type: 'boolean',
description: 'Grid visibility'
}
},
required: ['visible']
}
},
{
name: 'query_grid_visible',
description: 'Query grid visibility status',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'set_icon_gizmo_3d',
description: 'Set IconGizmo to 3D or 2D mode',
inputSchema: {
type: 'object',
properties: {
is3D: {
type: 'boolean',
description: '3D/2D IconGizmo (true for 3D, false for 2D)'
}
},
required: ['is3D']
}
},
{
name: 'query_icon_gizmo_3d',
description: 'Query IconGizmo mode',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'set_icon_gizmo_size',
description: 'Set IconGizmo size',
inputSchema: {
type: 'object',
properties: {
size: {
type: 'number',
description: 'IconGizmo size',
minimum: 10,
maximum: 100
}
},
required: ['size']
}
},
{
name: 'query_icon_gizmo_size',
description: 'Query IconGizmo size',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'focus_camera_on_nodes',
description: 'Focus scene camera on nodes',
inputSchema: {
type: 'object',
properties: {
uuids: {
oneOf: [
{ type: 'array', items: { type: 'string' } },
{ type: 'null' }
],
description: 'Node UUIDs to focus on (null for all)'
}
},
required: ['uuids']
}
},
{
name: 'align_camera_with_view',
description: 'Apply scene camera position and angle to selected node',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'align_view_with_node',
description: 'Apply selected node position and angle to current view',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_scene_view_status',
description: 'Get comprehensive scene view status',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'reset_scene_view',
description: 'Reset scene view to default settings',
inputSchema: {
type: 'object',
properties: {}
}
}
];
}
async execute(toolName: string, args: any): Promise<ToolResponse> {
switch (toolName) {
case 'change_gizmo_tool':
return await this.changeGizmoTool(args.name);
case 'query_gizmo_tool_name':
return await this.queryGizmoToolName();
case 'change_gizmo_pivot':
return await this.changeGizmoPivot(args.name);
case 'query_gizmo_pivot':
return await this.queryGizmoPivot();
case 'query_gizmo_view_mode':
return await this.queryGizmoViewMode();
case 'change_gizmo_coordinate':
return await this.changeGizmoCoordinate(args.type);
case 'query_gizmo_coordinate':
return await this.queryGizmoCoordinate();
case 'change_view_mode_2d_3d':
return await this.changeViewMode2D3D(args.is2D);
case 'query_view_mode_2d_3d':
return await this.queryViewMode2D3D();
case 'set_grid_visible':
return await this.setGridVisible(args.visible);
case 'query_grid_visible':
return await this.queryGridVisible();
case 'set_icon_gizmo_3d':
return await this.setIconGizmo3D(args.is3D);
case 'query_icon_gizmo_3d':
return await this.queryIconGizmo3D();
case 'set_icon_gizmo_size':
return await this.setIconGizmoSize(args.size);
case 'query_icon_gizmo_size':
return await this.queryIconGizmoSize();
case 'focus_camera_on_nodes':
return await this.focusCameraOnNodes(args.uuids);
case 'align_camera_with_view':
return await this.alignCameraWithView();
case 'align_view_with_node':
return await this.alignViewWithNode();
case 'get_scene_view_status':
return await this.getSceneViewStatus();
case 'reset_scene_view':
return await this.resetSceneView();
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
private async changeGizmoTool(name: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'change-gizmo-tool', name).then(() => {
resolve({
success: true,
message: `Gizmo tool changed to '${name}'`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryGizmoToolName(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-gizmo-tool-name').then((toolName: string) => {
resolve({
success: true,
data: {
currentTool: toolName,
message: `Current Gizmo tool: ${toolName}`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async changeGizmoPivot(name: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'change-gizmo-pivot', name).then(() => {
resolve({
success: true,
message: `Gizmo pivot changed to '${name}'`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryGizmoPivot(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-gizmo-pivot').then((pivotName: string) => {
resolve({
success: true,
data: {
currentPivot: pivotName,
message: `Current Gizmo pivot: ${pivotName}`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryGizmoViewMode(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-gizmo-view-mode').then((viewMode: string) => {
resolve({
success: true,
data: {
viewMode: viewMode,
message: `Current view mode: ${viewMode}`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async changeGizmoCoordinate(type: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'change-gizmo-coordinate', type).then(() => {
resolve({
success: true,
message: `Coordinate system changed to '${type}'`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryGizmoCoordinate(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-gizmo-coordinate').then((coordinate: string) => {
resolve({
success: true,
data: {
coordinate: coordinate,
message: `Current coordinate system: ${coordinate}`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async changeViewMode2D3D(is2D: boolean): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'change-is2D', is2D).then(() => {
resolve({
success: true,
message: `View mode changed to ${is2D ? '2D' : '3D'}`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryViewMode2D3D(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-is2D').then((is2D: boolean) => {
resolve({
success: true,
data: {
is2D: is2D,
viewMode: is2D ? '2D' : '3D',
message: `Current view mode: ${is2D ? '2D' : '3D'}`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async setGridVisible(visible: boolean): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'set-grid-visible', visible).then(() => {
resolve({
success: true,
message: `Grid ${visible ? 'shown' : 'hidden'}`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryGridVisible(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-is-grid-visible').then((visible: boolean) => {
resolve({
success: true,
data: {
visible: visible,
message: `Grid is ${visible ? 'visible' : 'hidden'}`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async setIconGizmo3D(is3D: boolean): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'set-icon-gizmo-3d', is3D).then(() => {
resolve({
success: true,
message: `IconGizmo set to ${is3D ? '3D' : '2D'} mode`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryIconGizmo3D(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-is-icon-gizmo-3d').then((is3D: boolean) => {
resolve({
success: true,
data: {
is3D: is3D,
mode: is3D ? '3D' : '2D',
message: `IconGizmo is in ${is3D ? '3D' : '2D'} mode`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async setIconGizmoSize(size: number): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'set-icon-gizmo-size', size).then(() => {
resolve({
success: true,
message: `IconGizmo size set to ${size}`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryIconGizmoSize(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-icon-gizmo-size').then((size: number) => {
resolve({
success: true,
data: {
size: size,
message: `IconGizmo size: ${size}`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async focusCameraOnNodes(uuids: string[] | null): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'focus-camera', uuids || []).then(() => {
const message = uuids === null ?
'Camera focused on all nodes' :
`Camera focused on ${uuids.length} node(s)`;
resolve({
success: true,
message: message
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async alignCameraWithView(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'align-with-view').then(() => {
resolve({
success: true,
message: 'Scene camera aligned with current view'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async alignViewWithNode(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'align-with-view-node').then(() => {
resolve({
success: true,
message: 'View aligned with selected node'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async getSceneViewStatus(): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
// Gather all view status information
const [
gizmoTool,
gizmoPivot,
gizmoCoordinate,
viewMode2D3D,
gridVisible,
iconGizmo3D,
iconGizmoSize
] = await Promise.allSettled([
this.queryGizmoToolName(),
this.queryGizmoPivot(),
this.queryGizmoCoordinate(),
this.queryViewMode2D3D(),
this.queryGridVisible(),
this.queryIconGizmo3D(),
this.queryIconGizmoSize()
]);
const status: any = {
timestamp: new Date().toISOString()
};
// Extract data from fulfilled promises
if (gizmoTool.status === 'fulfilled' && gizmoTool.value.success) {
status.gizmoTool = gizmoTool.value.data.currentTool;
}
if (gizmoPivot.status === 'fulfilled' && gizmoPivot.value.success) {
status.gizmoPivot = gizmoPivot.value.data.currentPivot;
}
if (gizmoCoordinate.status === 'fulfilled' && gizmoCoordinate.value.success) {
status.coordinate = gizmoCoordinate.value.data.coordinate;
}
if (viewMode2D3D.status === 'fulfilled' && viewMode2D3D.value.success) {
status.is2D = viewMode2D3D.value.data.is2D;
status.viewMode = viewMode2D3D.value.data.viewMode;
}
if (gridVisible.status === 'fulfilled' && gridVisible.value.success) {
status.gridVisible = gridVisible.value.data.visible;
}
if (iconGizmo3D.status === 'fulfilled' && iconGizmo3D.value.success) {
status.iconGizmo3D = iconGizmo3D.value.data.is3D;
}
if (iconGizmoSize.status === 'fulfilled' && iconGizmoSize.value.success) {
status.iconGizmoSize = iconGizmoSize.value.data.size;
}
resolve({
success: true,
data: status
});
} catch (err: any) {
resolve({
success: false,
error: `Failed to get scene view status: ${err.message}`
});
}
});
}
private async resetSceneView(): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
// Reset scene view to default settings
const resetActions = [
this.changeGizmoTool('position'),
this.changeGizmoPivot('pivot'),
this.changeGizmoCoordinate('local'),
this.changeViewMode2D3D(false), // 3D mode
this.setGridVisible(true),
this.setIconGizmo3D(true),
this.setIconGizmoSize(60)
];
await Promise.all(resetActions);
resolve({
success: true,
message: 'Scene view reset to default settings'
});
} catch (err: any) {
resolve({
success: false,
error: `Failed to reset scene view: ${err.message}`
});
}
});
}
}
```
--------------------------------------------------------------------------------
/source/panels/tool-manager/index.ts:
--------------------------------------------------------------------------------
```typescript
import { readFileSync } from 'fs-extra';
import { join } from 'path';
module.exports = Editor.Panel.define({
listeners: {
show() { console.log('Tool Manager panel shown'); },
hide() { console.log('Tool Manager panel hidden'); }
},
template: readFileSync(join(__dirname, '../../../static/template/default/tool-manager.html'), 'utf-8'),
style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
$: {
panelTitle: '#panelTitle',
createConfigBtn: '#createConfigBtn',
importConfigBtn: '#importConfigBtn',
exportConfigBtn: '#exportConfigBtn',
configSelector: '#configSelector',
applyConfigBtn: '#applyConfigBtn',
editConfigBtn: '#editConfigBtn',
deleteConfigBtn: '#deleteConfigBtn',
toolsContainer: '#toolsContainer',
selectAllBtn: '#selectAllBtn',
deselectAllBtn: '#deselectAllBtn',
saveChangesBtn: '#saveChangesBtn',
totalToolsCount: '#totalToolsCount',
enabledToolsCount: '#enabledToolsCount',
disabledToolsCount: '#disabledToolsCount',
configModal: '#configModal',
modalTitle: '#modalTitle',
configForm: '#configForm',
configName: '#configName',
configDescription: '#configDescription',
closeModal: '#closeModal',
cancelConfigBtn: '#cancelConfigBtn',
saveConfigBtn: '#saveConfigBtn',
importModal: '#importModal',
importConfigJson: '#importConfigJson',
closeImportModal: '#closeImportModal',
cancelImportBtn: '#cancelImportBtn',
confirmImportBtn: '#confirmImportBtn'
},
methods: {
async loadToolManagerState(this: any) {
try {
this.toolManagerState = await Editor.Message.request('cocos-mcp-server', 'getToolManagerState');
this.currentConfiguration = this.toolManagerState.currentConfiguration;
this.configurations = this.toolManagerState.configurations;
this.availableTools = this.toolManagerState.availableTools;
this.updateUI();
} catch (error) {
console.error('Failed to load tool manager state:', error);
this.showError('加载工具管理器状态失败');
}
},
updateUI(this: any) {
this.updateConfigSelector();
this.updateToolsDisplay();
this.updateStatusBar();
this.updateButtons();
},
updateConfigSelector(this: any) {
const selector = this.$.configSelector;
selector.innerHTML = '<option value="">选择配置...</option>';
this.configurations.forEach((config: any) => {
const option = document.createElement('option');
option.value = config.id;
option.textContent = config.name;
if (this.currentConfiguration && config.id === this.currentConfiguration.id) {
option.selected = true;
}
selector.appendChild(option);
});
},
updateToolsDisplay(this: any) {
const container = this.$.toolsContainer;
if (!this.currentConfiguration) {
container.innerHTML = `
<div class="empty-state">
<h3>没有选择配置</h3>
<p>请先选择一个配置或创建新配置</p>
</div>
`;
return;
}
const toolsByCategory: any = {};
this.currentConfiguration.tools.forEach((tool: any) => {
if (!toolsByCategory[tool.category]) {
toolsByCategory[tool.category] = [];
}
toolsByCategory[tool.category].push(tool);
});
container.innerHTML = '';
Object.entries(toolsByCategory).forEach(([category, tools]: [string, any]) => {
const categoryDiv = document.createElement('div');
categoryDiv.className = 'tool-category';
const enabledCount = tools.filter((t: any) => t.enabled).length;
const totalCount = tools.length;
categoryDiv.innerHTML = `
<div class="category-header">
<div class="category-name">${this.getCategoryDisplayName(category)}</div>
<div class="category-toggle">
<span>${enabledCount}/${totalCount}</span>
<input type="checkbox" class="checkbox category-checkbox"
data-category="${category}"
${enabledCount === totalCount ? 'checked' : ''}>
</div>
</div>
<div class="tool-list">
${tools.map((tool: any) => `
<div class="tool-item">
<div class="tool-info">
<div class="tool-name">${tool.name}</div>
<div class="tool-description">${tool.description}</div>
</div>
<div class="tool-toggle">
<input type="checkbox" class="checkbox tool-checkbox"
data-category="${tool.category}"
data-name="${tool.name}"
${tool.enabled ? 'checked' : ''}>
</div>
</div>
`).join('')}
</div>
`;
container.appendChild(categoryDiv);
});
this.bindToolEvents();
},
bindToolEvents(this: any) {
document.querySelectorAll('.category-checkbox').forEach((checkbox: any) => {
checkbox.addEventListener('change', (e: any) => {
const category = e.target.dataset.category;
const checked = e.target.checked;
this.toggleCategoryTools(category, checked);
});
});
document.querySelectorAll('.tool-checkbox').forEach((checkbox: any) => {
checkbox.addEventListener('change', (e: any) => {
const category = e.target.dataset.category;
const name = e.target.dataset.name;
const enabled = e.target.checked;
this.updateToolStatus(category, name, enabled);
});
});
},
async toggleCategoryTools(this: any, category: string, enabled: boolean) {
if (!this.currentConfiguration) return;
console.log(`Toggling category tools: ${category} = ${enabled}`);
const categoryTools = this.currentConfiguration.tools.filter((tool: any) => tool.category === category);
if (categoryTools.length === 0) return;
const updates = categoryTools.map((tool: any) => ({
category: tool.category,
name: tool.name,
enabled: enabled
}));
try {
// 先更新本地状态
categoryTools.forEach((tool: any) => {
tool.enabled = enabled;
});
console.log(`Updated local category state: ${category} = ${enabled}`);
// 立即更新UI
this.updateStatusBar();
this.updateCategoryCounts();
this.updateToolCheckboxes(category, enabled);
// 然后发送到后端
await Editor.Message.request('cocos-mcp-server', 'updateToolStatusBatch',
this.currentConfiguration.id, updates);
} catch (error) {
console.error('Failed to toggle category tools:', error);
this.showError('切换类别工具失败');
// 如果后端更新失败,回滚本地状态
categoryTools.forEach((tool: any) => {
tool.enabled = !enabled;
});
this.updateStatusBar();
this.updateCategoryCounts();
this.updateToolCheckboxes(category, !enabled);
}
},
async updateToolStatus(this: any, category: string, name: string, enabled: boolean) {
if (!this.currentConfiguration) return;
console.log(`Updating tool status: ${category}.${name} = ${enabled}`);
console.log(`Current config ID: ${this.currentConfiguration.id}`);
// 先更新本地状态
const tool = this.currentConfiguration.tools.find((t: any) =>
t.category === category && t.name === name);
if (!tool) {
console.error(`Tool not found: ${category}.${name}`);
return;
}
try {
tool.enabled = enabled;
console.log(`Updated local tool state: ${tool.name} = ${tool.enabled}`);
// 立即更新UI(只更新统计信息,不重新渲染工具列表)
this.updateStatusBar();
this.updateCategoryCounts();
// 然后发送到后端
console.log(`Sending to backend: configId=${this.currentConfiguration.id}, category=${category}, name=${name}, enabled=${enabled}`);
const result = await Editor.Message.request('cocos-mcp-server', 'updateToolStatus',
this.currentConfiguration.id, category, name, enabled);
console.log('Backend response:', result);
} catch (error) {
console.error('Failed to update tool status:', error);
this.showError('更新工具状态失败');
// 如果后端更新失败,回滚本地状态
tool.enabled = !enabled;
this.updateStatusBar();
this.updateCategoryCounts();
}
},
updateStatusBar(this: any) {
if (!this.currentConfiguration) {
this.$.totalToolsCount.textContent = '0';
this.$.enabledToolsCount.textContent = '0';
this.$.disabledToolsCount.textContent = '0';
return;
}
const total = this.currentConfiguration.tools.length;
const enabled = this.currentConfiguration.tools.filter((t: any) => t.enabled).length;
const disabled = total - enabled;
console.log(`Status bar update: total=${total}, enabled=${enabled}, disabled=${disabled}`);
this.$.totalToolsCount.textContent = total.toString();
this.$.enabledToolsCount.textContent = enabled.toString();
this.$.disabledToolsCount.textContent = disabled.toString();
},
updateCategoryCounts(this: any) {
if (!this.currentConfiguration) return;
// 更新每个类别的计数显示
document.querySelectorAll('.category-checkbox').forEach((checkbox: any) => {
const category = checkbox.dataset.category;
const categoryTools = this.currentConfiguration.tools.filter((t: any) => t.category === category);
const enabledCount = categoryTools.filter((t: any) => t.enabled).length;
const totalCount = categoryTools.length;
// 更新计数显示
const countSpan = checkbox.parentElement.querySelector('span');
if (countSpan) {
countSpan.textContent = `${enabledCount}/${totalCount}`;
}
// 更新类别复选框状态
checkbox.checked = enabledCount === totalCount;
});
},
updateToolCheckboxes(this: any, category: string, enabled: boolean) {
// 更新特定类别的所有工具复选框
document.querySelectorAll(`.tool-checkbox[data-category="${category}"]`).forEach((checkbox: any) => {
checkbox.checked = enabled;
});
},
updateButtons(this: any) {
const hasCurrentConfig = !!this.currentConfiguration;
this.$.editConfigBtn.disabled = !hasCurrentConfig;
this.$.deleteConfigBtn.disabled = !hasCurrentConfig;
this.$.exportConfigBtn.disabled = !hasCurrentConfig;
this.$.applyConfigBtn.disabled = !hasCurrentConfig;
},
async createConfiguration(this: any) {
this.editingConfig = null;
this.$.modalTitle.textContent = '新建配置';
this.$.configName.value = '';
this.$.configDescription.value = '';
this.showModal('configModal');
},
async editConfiguration(this: any) {
if (!this.currentConfiguration) return;
this.editingConfig = this.currentConfiguration;
this.$.modalTitle.textContent = '编辑配置';
this.$.configName.value = this.currentConfiguration.name;
this.$.configDescription.value = this.currentConfiguration.description || '';
this.showModal('configModal');
},
async saveConfiguration(this: any) {
const name = this.$.configName.value.trim();
const description = this.$.configDescription.value.trim();
if (!name) {
this.showError('配置名称不能为空');
return;
}
try {
if (this.editingConfig) {
await Editor.Message.request('cocos-mcp-server', 'updateToolConfiguration',
this.editingConfig.id, { name, description });
} else {
await Editor.Message.request('cocos-mcp-server', 'createToolConfiguration', name, description);
}
this.hideModal('configModal');
await this.loadToolManagerState();
} catch (error) {
console.error('Failed to save configuration:', error);
this.showError('保存配置失败');
}
},
async deleteConfiguration(this: any) {
if (!this.currentConfiguration) return;
const confirmed = await Editor.Dialog.warn('确认删除', {
detail: `确定要删除配置 "${this.currentConfiguration.name}" 吗?此操作不可撤销。`
});
if (confirmed) {
try {
await Editor.Message.request('cocos-mcp-server', 'deleteToolConfiguration',
this.currentConfiguration.id);
await this.loadToolManagerState();
} catch (error) {
console.error('Failed to delete configuration:', error);
this.showError('删除配置失败');
}
}
},
async applyConfiguration(this: any) {
const configId = this.$.configSelector.value;
if (!configId) return;
try {
await Editor.Message.request('cocos-mcp-server', 'setCurrentToolConfiguration', configId);
await this.loadToolManagerState();
} catch (error) {
console.error('Failed to apply configuration:', error);
this.showError('应用配置失败');
}
},
async exportConfiguration(this: any) {
if (!this.currentConfiguration) return;
try {
const result = await Editor.Message.request('cocos-mcp-server', 'exportToolConfiguration',
this.currentConfiguration.id);
Editor.Clipboard.write('text', result.configJson);
Editor.Dialog.info('导出成功', { detail: '配置已复制到剪贴板' });
} catch (error) {
console.error('Failed to export configuration:', error);
this.showError('导出配置失败');
}
},
async importConfiguration(this: any) {
this.$.importConfigJson.value = '';
this.showModal('importModal');
},
async confirmImport(this: any) {
const configJson = this.$.importConfigJson.value.trim();
if (!configJson) {
this.showError('请输入配置JSON');
return;
}
try {
await Editor.Message.request('cocos-mcp-server', 'importToolConfiguration', configJson);
this.hideModal('importModal');
await this.loadToolManagerState();
Editor.Dialog.info('导入成功', { detail: '配置已成功导入' });
} catch (error) {
console.error('Failed to import configuration:', error);
this.showError('导入配置失败');
}
},
async selectAllTools(this: any) {
if (!this.currentConfiguration) return;
console.log('Selecting all tools');
const updates = this.currentConfiguration.tools.map((tool: any) => ({
category: tool.category,
name: tool.name,
enabled: true
}));
try {
// 先更新本地状态
this.currentConfiguration.tools.forEach((tool: any) => {
tool.enabled = true;
});
console.log('Updated local state: all tools enabled');
// 立即更新UI
this.updateStatusBar();
this.updateToolsDisplay();
// 然后发送到后端
await Editor.Message.request('cocos-mcp-server', 'updateToolStatusBatch',
this.currentConfiguration.id, updates);
} catch (error) {
console.error('Failed to select all tools:', error);
this.showError('全选工具失败');
// 如果后端更新失败,回滚本地状态
this.currentConfiguration.tools.forEach((tool: any) => {
tool.enabled = false;
});
this.updateStatusBar();
this.updateToolsDisplay();
}
},
async deselectAllTools(this: any) {
if (!this.currentConfiguration) return;
console.log('Deselecting all tools');
const updates = this.currentConfiguration.tools.map((tool: any) => ({
category: tool.category,
name: tool.name,
enabled: false
}));
try {
// 先更新本地状态
this.currentConfiguration.tools.forEach((tool: any) => {
tool.enabled = false;
});
console.log('Updated local state: all tools disabled');
// 立即更新UI
this.updateStatusBar();
this.updateToolsDisplay();
// 然后发送到后端
await Editor.Message.request('cocos-mcp-server', 'updateToolStatusBatch',
this.currentConfiguration.id, updates);
} catch (error) {
console.error('Failed to deselect all tools:', error);
this.showError('取消全选工具失败');
// 如果后端更新失败,回滚本地状态
this.currentConfiguration.tools.forEach((tool: any) => {
tool.enabled = true;
});
this.updateStatusBar();
this.updateToolsDisplay();
}
},
getCategoryDisplayName(this: any, category: string): string {
const categoryNames: any = {
'scene': '场景工具',
'node': '节点工具',
'component': '组件工具',
'prefab': '预制体工具',
'project': '项目工具',
'debug': '调试工具',
'preferences': '偏好设置工具',
'server': '服务器工具',
'broadcast': '广播工具',
'sceneAdvanced': '高级场景工具',
'sceneView': '场景视图工具',
'referenceImage': '参考图片工具',
'assetAdvanced': '高级资源工具',
'validation': '验证工具'
};
return categoryNames[category] || category;
},
showModal(this: any, modalId: string) {
this.$[modalId].style.display = 'block';
},
hideModal(this: any, modalId: string) {
this.$[modalId].style.display = 'none';
},
showError(this: any, message: string) {
Editor.Dialog.error('错误', { detail: message });
},
async saveChanges(this: any) {
if (!this.currentConfiguration) {
this.showError('没有选择配置');
return;
}
try {
// 确保当前配置已保存到后端
await Editor.Message.request('cocos-mcp-server', 'updateToolConfiguration',
this.currentConfiguration.id, {
name: this.currentConfiguration.name,
description: this.currentConfiguration.description,
tools: this.currentConfiguration.tools
});
Editor.Dialog.info('保存成功', { detail: '配置更改已保存' });
} catch (error) {
console.error('Failed to save changes:', error);
this.showError('保存更改失败');
}
},
bindEvents(this: any) {
this.$.createConfigBtn.addEventListener('click', this.createConfiguration.bind(this));
this.$.editConfigBtn.addEventListener('click', this.editConfiguration.bind(this));
this.$.deleteConfigBtn.addEventListener('click', this.deleteConfiguration.bind(this));
this.$.applyConfigBtn.addEventListener('click', this.applyConfiguration.bind(this));
this.$.exportConfigBtn.addEventListener('click', this.exportConfiguration.bind(this));
this.$.importConfigBtn.addEventListener('click', this.importConfiguration.bind(this));
this.$.selectAllBtn.addEventListener('click', this.selectAllTools.bind(this));
this.$.deselectAllBtn.addEventListener('click', this.deselectAllTools.bind(this));
this.$.saveChangesBtn.addEventListener('click', this.saveChanges.bind(this));
this.$.closeModal.addEventListener('click', () => this.hideModal('configModal'));
this.$.cancelConfigBtn.addEventListener('click', () => this.hideModal('configModal'));
this.$.configForm.addEventListener('submit', (e: any) => {
e.preventDefault();
this.saveConfiguration();
});
this.$.closeImportModal.addEventListener('click', () => this.hideModal('importModal'));
this.$.cancelImportBtn.addEventListener('click', () => this.hideModal('importModal'));
this.$.confirmImportBtn.addEventListener('click', this.confirmImport.bind(this));
this.$.configSelector.addEventListener('change', this.applyConfiguration.bind(this));
}
},
ready() {
(this as any).toolManagerState = null;
(this as any).currentConfiguration = null;
(this as any).configurations = [];
(this as any).availableTools = [];
(this as any).editingConfig = null;
(this as any).bindEvents();
(this as any).loadToolManagerState();
},
beforeClose() {
// 清理工作
},
close() {
// 面板关闭清理
}
} as any);
```
--------------------------------------------------------------------------------
/source/tools/debug-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolDefinition, ToolResponse, ToolExecutor, ConsoleMessage, PerformanceStats, ValidationResult, ValidationIssue } from '../types';
import * as fs from 'fs';
import * as path from 'path';
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: {}
}
},
{
name: 'get_project_logs',
description: 'Get project logs from temp/logs/project.log file',
inputSchema: {
type: 'object',
properties: {
lines: {
type: 'number',
description: 'Number of lines to read from the end of the log file (default: 100)',
default: 100,
minimum: 1,
maximum: 10000
},
filterKeyword: {
type: 'string',
description: 'Filter logs containing specific keyword (optional)'
},
logLevel: {
type: 'string',
description: 'Filter by log level',
enum: ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL'],
default: 'ALL'
}
}
}
},
{
name: 'get_log_file_info',
description: 'Get information about the project log file',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'search_project_logs',
description: 'Search for specific patterns or errors in project logs',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Search pattern (supports regex)'
},
maxResults: {
type: 'number',
description: 'Maximum number of matching results',
default: 20,
minimum: 1,
maximum: 100
},
contextLines: {
type: 'number',
description: 'Number of context lines to show around each match',
default: 2,
minimum: 0,
maximum: 10
}
},
required: ['pattern']
}
}
];
}
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();
case 'get_project_logs':
return await this.getProjectLogs(args.lines, args.filterKeyword, args.logLevel);
case 'get_log_file_info':
return await this.getLogFileInfo();
case 'search_project_logs':
return await this.searchProjectLogs(args.pattern, args.maxResults, args.contextLines);
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-scene-script', {
name: 'console',
method: 'eval',
args: [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 };
}
private async getProjectLogs(lines: number = 100, filterKeyword?: string, logLevel: string = 'ALL'): Promise<ToolResponse> {
try {
// Try multiple possible project paths
let logFilePath = '';
const possiblePaths = [
Editor.Project ? Editor.Project.path : null,
'/Users/lizhiyong/NewProject_3',
process.cwd(),
].filter(p => p !== null);
for (const basePath of possiblePaths) {
const testPath = path.join(basePath, 'temp/logs/project.log');
if (fs.existsSync(testPath)) {
logFilePath = testPath;
break;
}
}
if (!logFilePath) {
return {
success: false,
error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
};
}
// Read the file content
const logContent = fs.readFileSync(logFilePath, 'utf8');
const logLines = logContent.split('\n').filter(line => line.trim() !== '');
// Get the last N lines
const recentLines = logLines.slice(-lines);
// Apply filters
let filteredLines = recentLines;
// Filter by log level if not 'ALL'
if (logLevel !== 'ALL') {
filteredLines = filteredLines.filter(line =>
line.includes(`[${logLevel}]`) || line.includes(logLevel.toLowerCase())
);
}
// Filter by keyword if provided
if (filterKeyword) {
filteredLines = filteredLines.filter(line =>
line.toLowerCase().includes(filterKeyword.toLowerCase())
);
}
return {
success: true,
data: {
totalLines: logLines.length,
requestedLines: lines,
filteredLines: filteredLines.length,
logLevel: logLevel,
filterKeyword: filterKeyword || null,
logs: filteredLines,
logFilePath: logFilePath
}
};
} catch (error: any) {
return {
success: false,
error: `Failed to read project logs: ${error.message}`
};
}
}
private async getLogFileInfo(): Promise<ToolResponse> {
try {
// Try multiple possible project paths
let logFilePath = '';
const possiblePaths = [
Editor.Project ? Editor.Project.path : null,
'/Users/lizhiyong/NewProject_3',
process.cwd(),
].filter(p => p !== null);
for (const basePath of possiblePaths) {
const testPath = path.join(basePath, 'temp/logs/project.log');
if (fs.existsSync(testPath)) {
logFilePath = testPath;
break;
}
}
if (!logFilePath) {
return {
success: false,
error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
};
}
const stats = fs.statSync(logFilePath);
const logContent = fs.readFileSync(logFilePath, 'utf8');
const lineCount = logContent.split('\n').filter(line => line.trim() !== '').length;
return {
success: true,
data: {
filePath: logFilePath,
fileSize: stats.size,
fileSizeFormatted: this.formatFileSize(stats.size),
lastModified: stats.mtime.toISOString(),
lineCount: lineCount,
created: stats.birthtime.toISOString(),
accessible: fs.constants.R_OK
}
};
} catch (error: any) {
return {
success: false,
error: `Failed to get log file info: ${error.message}`
};
}
}
private async searchProjectLogs(pattern: string, maxResults: number = 20, contextLines: number = 2): Promise<ToolResponse> {
try {
// Try multiple possible project paths
let logFilePath = '';
const possiblePaths = [
Editor.Project ? Editor.Project.path : null,
'/Users/lizhiyong/NewProject_3',
process.cwd(),
].filter(p => p !== null);
for (const basePath of possiblePaths) {
const testPath = path.join(basePath, 'temp/logs/project.log');
if (fs.existsSync(testPath)) {
logFilePath = testPath;
break;
}
}
if (!logFilePath) {
return {
success: false,
error: `Project log file not found. Tried paths: ${possiblePaths.map(p => path.join(p, 'temp/logs/project.log')).join(', ')}`
};
}
const logContent = fs.readFileSync(logFilePath, 'utf8');
const logLines = logContent.split('\n');
// Create regex pattern (support both string and regex patterns)
let regex: RegExp;
try {
regex = new RegExp(pattern, 'gi');
} catch {
// If pattern is not valid regex, treat as literal string
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
}
const matches: any[] = [];
let resultCount = 0;
for (let i = 0; i < logLines.length && resultCount < maxResults; i++) {
const line = logLines[i];
if (regex.test(line)) {
// Get context lines
const contextStart = Math.max(0, i - contextLines);
const contextEnd = Math.min(logLines.length - 1, i + contextLines);
const contextLinesArray = [];
for (let j = contextStart; j <= contextEnd; j++) {
contextLinesArray.push({
lineNumber: j + 1,
content: logLines[j],
isMatch: j === i
});
}
matches.push({
lineNumber: i + 1,
matchedLine: line,
context: contextLinesArray
});
resultCount++;
// Reset regex lastIndex for global search
regex.lastIndex = 0;
}
}
return {
success: true,
data: {
pattern: pattern,
totalMatches: matches.length,
maxResults: maxResults,
contextLines: contextLines,
logFilePath: logFilePath,
matches: matches
}
};
} catch (error: any) {
return {
success: false,
error: `Failed to search project logs: ${error.message}`
};
}
}
private formatFileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}
```
--------------------------------------------------------------------------------
/source/tools/asset-advanced-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
export class AssetAdvancedTools implements ToolExecutor {
getTools(): ToolDefinition[] {
return [
{
name: 'save_asset_meta',
description: 'Save asset meta information',
inputSchema: {
type: 'object',
properties: {
urlOrUUID: {
type: 'string',
description: 'Asset URL or UUID'
},
content: {
type: 'string',
description: 'Asset meta serialized content string'
}
},
required: ['urlOrUUID', 'content']
}
},
{
name: 'generate_available_url',
description: 'Generate an available URL based on input URL',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'Asset URL to generate available URL for'
}
},
required: ['url']
}
},
{
name: 'query_asset_db_ready',
description: 'Check if asset database is ready',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'open_asset_external',
description: 'Open asset with external program',
inputSchema: {
type: 'object',
properties: {
urlOrUUID: {
type: 'string',
description: 'Asset URL or UUID to open'
}
},
required: ['urlOrUUID']
}
},
{
name: 'batch_import_assets',
description: 'Import multiple assets in batch',
inputSchema: {
type: 'object',
properties: {
sourceDirectory: {
type: 'string',
description: 'Source directory path'
},
targetDirectory: {
type: 'string',
description: 'Target directory URL'
},
fileFilter: {
type: 'array',
items: { type: 'string' },
description: 'File extensions to include (e.g., [".png", ".jpg"])',
default: []
},
recursive: {
type: 'boolean',
description: 'Include subdirectories',
default: false
},
overwrite: {
type: 'boolean',
description: 'Overwrite existing files',
default: false
}
},
required: ['sourceDirectory', 'targetDirectory']
}
},
{
name: 'batch_delete_assets',
description: 'Delete multiple assets in batch',
inputSchema: {
type: 'object',
properties: {
urls: {
type: 'array',
items: { type: 'string' },
description: 'Array of asset URLs to delete'
}
},
required: ['urls']
}
},
{
name: 'validate_asset_references',
description: 'Validate asset references and find broken links',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory to validate (default: entire project)',
default: 'db://assets'
}
}
}
},
{
name: 'get_asset_dependencies',
description: 'Get asset dependency tree',
inputSchema: {
type: 'object',
properties: {
urlOrUUID: {
type: 'string',
description: 'Asset URL or UUID'
},
direction: {
type: 'string',
description: 'Dependency direction',
enum: ['dependents', 'dependencies', 'both'],
default: 'dependencies'
}
},
required: ['urlOrUUID']
}
},
{
name: 'get_unused_assets',
description: 'Find unused assets in project',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory to scan (default: entire project)',
default: 'db://assets'
},
excludeDirectories: {
type: 'array',
items: { type: 'string' },
description: 'Directories to exclude from scan',
default: []
}
}
}
},
{
name: 'compress_textures',
description: 'Batch compress texture assets',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory containing textures',
default: 'db://assets'
},
format: {
type: 'string',
description: 'Compression format',
enum: ['auto', 'jpg', 'png', 'webp'],
default: 'auto'
},
quality: {
type: 'number',
description: 'Compression quality (0.1-1.0)',
minimum: 0.1,
maximum: 1.0,
default: 0.8
}
}
}
},
{
name: 'export_asset_manifest',
description: 'Export asset manifest/inventory',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory to export manifest for',
default: 'db://assets'
},
format: {
type: 'string',
description: 'Export format',
enum: ['json', 'csv', 'xml'],
default: 'json'
},
includeMetadata: {
type: 'boolean',
description: 'Include asset metadata',
default: true
}
}
}
}
];
}
async execute(toolName: string, args: any): Promise<ToolResponse> {
switch (toolName) {
case 'save_asset_meta':
return await this.saveAssetMeta(args.urlOrUUID, args.content);
case 'generate_available_url':
return await this.generateAvailableUrl(args.url);
case 'query_asset_db_ready':
return await this.queryAssetDbReady();
case 'open_asset_external':
return await this.openAssetExternal(args.urlOrUUID);
case 'batch_import_assets':
return await this.batchImportAssets(args);
case 'batch_delete_assets':
return await this.batchDeleteAssets(args.urls);
case 'validate_asset_references':
return await this.validateAssetReferences(args.directory);
case 'get_asset_dependencies':
return await this.getAssetDependencies(args.urlOrUUID, args.direction);
case 'get_unused_assets':
return await this.getUnusedAssets(args.directory, args.excludeDirectories);
case 'compress_textures':
return await this.compressTextures(args.directory, args.format, args.quality);
case 'export_asset_manifest':
return await this.exportAssetManifest(args.directory, args.format, args.includeMetadata);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
private async saveAssetMeta(urlOrUUID: string, content: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('asset-db', 'save-asset-meta', urlOrUUID, content).then((result: any) => {
resolve({
success: true,
data: {
uuid: result?.uuid,
url: result?.url,
message: 'Asset meta saved successfully'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async generateAvailableUrl(url: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('asset-db', 'generate-available-url', url).then((availableUrl: string) => {
resolve({
success: true,
data: {
originalUrl: url,
availableUrl: availableUrl,
message: availableUrl === url ?
'URL is available' :
'Generated new available URL'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryAssetDbReady(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('asset-db', 'query-ready').then((ready: boolean) => {
resolve({
success: true,
data: {
ready: ready,
message: ready ? 'Asset database is ready' : 'Asset database is not ready'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async openAssetExternal(urlOrUUID: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('asset-db', 'open-asset', urlOrUUID).then(() => {
resolve({
success: true,
message: 'Asset opened with external program'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async batchImportAssets(args: any): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
const fs = require('fs');
const path = require('path');
if (!fs.existsSync(args.sourceDirectory)) {
resolve({ success: false, error: 'Source directory does not exist' });
return;
}
const files = this.getFilesFromDirectory(
args.sourceDirectory,
args.fileFilter || [],
args.recursive || false
);
const importResults: any[] = [];
let successCount = 0;
let errorCount = 0;
for (const filePath of files) {
try {
const fileName = path.basename(filePath);
const targetPath = `${args.targetDirectory}/${fileName}`;
const result = await Editor.Message.request('asset-db', 'import-asset',
filePath, targetPath, {
overwrite: args.overwrite || false,
rename: !(args.overwrite || false)
});
importResults.push({
source: filePath,
target: targetPath,
success: true,
uuid: result?.uuid
});
successCount++;
} catch (err: any) {
importResults.push({
source: filePath,
success: false,
error: err.message
});
errorCount++;
}
}
resolve({
success: true,
data: {
totalFiles: files.length,
successCount: successCount,
errorCount: errorCount,
results: importResults,
message: `Batch import completed: ${successCount} success, ${errorCount} errors`
}
});
} catch (err: any) {
resolve({ success: false, error: err.message });
}
});
}
private getFilesFromDirectory(dirPath: string, fileFilter: string[], recursive: boolean): string[] {
const fs = require('fs');
const path = require('path');
const files: string[] = [];
const items = fs.readdirSync(dirPath);
for (const item of items) {
const fullPath = path.join(dirPath, item);
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
if (fileFilter.length === 0 || fileFilter.some(ext => item.toLowerCase().endsWith(ext.toLowerCase()))) {
files.push(fullPath);
}
} else if (stat.isDirectory() && recursive) {
files.push(...this.getFilesFromDirectory(fullPath, fileFilter, recursive));
}
}
return files;
}
private async batchDeleteAssets(urls: string[]): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
const deleteResults: any[] = [];
let successCount = 0;
let errorCount = 0;
for (const url of urls) {
try {
await Editor.Message.request('asset-db', 'delete-asset', url);
deleteResults.push({
url: url,
success: true
});
successCount++;
} catch (err: any) {
deleteResults.push({
url: url,
success: false,
error: err.message
});
errorCount++;
}
}
resolve({
success: true,
data: {
totalAssets: urls.length,
successCount: successCount,
errorCount: errorCount,
results: deleteResults,
message: `Batch delete completed: ${successCount} success, ${errorCount} errors`
}
});
} catch (err: any) {
resolve({ success: false, error: err.message });
}
});
}
private async validateAssetReferences(directory: string = 'db://assets'): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
// Get all assets in directory
const assets = await Editor.Message.request('asset-db', 'query-assets', { pattern: `${directory}/**/*` });
const brokenReferences: any[] = [];
const validReferences: any[] = [];
for (const asset of assets) {
try {
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', asset.url);
if (assetInfo) {
validReferences.push({
url: asset.url,
uuid: asset.uuid,
name: asset.name
});
}
} catch (err) {
brokenReferences.push({
url: asset.url,
uuid: asset.uuid,
name: asset.name,
error: (err as Error).message
});
}
}
resolve({
success: true,
data: {
directory: directory,
totalAssets: assets.length,
validReferences: validReferences.length,
brokenReferences: brokenReferences.length,
brokenAssets: brokenReferences,
message: `Validation completed: ${brokenReferences.length} broken references found`
}
});
} catch (err: any) {
resolve({ success: false, error: err.message });
}
});
}
private async getAssetDependencies(urlOrUUID: string, direction: string = 'dependencies'): Promise<ToolResponse> {
return new Promise((resolve) => {
// Note: This would require scene analysis or additional APIs not available in current documentation
resolve({
success: false,
error: 'Asset dependency analysis requires additional APIs not available in current Cocos Creator MCP implementation. Consider using the Editor UI for dependency analysis.'
});
});
}
private async getUnusedAssets(directory: string = 'db://assets', excludeDirectories: string[] = []): Promise<ToolResponse> {
return new Promise((resolve) => {
// Note: This would require comprehensive project analysis
resolve({
success: false,
error: 'Unused asset detection requires comprehensive project analysis not available in current Cocos Creator MCP implementation. Consider using the Editor UI or third-party tools for unused asset detection.'
});
});
}
private async compressTextures(directory: string = 'db://assets', format: string = 'auto', quality: number = 0.8): Promise<ToolResponse> {
return new Promise((resolve) => {
// Note: Texture compression would require image processing APIs
resolve({
success: false,
error: 'Texture compression requires image processing capabilities not available in current Cocos Creator MCP implementation. Use the Editor\'s built-in texture compression settings or external tools.'
});
});
}
private async exportAssetManifest(directory: string = 'db://assets', format: string = 'json', includeMetadata: boolean = true): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
const assets = await Editor.Message.request('asset-db', 'query-assets', { pattern: `${directory}/**/*` });
const manifest: any[] = [];
for (const asset of assets) {
const manifestEntry: any = {
name: asset.name,
url: asset.url,
uuid: asset.uuid,
type: asset.type,
size: (asset as any).size || 0,
isDirectory: asset.isDirectory || false
};
if (includeMetadata) {
try {
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', asset.url);
if (assetInfo && assetInfo.meta) {
manifestEntry.meta = assetInfo.meta;
}
} catch (err) {
// Skip metadata if not available
}
}
manifest.push(manifestEntry);
}
let exportData: string;
switch (format) {
case 'json':
exportData = JSON.stringify(manifest, null, 2);
break;
case 'csv':
exportData = this.convertToCSV(manifest);
break;
case 'xml':
exportData = this.convertToXML(manifest);
break;
default:
exportData = JSON.stringify(manifest, null, 2);
}
resolve({
success: true,
data: {
directory: directory,
format: format,
assetCount: manifest.length,
includeMetadata: includeMetadata,
manifest: exportData,
message: `Asset manifest exported with ${manifest.length} assets`
}
});
} catch (err: any) {
resolve({ success: false, error: err.message });
}
});
}
private convertToCSV(data: any[]): string {
if (data.length === 0) return '';
const headers = Object.keys(data[0]);
const csvRows = [headers.join(',')];
for (const row of data) {
const values = headers.map(header => {
const value = row[header];
return typeof value === 'object' ? JSON.stringify(value) : String(value);
});
csvRows.push(values.join(','));
}
return csvRows.join('\n');
}
private convertToXML(data: any[]): string {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<assets>\n';
for (const item of data) {
xml += ' <asset>\n';
for (const [key, value] of Object.entries(item)) {
const xmlValue = typeof value === 'object' ?
JSON.stringify(value) :
String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
xml += ` <${key}>${xmlValue}</${key}>\n`;
}
xml += ' </asset>\n';
}
xml += '</assets>';
return xml;
}
}
```
--------------------------------------------------------------------------------
/source/tools/scene-advanced-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
export class SceneAdvancedTools implements ToolExecutor {
getTools(): ToolDefinition[] {
return [
{
name: 'reset_node_property',
description: 'Reset node property to default value',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
},
path: {
type: 'string',
description: 'Property path (e.g., position, rotation, scale)'
}
},
required: ['uuid', 'path']
}
},
{
name: 'move_array_element',
description: 'Move array element position',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
},
path: {
type: 'string',
description: 'Array property path (e.g., __comps__)'
},
target: {
type: 'number',
description: 'Target item original index'
},
offset: {
type: 'number',
description: 'Offset amount (positive or negative)'
}
},
required: ['uuid', 'path', 'target', 'offset']
}
},
{
name: 'remove_array_element',
description: 'Remove array element at specific index',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
},
path: {
type: 'string',
description: 'Array property path'
},
index: {
type: 'number',
description: 'Target item index to remove'
}
},
required: ['uuid', 'path', 'index']
}
},
{
name: 'copy_node',
description: 'Copy node for later paste operation',
inputSchema: {
type: 'object',
properties: {
uuids: {
oneOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' } }
],
description: 'Node UUID or array of UUIDs to copy'
}
},
required: ['uuids']
}
},
{
name: 'paste_node',
description: 'Paste previously copied nodes',
inputSchema: {
type: 'object',
properties: {
target: {
type: 'string',
description: 'Target parent node UUID'
},
uuids: {
oneOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' } }
],
description: 'Node UUIDs to paste'
},
keepWorldTransform: {
type: 'boolean',
description: 'Keep world transform coordinates',
default: false
}
},
required: ['target', 'uuids']
}
},
{
name: 'cut_node',
description: 'Cut node (copy + mark for move)',
inputSchema: {
type: 'object',
properties: {
uuids: {
oneOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' } }
],
description: 'Node UUID or array of UUIDs to cut'
}
},
required: ['uuids']
}
},
{
name: 'reset_node_transform',
description: 'Reset node position, rotation and scale',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
}
},
required: ['uuid']
}
},
{
name: 'reset_component',
description: 'Reset component to default values',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Component UUID'
}
},
required: ['uuid']
}
},
{
name: 'restore_prefab',
description: 'Restore prefab instance from asset',
inputSchema: {
type: 'object',
properties: {
nodeUuid: {
type: 'string',
description: 'Node UUID'
},
assetUuid: {
type: 'string',
description: 'Prefab asset UUID'
}
},
required: ['nodeUuid', 'assetUuid']
}
},
{
name: 'execute_component_method',
description: 'Execute method on component',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Component UUID'
},
name: {
type: 'string',
description: 'Method name'
},
args: {
type: 'array',
description: 'Method arguments',
default: []
}
},
required: ['uuid', 'name']
}
},
{
name: 'execute_scene_script',
description: 'Execute scene script method',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Plugin name'
},
method: {
type: 'string',
description: 'Method name'
},
args: {
type: 'array',
description: 'Method arguments',
default: []
}
},
required: ['name', 'method']
}
},
{
name: 'scene_snapshot',
description: 'Create scene state snapshot',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'scene_snapshot_abort',
description: 'Abort scene snapshot creation',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'begin_undo_recording',
description: 'Begin recording undo data',
inputSchema: {
type: 'object',
properties: {
nodeUuid: {
type: 'string',
description: 'Node UUID to record'
}
},
required: ['nodeUuid']
}
},
{
name: 'end_undo_recording',
description: 'End recording undo data',
inputSchema: {
type: 'object',
properties: {
undoId: {
type: 'string',
description: 'Undo recording ID from begin_undo_recording'
}
},
required: ['undoId']
}
},
{
name: 'cancel_undo_recording',
description: 'Cancel undo recording',
inputSchema: {
type: 'object',
properties: {
undoId: {
type: 'string',
description: 'Undo recording ID to cancel'
}
},
required: ['undoId']
}
},
{
name: 'soft_reload_scene',
description: 'Soft reload current scene',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'query_scene_ready',
description: 'Check if scene is ready',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'query_scene_dirty',
description: 'Check if scene has unsaved changes',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'query_scene_classes',
description: 'Query all registered classes',
inputSchema: {
type: 'object',
properties: {
extends: {
type: 'string',
description: 'Filter classes that extend this base class'
}
}
}
},
{
name: 'query_scene_components',
description: 'Query available scene components',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'query_component_has_script',
description: 'Check if component has script',
inputSchema: {
type: 'object',
properties: {
className: {
type: 'string',
description: 'Script class name to check'
}
},
required: ['className']
}
},
{
name: 'query_nodes_by_asset_uuid',
description: 'Find nodes that use specific asset UUID',
inputSchema: {
type: 'object',
properties: {
assetUuid: {
type: 'string',
description: 'Asset UUID to search for'
}
},
required: ['assetUuid']
}
}
];
}
async execute(toolName: string, args: any): Promise<ToolResponse> {
switch (toolName) {
case 'reset_node_property':
return await this.resetNodeProperty(args.uuid, args.path);
case 'move_array_element':
return await this.moveArrayElement(args.uuid, args.path, args.target, args.offset);
case 'remove_array_element':
return await this.removeArrayElement(args.uuid, args.path, args.index);
case 'copy_node':
return await this.copyNode(args.uuids);
case 'paste_node':
return await this.pasteNode(args.target, args.uuids, args.keepWorldTransform);
case 'cut_node':
return await this.cutNode(args.uuids);
case 'reset_node_transform':
return await this.resetNodeTransform(args.uuid);
case 'reset_component':
return await this.resetComponent(args.uuid);
case 'restore_prefab':
return await this.restorePrefab(args.nodeUuid, args.assetUuid);
case 'execute_component_method':
return await this.executeComponentMethod(args.uuid, args.name, args.args);
case 'execute_scene_script':
return await this.executeSceneScript(args.name, args.method, args.args);
case 'scene_snapshot':
return await this.sceneSnapshot();
case 'scene_snapshot_abort':
return await this.sceneSnapshotAbort();
case 'begin_undo_recording':
return await this.beginUndoRecording(args.nodeUuid);
case 'end_undo_recording':
return await this.endUndoRecording(args.undoId);
case 'cancel_undo_recording':
return await this.cancelUndoRecording(args.undoId);
case 'soft_reload_scene':
return await this.softReloadScene();
case 'query_scene_ready':
return await this.querySceneReady();
case 'query_scene_dirty':
return await this.querySceneDirty();
case 'query_scene_classes':
return await this.querySceneClasses(args.extends);
case 'query_scene_components':
return await this.querySceneComponents();
case 'query_component_has_script':
return await this.queryComponentHasScript(args.className);
case 'query_nodes_by_asset_uuid':
return await this.queryNodesByAssetUuid(args.assetUuid);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
private async resetNodeProperty(uuid: string, path: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'reset-property', {
uuid,
path,
dump: { value: null }
}).then(() => {
resolve({
success: true,
message: `Property '${path}' reset to default value`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async moveArrayElement(uuid: string, path: string, target: number, offset: number): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'move-array-element', {
uuid,
path,
target,
offset
}).then(() => {
resolve({
success: true,
message: `Array element at index ${target} moved by ${offset}`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async removeArrayElement(uuid: string, path: string, index: number): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'remove-array-element', {
uuid,
path,
index
}).then(() => {
resolve({
success: true,
message: `Array element at index ${index} removed`
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async copyNode(uuids: string | string[]): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'copy-node', uuids).then((result: string | string[]) => {
resolve({
success: true,
data: {
copiedUuids: result,
message: 'Node(s) copied successfully'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async pasteNode(target: string, uuids: string | string[], keepWorldTransform: boolean = false): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'paste-node', {
target,
uuids,
keepWorldTransform
}).then((result: string | string[]) => {
resolve({
success: true,
data: {
newUuids: result,
message: 'Node(s) pasted successfully'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async cutNode(uuids: string | string[]): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'cut-node', uuids).then((result: any) => {
resolve({
success: true,
data: {
cutUuids: result,
message: 'Node(s) cut successfully'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async resetNodeTransform(uuid: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'reset-node', { uuid }).then(() => {
resolve({
success: true,
message: 'Node transform reset to default'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async resetComponent(uuid: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'reset-component', { uuid }).then(() => {
resolve({
success: true,
message: 'Component reset to default values'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async restorePrefab(nodeUuid: string, assetUuid: string): Promise<ToolResponse> {
return new Promise((resolve) => {
(Editor.Message.request as any)('scene', 'restore-prefab', nodeUuid, assetUuid).then(() => {
resolve({
success: true,
message: 'Prefab restored successfully'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async executeComponentMethod(uuid: string, name: string, args: any[] = []): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'execute-component-method', {
uuid,
name,
args
}).then((result: any) => {
resolve({
success: true,
data: {
result: result,
message: `Method '${name}' executed successfully`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async executeSceneScript(name: string, method: string, args: any[] = []): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'execute-scene-script', {
name,
method,
args
}).then((result: any) => {
resolve({
success: true,
data: result
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async sceneSnapshot(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'snapshot').then(() => {
resolve({
success: true,
message: 'Scene snapshot created'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async sceneSnapshotAbort(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'snapshot-abort').then(() => {
resolve({
success: true,
message: 'Scene snapshot aborted'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async beginUndoRecording(nodeUuid: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'begin-recording', nodeUuid).then((undoId: string) => {
resolve({
success: true,
data: {
undoId: undoId,
message: 'Undo recording started'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async endUndoRecording(undoId: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'end-recording', undoId).then(() => {
resolve({
success: true,
message: 'Undo recording ended'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async cancelUndoRecording(undoId: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'cancel-recording', undoId).then(() => {
resolve({
success: true,
message: 'Undo recording cancelled'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async softReloadScene(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'soft-reload').then(() => {
resolve({
success: true,
message: 'Scene soft reloaded successfully'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async querySceneReady(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-is-ready').then((ready: boolean) => {
resolve({
success: true,
data: {
ready: ready,
message: ready ? 'Scene is ready' : 'Scene is not ready'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async querySceneDirty(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-dirty').then((dirty: boolean) => {
resolve({
success: true,
data: {
dirty: dirty,
message: dirty ? 'Scene has unsaved changes' : 'Scene is clean'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async querySceneClasses(extendsClass?: string): Promise<ToolResponse> {
return new Promise((resolve) => {
const options: any = {};
if (extendsClass) {
options.extends = extendsClass;
}
Editor.Message.request('scene', 'query-classes', options).then((classes: any[]) => {
resolve({
success: true,
data: {
classes: classes,
count: classes.length,
extendsFilter: extendsClass
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async querySceneComponents(): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-components').then((components: any[]) => {
resolve({
success: true,
data: {
components: components,
count: components.length
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryComponentHasScript(className: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-component-has-script', className).then((hasScript: boolean) => {
resolve({
success: true,
data: {
className: className,
hasScript: hasScript,
message: hasScript ? `Component '${className}' has script` : `Component '${className}' does not have script`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async queryNodesByAssetUuid(assetUuid: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-nodes-by-asset-uuid', assetUuid).then((nodeUuids: string[]) => {
resolve({
success: true,
data: {
assetUuid: assetUuid,
nodeUuids: nodeUuids,
count: nodeUuids.length,
message: `Found ${nodeUuids.length} nodes using asset`
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
}
```
--------------------------------------------------------------------------------
/FEATURE_GUIDE_EN.md:
--------------------------------------------------------------------------------
```markdown
# Cocos Creator MCP Server Feature Guide
## Overview
The Cocos Creator MCP Server is a comprehensive Model Context Protocol (MCP) server plugin designed for Cocos Creator 3.8+, enabling AI assistants to interact with the Cocos Creator editor through standardized protocols.
This document provides detailed information about all available MCP tools and their usage.
## Tool Categories
The MCP server provides **158 tools** organized into 13 main categories by functionality:
1. [Scene Tools](#1-scene-tools)
2. [Node Tools](#2-node-tools)
3. [Component Management Tools](#3-component-management-tools)
4. [Prefab Tools](#4-prefab-tools)
5. [Project Control Tools](#5-project-control-tools)
6. [Debug Tools](#6-debug-tools)
7. [Preferences Tools](#7-preferences-tools)
8. [Server Tools](#8-server-tools)
9. [Broadcast Tools](#9-broadcast-tools)
10. [Asset Advanced Tools](#10-asset-advanced-tools)
11. [Reference Image Tools](#11-reference-image-tools)
12. [Scene Advanced Tools](#12-scene-advanced-tools)
13. [Scene View Tools](#13-scene-view-tools)
---
## 1. Scene Tools
### 1.1 scene_get_current_scene
Get current scene information
**Parameters**: None
**Returns**: Current scene name, UUID, type, active status, and node count
**Example**:
```json
{
"tool": "scene_get_current_scene",
"arguments": {}
}
```
### 1.2 scene_get_scene_list
Get all scenes in the project
**Parameters**: None
**Returns**: List of all scenes in the project, including names, paths, and UUIDs
**Example**:
```json
{
"tool": "scene_get_scene_list",
"arguments": {}
}
```
### 1.3 scene_open_scene
Open a scene by path
**Parameters**:
- `scenePath` (string, required): Scene file path
**Example**:
```json
{
"tool": "scene_open_scene",
"arguments": {
"scenePath": "db://assets/scenes/GameScene.scene"
}
}
```
### 1.4 scene_save_scene
Save current scene
**Parameters**: None
**Example**:
```json
{
"tool": "scene_save_scene",
"arguments": {}
}
```
### 1.5 scene_create_scene
Create a new scene asset
**Parameters**:
- `sceneName` (string, required): Name of the new scene
- `savePath` (string, required): Path to save the scene
**Example**:
```json
{
"tool": "scene_create_scene",
"arguments": {
"sceneName": "NewLevel",
"savePath": "db://assets/scenes/NewLevel.scene"
}
}
```
### 1.6 scene_save_scene_as
Save scene as a new file
**Parameters**:
- `path` (string, required): Path to save the scene
**Example**:
```json
{
"tool": "scene_save_scene_as",
"arguments": {
"path": "db://assets/scenes/GameScene_Copy.scene"
}
}
```
### 1.7 scene_close_scene
Close current scene
**Parameters**: None
**Example**:
```json
{
"tool": "scene_close_scene",
"arguments": {}
}
```
### 1.8 scene_get_scene_hierarchy
Get the complete hierarchy of current scene
**Parameters**:
- `includeComponents` (boolean, optional): Whether to include component information, defaults to false
**Example**:
```json
{
"tool": "scene_get_scene_hierarchy",
"arguments": {
"includeComponents": true
}
}
```
---
## 2. Node Tools
### 2.1 node_create_node
Create a new node in the scene
**Parameters**:
- `name` (string, required): Node name
- `parentUuid` (string, **strongly recommended**): Parent node UUID. **Important**: It is strongly recommended to always provide this parameter. Use `get_current_scene` or `get_all_nodes` to find parent node UUIDs. If not provided, the node will be created at the scene root.
- `nodeType` (string, optional): Node type, options: `Node`, `2DNode`, `3DNode`, defaults to `Node`
- `siblingIndex` (number, optional): Sibling index, -1 means append at end, defaults to -1
**Important Note**: To ensure the node is created at the expected location, always provide the `parentUuid` parameter. You can obtain parent node UUIDs by:
- Using `scene_get_current_scene` to get the scene root node UUID
- Using `node_get_all_nodes` to view all nodes and their UUIDs
- Using `node_find_node_by_name` to find specific node UUIDs
**Example**:
```json
{
"tool": "node_create_node",
"arguments": {
"name": "PlayerNode",
"nodeType": "2DNode",
"parentUuid": "parent-uuid-here"
}
}
```
### 2.2 node_get_node_info
Get node information by UUID
**Parameters**:
- `uuid` (string, required): Node UUID
**Example**:
```json
{
"tool": "node_get_node_info",
"arguments": {
"uuid": "node-uuid-here"
}
}
```
### 2.3 node_find_nodes
Find nodes by name pattern
**Parameters**:
- `pattern` (string, required): Name pattern to search
- `exactMatch` (boolean, optional): Whether to match exactly, defaults to false
**Example**:
```json
{
"tool": "node_find_nodes",
"arguments": {
"pattern": "Enemy",
"exactMatch": false
}
}
```
### 2.4 node_find_node_by_name
Find the first node by exact name
**Parameters**:
- `name` (string, required): Node name to find
**Example**:
```json
{
"tool": "node_find_node_by_name",
"arguments": {
"name": "Player"
}
}
```
### 2.5 node_get_all_nodes
Get all nodes in the scene with their UUIDs
**Parameters**: None
**Example**:
```json
{
"tool": "node_get_all_nodes",
"arguments": {}
}
```
### 2.6 node_set_node_property
Set node property value
**Parameters**:
- `uuid` (string, required): Node UUID
- `property` (string, required): Property name (e.g., position, rotation, scale, active)
- `value` (any, required): Property value
**Example**:
```json
{
"tool": "node_set_node_property",
"arguments": {
"uuid": "node-uuid-here",
"property": "position",
"value": {"x": 100, "y": 200, "z": 0}
}
}
```
### 2.7 node_delete_node
Delete a node from the scene
**Parameters**:
- `uuid` (string, required): UUID of the node to delete
**Example**:
```json
{
"tool": "node_delete_node",
"arguments": {
"uuid": "node-uuid-here"
}
}
```
### 2.8 node_move_node
Move a node to a new parent
**Parameters**:
- `nodeUuid` (string, required): UUID of the node to move
- `newParentUuid` (string, required): New parent node UUID
- `siblingIndex` (number, optional): Sibling index in the new parent, defaults to -1
**Example**:
```json
{
"tool": "node_move_node",
"arguments": {
"nodeUuid": "node-uuid-here",
"newParentUuid": "parent-uuid-here",
"siblingIndex": 0
}
}
```
### 2.9 node_duplicate_node
Duplicate a node
**Parameters**:
- `uuid` (string, required): UUID of the node to duplicate
- `includeChildren` (boolean, optional): Whether to include child nodes, defaults to true
**Example**:
```json
{
"tool": "node_duplicate_node",
"arguments": {
"uuid": "node-uuid-here",
"includeChildren": true
}
}
```
---
## 3. Component Management Tools
### 3.1 component_add_component
Add a component to a specific node
**Parameters**:
- `nodeUuid` (string, **required**): Target node UUID. **Important**: You must specify the exact node to add the component to. Use `get_all_nodes` or `find_node_by_name` to get the UUID of the desired node.
- `componentType` (string, required): Component type (e.g., cc.Sprite, cc.Label, cc.Button)
**Important Note**: Before adding a component, ensure:
1. First use `node_get_all_nodes` or `node_find_node_by_name` to find the target node's UUID
2. Verify the node exists and the UUID is correct
3. Choose the appropriate component type
**Example**:
```json
{
"tool": "component_add_component",
"arguments": {
"nodeUuid": "node-uuid-here",
"componentType": "cc.Sprite"
}
}
```
### 3.2 component_remove_component
Remove a component from a node
**Parameters**:
- `nodeUuid` (string, required): Node UUID
- `componentType` (string, required): Component type to remove
**Example**:
```json
{
"tool": "component_remove_component",
"arguments": {
"nodeUuid": "node-uuid-here",
"componentType": "cc.Sprite"
}
}
```
### 3.3 component_get_components
Get all components of a node
**Parameters**:
- `nodeUuid` (string, required): Node UUID
**Example**:
```json
{
"tool": "component_get_components",
"arguments": {
"nodeUuid": "node-uuid-here"
}
}
```
### 3.4 component_get_component_info
Get specific component information
**Parameters**:
- `nodeUuid` (string, required): Node UUID
- `componentType` (string, required): Component type to get info for
**Example**:
```json
{
"tool": "component_get_component_info",
"arguments": {
"nodeUuid": "node-uuid-here",
"componentType": "cc.Sprite"
}
}
```
### 3.5 component_set_component_property
Set component property value
**Parameters**:
- `nodeUuid` (string, required): Node UUID
- `componentType` (string, required): Component type
- `property` (string, required): Property name
- `value` (any, required): Property value
**Example**:
```json
{
"tool": "component_set_component_property",
"arguments": {
"nodeUuid": "node-uuid-here",
"componentType": "cc.Sprite",
"property": "spriteFrame",
"value": "sprite-frame-uuid"
}
}
```
### 3.6 component_attach_script
Attach a script component to a node
**Parameters**:
- `nodeUuid` (string, required): Node UUID
- `scriptPath` (string, required): Script asset path
**Example**:
```json
{
"tool": "component_attach_script",
"arguments": {
"nodeUuid": "node-uuid-here",
"scriptPath": "db://assets/scripts/PlayerController.ts"
}
}
```
### 3.7 component_get_available_components
Get list of available component types
**Parameters**:
- `category` (string, optional): Component category filter, options: `all`, `renderer`, `ui`, `physics`, `animation`, `audio`, defaults to `all`
**Example**:
```json
{
"tool": "component_get_available_components",
"arguments": {
"category": "ui"
}
}
```
---
## 4. Prefab Tools
**⚠️ Known Issue**: When using standard Cocos Creator API for prefab instantiation, complex prefabs with child nodes may not be properly restored. While prefab creation functionality can correctly save all child node information, the instantiation process through `create-node` with `assetUuid` has limitations that may result in missing child nodes in the instantiated prefab.
### 4.1 prefab_get_prefab_list
Get all prefabs in the project
**Parameters**:
- `folder` (string, optional): Search folder path, defaults to `db://assets`
**Example**:
```json
{
"tool": "prefab_get_prefab_list",
"arguments": {
"folder": "db://assets/prefabs"
}
}
```
### 4.2 prefab_load_prefab
Load a prefab by path
**Parameters**:
- `prefabPath` (string, required): Prefab asset path
**Example**:
```json
{
"tool": "prefab_load_prefab",
"arguments": {
"prefabPath": "db://assets/prefabs/Enemy.prefab"
}
}
```
### 4.3 prefab_instantiate_prefab
Instantiate a prefab in the scene
**Parameters**:
- `prefabPath` (string, required): Prefab asset path
- `parentUuid` (string, optional): Parent node UUID
- `position` (object, optional): Initial position with x, y, z properties
**Example**:
```json
{
"tool": "prefab_instantiate_prefab",
"arguments": {
"prefabPath": "db://assets/prefabs/Enemy.prefab",
"parentUuid": "parent-uuid-here",
"position": {"x": 100, "y": 200, "z": 0}
}
}
```
**⚠️ Functionality Limitation**: Complex prefabs with child nodes may not instantiate correctly. Due to Cocos Creator API limitations in the standard `create-node` method using `assetUuid`, only the root node may be created, and child nodes may be lost. This is a known issue with the current implementation.
### 4.4 prefab_create_prefab
Create a prefab from a node
**Parameters**:
- `nodeUuid` (string, required): Source node UUID
- `savePath` (string, required): Path to save the prefab
- `prefabName` (string, required): Prefab name
**Example**:
```json
{
"tool": "prefab_create_prefab",
"arguments": {
"nodeUuid": "node-uuid-here",
"savePath": "db://assets/prefabs/",
"prefabName": "MyPrefab"
}
}
```
### 4.5 prefab_create_prefab_from_node
Create a prefab from a node (alias for create_prefab)
**Parameters**:
- `nodeUuid` (string, required): Source node UUID
- `prefabPath` (string, required): Path to save the prefab
**Example**:
```json
{
"tool": "prefab_create_prefab_from_node",
"arguments": {
"nodeUuid": "node-uuid-here",
"prefabPath": "db://assets/prefabs/MyPrefab.prefab"
}
}
```
### 4.6 prefab_update_prefab
Update an existing prefab
**Parameters**:
- `prefabPath` (string, required): Prefab asset path
- `nodeUuid` (string, required): Node UUID containing changes
**Example**:
```json
{
"tool": "prefab_update_prefab",
"arguments": {
"prefabPath": "db://assets/prefabs/Enemy.prefab",
"nodeUuid": "node-uuid-here"
}
}
```
### 4.7 prefab_revert_prefab
Revert a prefab instance to its original state
**Parameters**:
- `nodeUuid` (string, required): Prefab instance node UUID
**Example**:
```json
{
"tool": "prefab_revert_prefab",
"arguments": {
"nodeUuid": "prefab-instance-uuid-here"
}
}
```
### 4.8 prefab_get_prefab_info
Get detailed prefab information
**Parameters**:
- `prefabPath` (string, required): Prefab asset path
**Example**:
```json
{
"tool": "prefab_get_prefab_info",
"arguments": {
"prefabPath": "db://assets/prefabs/Enemy.prefab"
}
}
```
---
## 5. Project Control Tools
### 5.1 project_run_project
Run the project in preview mode
**Parameters**:
- `platform` (string, optional): Target platform, options: `browser`, `simulator`, `preview`, defaults to `browser`
**Example**:
```json
{
"tool": "project_run_project",
"arguments": {
"platform": "browser"
}
}
```
### 5.2 project_build_project
Build the project
**Parameters**:
- `platform` (string, required): Build platform, options: `web-mobile`, `web-desktop`, `ios`, `android`, `windows`, `mac`
- `debug` (boolean, optional): Whether to build in debug mode, defaults to true
**Example**:
```json
{
"tool": "project_build_project",
"arguments": {
"platform": "web-mobile",
"debug": false
}
}
```
### 5.3 project_get_project_info
Get project information
**Parameters**: None
**Example**:
```json
{
"tool": "project_get_project_info",
"arguments": {}
}
```
### 5.4 project_get_project_settings
Get project settings
**Parameters**:
- `category` (string, optional): Settings category, options: `general`, `physics`, `render`, `assets`, defaults to `general`
**Example**:
```json
{
"tool": "project_get_project_settings",
"arguments": {
"category": "physics"
}
}
```
### 5.5 project_refresh_assets
Refresh the asset database
**Parameters**:
- `folder` (string, optional): Specific folder to refresh
**Example**:
```json
{
"tool": "project_refresh_assets",
"arguments": {
"folder": "db://assets/textures"
}
}
```
### 5.6 project_import_asset
Import an asset file
**Parameters**:
- `sourcePath` (string, required): Source file path
- `targetFolder` (string, required): Target folder in assets
**Example**:
```json
{
"tool": "project_import_asset",
"arguments": {
"sourcePath": "/path/to/image.png",
"targetFolder": "db://assets/textures"
}
}
```
### 5.7 project_get_asset_info
Get asset information
**Parameters**:
- `assetPath` (string, required): Asset path
**Example**:
```json
{
"tool": "project_get_asset_info",
"arguments": {
"assetPath": "db://assets/textures/player.png"
}
}
```
### 5.8 project_get_assets
Get assets by type
**Parameters**:
- `type` (string, optional): Asset type filter, options: `all`, `scene`, `prefab`, `script`, `texture`, `material`, `mesh`, `audio`, `animation`, defaults to `all`
- `folder` (string, optional): Search folder, defaults to `db://assets`
**Example**:
```json
{
"tool": "project_get_assets",
"arguments": {
"type": "texture",
"folder": "db://assets/textures"
}
}
```
### 5.9 project_get_build_settings
Get build settings
**Parameters**: None
**Example**:
```json
{
"tool": "project_get_build_settings",
"arguments": {}
}
```
### 5.10 project_open_build_panel
Open the build panel in the editor
**Parameters**: None
**Example**:
```json
{
"tool": "project_open_build_panel",
"arguments": {}
}
```
### 5.11 project_check_builder_status
Check if the builder worker process is ready
**Parameters**: None
**Example**:
```json
{
"tool": "project_check_builder_status",
"arguments": {}
}
```
### 5.12 project_start_preview_server
Start the preview server
**Parameters**:
- `port` (number, optional): Preview server port, defaults to 7456
**Example**:
```json
{
"tool": "project_start_preview_server",
"arguments": {
"port": 8080
}
}
```
### 5.13 project_stop_preview_server
Stop the preview server
**Parameters**: None
**Example**:
```json
{
"tool": "project_stop_preview_server",
"arguments": {}
}
```
### 5.14 project_create_asset
Create a new asset file or folder
**Parameters**:
- `url` (string, required): Asset URL
- `content` (string, optional): File content, null means create folder
- `overwrite` (boolean, optional): Whether to overwrite existing file, defaults to false
**Example**:
```json
{
"tool": "project_create_asset",
"arguments": {
"url": "db://assets/scripts/NewScript.ts",
"content": "// New TypeScript script\n",
"overwrite": false
}
}
```
### 5.15 project_copy_asset
Copy an asset to another location
**Parameters**:
- `source` (string, required): Source asset URL
- `target` (string, required): Target location URL
- `overwrite` (boolean, optional): Whether to overwrite existing file, defaults to false
**Example**:
```json
{
"tool": "project_copy_asset",
"arguments": {
"source": "db://assets/textures/player.png",
"target": "db://assets/textures/backup/player.png",
"overwrite": false
}
}
```
### 5.16 project_move_asset
Move an asset to another location
**Parameters**:
- `source` (string, required): Source asset URL
- `target` (string, required): Target location URL
- `overwrite` (boolean, optional): Whether to overwrite existing file, defaults to false
**Example**:
```json
{
"tool": "project_move_asset",
"arguments": {
"source": "db://assets/textures/old_player.png",
"target": "db://assets/textures/player.png",
"overwrite": true
}
}
```
### 5.17 project_delete_asset
Delete an asset
**Parameters**:
- `url` (string, required): Asset URL to delete
**Example**:
```json
{
"tool": "project_delete_asset",
"arguments": {
"url": "db://assets/textures/unused.png"
}
}
```
### 5.18 project_save_asset
Save asset content
**Parameters**:
- `url` (string, required): Asset URL
- `content` (string, required): Asset content
**Example**:
```json
{
"tool": "project_save_asset",
"arguments": {
"url": "db://assets/scripts/GameManager.ts",
"content": "// Updated script content\n"
}
}
```
### 5.19 project_reimport_asset
Reimport an asset
**Parameters**:
- `url` (string, required): Asset URL to reimport
**Example**:
```json
{
"tool": "project_reimport_asset",
"arguments": {
"url": "db://assets/textures/player.png"
}
}
```
### 5.20 project_query_asset_path
Get asset disk path
**Parameters**:
- `url` (string, required): Asset URL
**Example**:
```json
{
"tool": "project_query_asset_path",
"arguments": {
"url": "db://assets/textures/player.png"
}
}
```
### 5.21 project_query_asset_uuid
Get asset UUID from URL
**Parameters**:
- `url` (string, required): Asset URL
**Example**:
```json
{
"tool": "project_query_asset_uuid",
"arguments": {
"url": "db://assets/textures/player.png"
}
}
```
### 5.22 project_query_asset_url
Get asset URL from UUID
**Parameters**:
- `uuid` (string, required): Asset UUID
**Example**:
```json
{
"tool": "project_query_asset_url",
"arguments": {
"uuid": "asset-uuid-here"
}
}
```
---
## 6. Debug Tools
### 6.1 debug_get_console_logs
Get editor console logs
**Parameters**:
- `limit` (number, optional): Number of latest logs to retrieve, defaults to 100
- `filter` (string, optional): Filter logs by type, options: `all`, `log`, `warn`, `error`, `info`, defaults to `all`
**Example**:
```json
{
"tool": "debug_get_console_logs",
"arguments": {
"limit": 50,
"filter": "error"
}
}
```
### 6.2 debug_clear_console
Clear the editor console
**Parameters**: None
**Example**:
```json
{
"tool": "debug_clear_console",
"arguments": {}
}
```
### 6.3 debug_execute_script
Execute JavaScript code in scene context
**Parameters**:
- `script` (string, required): JavaScript code to execute
**Example**:
```json
{
"tool": "debug_execute_script",
"arguments": {
"script": "console.log('Hello from MCP!');"
}
}
```
### 6.4 debug_get_node_tree
Get detailed node tree for debugging
**Parameters**:
- `rootUuid` (string, optional): Root node UUID, if not provided uses scene root node
- `maxDepth` (number, optional): Maximum tree depth, defaults to 10
**Example**:
```json
{
"tool": "debug_get_node_tree",
"arguments": {
"rootUuid": "root-node-uuid",
"maxDepth": 5
}
}
```
### 6.5 debug_get_performance_stats
Get performance statistics
**Parameters**: None
**Example**:
```json
{
"tool": "debug_get_performance_stats",
"arguments": {}
}
```
### 6.6 debug_validate_scene
Validate if the current scene has issues
**Parameters**:
- `checkMissingAssets` (boolean, optional): Check for missing asset references, defaults to true
- `checkPerformance` (boolean, optional): Check for performance issues, defaults to true
**Example**:
```json
{
"tool": "debug_validate_scene",
"arguments": {
"checkMissingAssets": true,
"checkPerformance": true
}
}
```
### 6.7 debug_get_editor_info
Get editor and environment information
**Parameters**: None
**Example**:
```json
{
"tool": "debug_get_editor_info",
"arguments": {}
}
```
### 6.8 debug_get_project_logs
Get project logs from temp/logs/project.log file
**Parameters**:
- `lines` (number, optional): Number of lines to read from the end of the log file, default is 100, range: 1-10000
- `filterKeyword` (string, optional): Filter logs by specific keyword
- `logLevel` (string, optional): Filter by log level, options: `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`, `ALL`, defaults to `ALL`
**Example**:
```json
{
"tool": "debug_get_project_logs",
"arguments": {
"lines": 200,
"filterKeyword": "prefab",
"logLevel": "INFO"
}
}
```
### 6.9 debug_get_log_file_info
Get project log file information
**Parameters**: None
**Returns**: File size, last modified time, line count, and file path information
**Example**:
```json
{
"tool": "debug_get_log_file_info",
"arguments": {}
}
```
### 6.10 debug_search_project_logs
Search for specific patterns or errors in project logs
**Parameters**:
- `pattern` (string, required): Search pattern (supports regex)
- `maxResults` (number, optional): Maximum number of matching results, defaults to 20, range: 1-100
- `contextLines` (number, optional): Number of context lines to show around each match, defaults to 2, range: 0-10
**Example**:
```json
{
"tool": "debug_search_project_logs",
"arguments": {
"pattern": "error|failed|exception",
"maxResults": 10,
"contextLines": 3
}
}
```
---
## 7. Preferences Tools
### 7.1 preferences_get_preferences
Get editor preferences
**Parameters**:
- `key` (string, optional): Specific preference key to get
**Example**:
```json
{
"tool": "preferences_get_preferences",
"arguments": {
"key": "editor.theme"
}
}
```
### 7.2 preferences_set_preferences
Set editor preferences
**Parameters**:
- `key` (string, required): Preference key to set
- `value` (any, required): Preference value to set
**Example**:
```json
{
"tool": "preferences_set_preferences",
"arguments": {
"key": "editor.theme",
"value": "dark"
}
}
```
### 7.3 preferences_get_global_preferences
Get global editor preferences
**Parameters**:
- `key` (string, optional): Global preference key to get
**Example**:
```json
{
"tool": "preferences_get_global_preferences",
"arguments": {
"key": "global.autoSave"
}
}
```
### 7.4 preferences_set_global_preferences
Set global editor preferences
**Parameters**:
- `key` (string, required): Global preference key to set
- `value` (any, required): Global preference value to set
**Example**:
```json
{
"tool": "preferences_set_global_preferences",
"arguments": {
"key": "global.autoSave",
"value": true
}
}
```
### 7.5 preferences_get_recent_projects
Get recently opened projects
**Parameters**: None
**Example**:
```json
{
"tool": "preferences_get_recent_projects",
"arguments": {}
}
```
### 7.6 preferences_clear_recent_projects
Clear the list of recently opened projects
**Parameters**: None
**Example**:
```json
{
"tool": "preferences_clear_recent_projects",
"arguments": {}
}
```
---
## 8. Server Tools
### 8.1 server_get_server_info
Get server information
**Parameters**: None
**Example**:
```json
{
"tool": "server_get_server_info",
"arguments": {}
}
```
### 8.2 server_broadcast_custom_message
Broadcast a custom message
**Parameters**:
- `message` (string, required): Message name
- `data` (any, optional): Message data
**Example**:
```json
{
"tool": "server_broadcast_custom_message",
"arguments": {
"message": "custom_event",
"data": {"type": "test", "value": 123}
}
}
```
### 8.3 server_get_editor_version
Get editor version information
**Parameters**: None
**Example**:
```json
{
"tool": "server_get_editor_version",
"arguments": {}
}
```
### 8.4 server_get_project_name
Get current project name
**Parameters**: None
**Example**:
```json
{
"tool": "server_get_project_name",
"arguments": {}
}
```
### 8.5 server_get_project_path
Get current project path
**Parameters**: None
**Example**:
```json
{
"tool": "server_get_project_path",
"arguments": {}
}
```
### 8.6 server_get_project_uuid
Get current project UUID
**Parameters**: None
**Example**:
```json
{
"tool": "server_get_project_uuid",
"arguments": {}
}
```
### 8.7 server_restart_editor
Request to restart the editor
**Parameters**: None
**Example**:
```json
{
"tool": "server_restart_editor",
"arguments": {}
}
```
### 8.8 server_quit_editor
Request to quit the editor
**Parameters**: None
**Example**:
```json
{
"tool": "server_quit_editor",
"arguments": {}
}
```
---
## 9. Broadcast Tools
### 9.1 broadcast_get_broadcast_log
Get recent broadcast message log
**Parameters**:
- `limit` (number, optional): Number of latest messages to return, defaults to 50
- `messageType` (string, optional): Filter by message type
**Example**:
```json
{
"tool": "broadcast_get_broadcast_log",
"arguments": {
"limit": 100,
"messageType": "scene_change"
}
}
```
### 9.2 broadcast_listen_broadcast
Start listening for specific broadcast messages
**Parameters**:
- `messageType` (string, required): Message type to listen for
**Example**:
```json
{
"tool": "broadcast_listen_broadcast",
"arguments": {
"messageType": "node_created"
}
}
```
### 9.3 broadcast_stop_listening
Stop listening for specific broadcast messages
**Parameters**:
- `messageType` (string, required): Message type to stop listening for
**Example**:
```json
{
"tool": "broadcast_stop_listening",
"arguments": {
"messageType": "node_created"
}
}
```
### 9.4 broadcast_clear_broadcast_log
Clear broadcast message log
**Parameters**: None
**Example**:
```json
{
"tool": "broadcast_clear_broadcast_log",
"arguments": {}
}
```
### 9.5 broadcast_get_active_listeners
Get list of active broadcast listeners
**Parameters**: None
**Example**:
```json
{
"tool": "broadcast_get_active_listeners",
"arguments": {}
}
```
---
## Usage Guidelines
### 1. Tool Call Format
All tool calls use JSON-RPC 2.0 format:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "tool_name",
"arguments": {
// Tool parameters
}
},
"id": 1
}
```
### 2. Common UUID Retrieval Methods
- Use `node_get_all_nodes` to get all node UUIDs
- Use `node_find_node_by_name` to find node UUIDs by name
- Use `scene_get_current_scene` to get scene UUID
- Use `prefab_get_prefab_list` to get prefab information
### 3. Asset Path Format
Cocos Creator uses `db://` prefixed asset URL format:
- Scenes: `db://assets/scenes/GameScene.scene`
- Prefabs: `db://assets/prefabs/Player.prefab`
- Scripts: `db://assets/scripts/GameManager.ts`
- Textures: `db://assets/textures/player.png`
### 4. Error Handling
If a tool call fails, an error message will be returned:
```json
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Tool execution failed",
"data": {
"error": "Detailed error message"
}
}
}
```
### 5. Best Practices
1. **Query First, Then Operate**: Before modifying nodes or components, first use query tools to get current state
2. **Use UUIDs**: Prefer using UUIDs over names when referencing nodes and assets
3. **Error Checking**: Always check the return value of tool calls to ensure operations succeed
4. **Asset Management**: Before deleting or moving assets, ensure they are not referenced elsewhere
5. **Performance Considerations**: Avoid frequent tool calls in loops, consider batch operations
---
## Technical Support
If you encounter issues during use, you can:
1. Use `debug_get_console_logs` to view detailed error logs
2. Use `debug_validate_scene` to check if the scene has issues
3. Use `debug_get_editor_info` to get environment information
4. Check the MCP server's running status and logs
---
*This document is based on Cocos Creator MCP Server v1.3.0. Please refer to the latest version documentation for updates.*
```
--------------------------------------------------------------------------------
/source/tools/project-tools.ts:
--------------------------------------------------------------------------------
```typescript
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']
}
},
{
name: 'find_asset_by_name',
description: 'Find assets by name (supports partial matching and multiple results)',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Asset name to search for (supports partial matching)'
},
exactMatch: {
type: 'boolean',
description: 'Whether to use exact name matching',
default: false
},
assetType: {
type: 'string',
description: 'Filter by asset type',
enum: ['all', 'scene', 'prefab', 'script', 'texture', 'material', 'mesh', 'audio', 'animation', 'spriteFrame'],
default: 'all'
},
folder: {
type: 'string',
description: 'Folder to search in',
default: 'db://assets'
},
maxResults: {
type: 'number',
description: 'Maximum number of results to return',
default: 20,
minimum: 1,
maximum: 100
}
},
required: ['name']
}
},
{
name: 'get_asset_details',
description: 'Get detailed asset information including spriteFrame sub-assets',
inputSchema: {
type: 'object',
properties: {
assetPath: {
type: 'string',
description: 'Asset path (db://assets/...)'
},
includeSubAssets: {
type: 'boolean',
description: 'Include sub-assets like spriteFrame, texture',
default: true
}
},
required: ['assetPath']
}
}
];
}
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);
case 'find_asset_by_name':
return await this.findAssetByName(args);
case 'get_asset_details':
return await this.getAssetDetails(args.assetPath, args.includeSubAssets);
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
};
// Note: Preview module is not documented in official API
// Using fallback approach - open build panel as alternative
Editor.Message.request('builder', 'open').then(() => {
resolve({
success: true,
message: `Build panel opened. Preview functionality requires manual setup.`
});
}).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}`
};
// Note: Builder module only supports 'open' and 'query-worker-ready'
// Building requires manual interaction through the build panel
Editor.Message.request('builder', 'open').then(() => {
resolve({
success: true,
message: `Build panel opened for ${args.platform}. Please configure and start build manually.`,
data: {
platform: args.platform,
instruction: "Use the build panel to configure and start the build process"
}
});
}).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'
};
// Note: 'query-info' API doesn't exist, using 'query-config' instead
Editor.Message.request('project', 'query-config', 'project').then((additionalInfo: any) => {
if (additionalInfo) {
Object.assign(info, { config: 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}`;
}
}
// Note: query-assets API parameters corrected based on documentation
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 });
});
});
}
private async findAssetByName(args: any): Promise<ToolResponse> {
const { name, exactMatch = false, assetType = 'all', folder = 'db://assets', maxResults = 20 } = args;
return new Promise(async (resolve) => {
try {
// Get all assets in the specified folder
const allAssetsResponse = await this.getAssets(assetType, folder);
if (!allAssetsResponse.success || !allAssetsResponse.data) {
resolve({
success: false,
error: `Failed to get assets: ${allAssetsResponse.error}`
});
return;
}
const allAssets = allAssetsResponse.data.assets as any[];
let matchedAssets: any[] = [];
// Search for matching assets
for (const asset of allAssets) {
const assetName = asset.name;
let matches = false;
if (exactMatch) {
matches = assetName === name;
} else {
matches = assetName.toLowerCase().includes(name.toLowerCase());
}
if (matches) {
// Get detailed asset info if needed
try {
const detailResponse = await this.getAssetInfo(asset.path);
if (detailResponse.success) {
matchedAssets.push({
...asset,
details: detailResponse.data
});
} else {
matchedAssets.push(asset);
}
} catch {
matchedAssets.push(asset);
}
if (matchedAssets.length >= maxResults) {
break;
}
}
}
resolve({
success: true,
data: {
searchTerm: name,
exactMatch,
assetType,
folder,
totalFound: matchedAssets.length,
maxResults,
assets: matchedAssets,
message: `Found ${matchedAssets.length} assets matching '${name}'`
}
});
} catch (error: any) {
resolve({
success: false,
error: `Asset search failed: ${error.message}`
});
}
});
}
private async getAssetDetails(assetPath: string, includeSubAssets: boolean = true): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
// Get basic asset info
const assetInfoResponse = await this.getAssetInfo(assetPath);
if (!assetInfoResponse.success) {
resolve(assetInfoResponse);
return;
}
const assetInfo = assetInfoResponse.data;
const detailedInfo: any = {
...assetInfo,
subAssets: []
};
if (includeSubAssets && assetInfo) {
// For image assets, try to get spriteFrame and texture sub-assets
if (assetInfo.type === 'cc.ImageAsset' || assetPath.match(/\.(png|jpg|jpeg|gif|tga|bmp|psd)$/i)) {
// Generate common sub-asset UUIDs
const baseUuid = assetInfo.uuid;
const possibleSubAssets = [
{ type: 'spriteFrame', uuid: `${baseUuid}@f9941`, suffix: '@f9941' },
{ type: 'texture', uuid: `${baseUuid}@6c48a`, suffix: '@6c48a' },
{ type: 'texture2D', uuid: `${baseUuid}@6c48a`, suffix: '@6c48a' }
];
for (const subAsset of possibleSubAssets) {
try {
// Try to get URL for the sub-asset to verify it exists
const subAssetUrl = await Editor.Message.request('asset-db', 'query-url', subAsset.uuid);
if (subAssetUrl) {
detailedInfo.subAssets.push({
type: subAsset.type,
uuid: subAsset.uuid,
url: subAssetUrl,
suffix: subAsset.suffix
});
}
} catch {
// Sub-asset doesn't exist, skip it
}
}
}
}
resolve({
success: true,
data: {
assetPath,
includeSubAssets,
...detailedInfo,
message: `Asset details retrieved. Found ${detailedInfo.subAssets.length} sub-assets.`
}
});
} catch (error: any) {
resolve({
success: false,
error: `Failed to get asset details: ${error.message}`
});
}
});
}
}
```
--------------------------------------------------------------------------------
/source/tools/node-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolDefinition, ToolResponse, ToolExecutor, NodeInfo } from '../types';
import { ComponentTools } from './component-tools';
export class NodeTools implements ToolExecutor {
private componentTools = new ComponentTools();
getTools(): ToolDefinition[] {
return [
{
name: 'create_node',
description: 'Create a new node in the scene. Supports creating empty nodes, nodes with components, or instantiating from assets (prefabs, etc.). IMPORTANT: You should always provide parentUuid to specify where to create the node.',
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
},
assetUuid: {
type: 'string',
description: 'Asset UUID to instantiate from (e.g., prefab UUID). When provided, creates a node instance from the asset instead of an empty node.'
},
assetPath: {
type: 'string',
description: 'Asset path to instantiate from (e.g., "db://assets/prefabs/MyPrefab.prefab"). Alternative to assetUuid.'
},
components: {
type: 'array',
items: { type: 'string' },
description: 'Array of component type names to add to the new node (e.g., ["cc.Sprite", "cc.Button"])'
},
unlinkPrefab: {
type: 'boolean',
description: 'If true and creating from prefab, unlink from prefab to create a regular node',
default: false
},
keepWorldTransform: {
type: 'boolean',
description: 'Whether to keep world transform when creating the node',
default: false
},
initialTransform: {
type: 'object',
properties: {
position: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number' }
}
},
rotation: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number' }
}
},
scale: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number' }
}
}
},
description: 'Initial transform to apply to the created node'
}
},
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 (prefer using set_node_transform for active/layer/mobility/position/rotation/scale)',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
},
property: {
type: 'string',
description: 'Property name (e.g., active, name, layer)'
},
value: {
description: 'Property value'
}
},
required: ['uuid', 'property', 'value']
}
},
{
name: 'set_node_transform',
description: 'Set node transform properties (position, rotation, scale) with unified interface. Automatically handles 2D/3D node differences.',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
},
position: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number', description: 'Z coordinate (ignored for 2D nodes)' }
},
description: 'Node position. For 2D nodes, only x,y are used; z is ignored. For 3D nodes, all coordinates are used.'
},
rotation: {
type: 'object',
properties: {
x: { type: 'number', description: 'X rotation (ignored for 2D nodes)' },
y: { type: 'number', description: 'Y rotation (ignored for 2D nodes)' },
z: { type: 'number', description: 'Z rotation (main rotation axis for 2D nodes)' }
},
description: 'Node rotation in euler angles. For 2D nodes, only z rotation is used. For 3D nodes, all axes are used.'
},
scale: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number', description: 'Z scale (usually 1 for 2D nodes)' }
},
description: 'Node scale. For 2D nodes, z is typically 1. For 3D nodes, all axes are used.'
}
},
required: ['uuid']
}
},
{
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']
}
},
{
name: 'detect_node_type',
description: 'Detect if a node is 2D or 3D based on its components and properties',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID to analyze'
}
},
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 'set_node_transform':
return await this.setNodeTransform(args);
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);
case 'detect_node_type':
return await this.detectNodeType(args.uuid);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
private async createNode(args: any): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
let targetParentUuid = args.parentUuid;
// 如果没有提供父节点UUID,获取场景根节点
if (!targetParentUuid) {
try {
const sceneInfo = await Editor.Message.request('scene', 'query-node-tree');
if (sceneInfo && typeof sceneInfo === 'object' && !Array.isArray(sceneInfo) && Object.prototype.hasOwnProperty.call(sceneInfo, 'uuid')) {
targetParentUuid = (sceneInfo as any).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');
}
}
// 如果提供了assetPath,先解析为assetUuid
let finalAssetUuid = args.assetUuid;
if (args.assetPath && !finalAssetUuid) {
try {
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', args.assetPath);
if (assetInfo && assetInfo.uuid) {
finalAssetUuid = assetInfo.uuid;
console.log(`Asset path '${args.assetPath}' resolved to UUID: ${finalAssetUuid}`);
} else {
resolve({
success: false,
error: `Asset not found at path: ${args.assetPath}`
});
return;
}
} catch (err) {
resolve({
success: false,
error: `Failed to resolve asset path '${args.assetPath}': ${err}`
});
return;
}
}
// 构建create-node选项
const createNodeOptions: any = {
name: args.name
};
// 设置父节点
if (targetParentUuid) {
createNodeOptions.parent = targetParentUuid;
}
// 从资源实例化
if (finalAssetUuid) {
createNodeOptions.assetUuid = finalAssetUuid;
if (args.unlinkPrefab) {
createNodeOptions.unlinkPrefab = true;
}
}
// 添加组件
if (args.components && args.components.length > 0) {
createNodeOptions.components = args.components;
} else if (args.nodeType && args.nodeType !== 'Node' && !finalAssetUuid) {
// 只有在不从资源实例化时才添加nodeType组件
createNodeOptions.components = [args.nodeType];
}
// 保持世界变换
if (args.keepWorldTransform) {
createNodeOptions.keepWorldTransform = true;
}
// 不使用dump参数处理初始变换,创建后使用set_node_transform设置
console.log('Creating node with options:', createNodeOptions);
// 创建节点
const nodeUuid = await Editor.Message.request('scene', 'create-node', createNodeOptions);
const uuid = Array.isArray(nodeUuid) ? nodeUuid[0] : nodeUuid;
// 处理兄弟索引
if (args.siblingIndex !== undefined && args.siblingIndex >= 0 && uuid && targetParentUuid) {
try {
await new Promise(resolve => setTimeout(resolve, 100)); // 等待内部状态更新
await Editor.Message.request('scene', 'set-parent', {
parent: targetParentUuid,
uuids: [uuid],
keepWorldTransform: args.keepWorldTransform || false
});
} catch (err) {
console.warn('Failed to set sibling index:', err);
}
}
// 添加组件(如果提供的话)
if (args.components && args.components.length > 0 && uuid) {
try {
await new Promise(resolve => setTimeout(resolve, 100)); // 等待节点创建完成
for (const componentType of args.components) {
try {
const result = await this.componentTools.execute('add_component', {
nodeUuid: uuid,
componentType: componentType
});
if (result.success) {
console.log(`Component ${componentType} added successfully`);
} else {
console.warn(`Failed to add component ${componentType}:`, result.error);
}
} catch (err) {
console.warn(`Failed to add component ${componentType}:`, err);
}
}
} catch (err) {
console.warn('Failed to add components:', err);
}
}
// 设置初始变换(如果提供的话)
if (args.initialTransform && uuid) {
try {
await new Promise(resolve => setTimeout(resolve, 150)); // 等待节点和组件创建完成
await this.setNodeTransform({
uuid: uuid,
position: args.initialTransform.position,
rotation: args.initialTransform.rotation,
scale: args.initialTransform.scale
});
console.log('Initial transform applied successfully');
} catch (err) {
console.warn('Failed to set initial transform:', err);
}
}
// 获取创建后的节点信息进行验证
let verificationData: any = null;
try {
const nodeInfo = await this.getNodeInfo(uuid);
if (nodeInfo.success) {
verificationData = {
nodeInfo: nodeInfo.data,
creationDetails: {
parentUuid: targetParentUuid,
nodeType: args.nodeType || 'Node',
fromAsset: !!finalAssetUuid,
assetUuid: finalAssetUuid,
assetPath: args.assetPath,
timestamp: new Date().toISOString()
}
};
}
} catch (err) {
console.warn('Failed to get verification data:', err);
}
const successMessage = finalAssetUuid
? `Node '${args.name}' instantiated from asset successfully`
: `Node '${args.name}' created successfully`;
resolve({
success: true,
data: {
uuid: uuid,
name: args.name,
parentUuid: targetParentUuid,
nodeType: args.nodeType || 'Node',
fromAsset: !!finalAssetUuid,
assetUuid: finalAssetUuid,
message: successMessage
},
verificationData: verificationData
});
} catch (err: any) {
resolve({
success: false,
error: `Failed to create node: ${err.message}. Args: ${JSON.stringify(args)}`
});
}
});
}
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) => {
// 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<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(() => {
// Get comprehensive verification data including updated node info
this.getNodeInfo(uuid).then((nodeInfo) => {
resolve({
success: true,
message: `Property '${property}' updated successfully`,
data: {
nodeUuid: uuid,
property: property,
newValue: value
},
verificationData: {
nodeInfo: nodeInfo.data,
changeDetails: {
property: property,
value: value,
timestamp: new Date().toISOString()
}
}
});
}).catch(() => {
resolve({
success: true,
message: `Property '${property}' updated successfully (verification failed)`
});
});
}).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 setNodeTransform(args: any): Promise<ToolResponse> {
return new Promise(async (resolve) => {
const { uuid, position, rotation, scale } = args;
const updatePromises: Promise<any>[] = [];
const updates: string[] = [];
const warnings: string[] = [];
try {
// First get node info to determine if it's 2D or 3D
const nodeInfoResponse = await this.getNodeInfo(uuid);
if (!nodeInfoResponse.success || !nodeInfoResponse.data) {
resolve({ success: false, error: 'Failed to get node information' });
return;
}
const nodeInfo = nodeInfoResponse.data;
const is2DNode = this.is2DNode(nodeInfo);
if (position) {
const normalizedPosition = this.normalizeTransformValue(position, 'position', is2DNode);
if (normalizedPosition.warning) {
warnings.push(normalizedPosition.warning);
}
updatePromises.push(
Editor.Message.request('scene', 'set-property', {
uuid: uuid,
path: 'position',
dump: { value: normalizedPosition.value }
})
);
updates.push('position');
}
if (rotation) {
const normalizedRotation = this.normalizeTransformValue(rotation, 'rotation', is2DNode);
if (normalizedRotation.warning) {
warnings.push(normalizedRotation.warning);
}
updatePromises.push(
Editor.Message.request('scene', 'set-property', {
uuid: uuid,
path: 'rotation',
dump: { value: normalizedRotation.value }
})
);
updates.push('rotation');
}
if (scale) {
const normalizedScale = this.normalizeTransformValue(scale, 'scale', is2DNode);
if (normalizedScale.warning) {
warnings.push(normalizedScale.warning);
}
updatePromises.push(
Editor.Message.request('scene', 'set-property', {
uuid: uuid,
path: 'scale',
dump: { value: normalizedScale.value }
})
);
updates.push('scale');
}
if (updatePromises.length === 0) {
resolve({ success: false, error: 'No transform properties specified' });
return;
}
await Promise.all(updatePromises);
// Verify the changes by getting updated node info
const updatedNodeInfo = await this.getNodeInfo(uuid);
const response: any = {
success: true,
message: `Transform properties updated: ${updates.join(', ')} ${is2DNode ? '(2D node)' : '(3D node)'}`,
updatedProperties: updates,
data: {
nodeUuid: uuid,
nodeType: is2DNode ? '2D' : '3D',
appliedChanges: updates,
transformConstraints: {
position: is2DNode ? 'x, y only (z ignored)' : 'x, y, z all used',
rotation: is2DNode ? 'z only (x, y ignored)' : 'x, y, z all used',
scale: is2DNode ? 'x, y main, z typically 1' : 'x, y, z all used'
}
},
verificationData: {
nodeInfo: updatedNodeInfo.data,
transformDetails: {
originalNodeType: is2DNode ? '2D' : '3D',
appliedTransforms: updates,
timestamp: new Date().toISOString()
},
beforeAfterComparison: {
before: nodeInfo,
after: updatedNodeInfo.data
}
}
};
if (warnings.length > 0) {
response.warning = warnings.join('; ');
}
resolve(response);
} catch (err: any) {
resolve({
success: false,
error: `Failed to update transform: ${err.message}`
});
}
});
}
private is2DNode(nodeInfo: any): boolean {
// Check if node has 2D-specific components or is under Canvas
const components = nodeInfo.components || [];
// Check for common 2D components
const has2DComponents = components.some((comp: any) =>
comp.type && (
comp.type.includes('cc.Sprite') ||
comp.type.includes('cc.Label') ||
comp.type.includes('cc.Button') ||
comp.type.includes('cc.Layout') ||
comp.type.includes('cc.Widget') ||
comp.type.includes('cc.Mask') ||
comp.type.includes('cc.Graphics')
)
);
if (has2DComponents) {
return true;
}
// Check for 3D-specific components
const has3DComponents = components.some((comp: any) =>
comp.type && (
comp.type.includes('cc.MeshRenderer') ||
comp.type.includes('cc.Camera') ||
comp.type.includes('cc.Light') ||
comp.type.includes('cc.DirectionalLight') ||
comp.type.includes('cc.PointLight') ||
comp.type.includes('cc.SpotLight')
)
);
if (has3DComponents) {
return false;
}
// Default heuristic: if z position is 0 and hasn't been changed, likely 2D
const position = nodeInfo.position;
if (position && Math.abs(position.z) < 0.001) {
return true;
}
// Default to 3D if uncertain
return false;
}
private normalizeTransformValue(value: any, type: 'position' | 'rotation' | 'scale', is2D: boolean): { value: any, warning?: string } {
const result = { ...value };
let warning: string | undefined;
if (is2D) {
switch (type) {
case 'position':
if (value.z !== undefined && Math.abs(value.z) > 0.001) {
warning = `2D node: z position (${value.z}) ignored, set to 0`;
result.z = 0;
} else if (value.z === undefined) {
result.z = 0;
}
break;
case 'rotation':
if ((value.x !== undefined && Math.abs(value.x) > 0.001) ||
(value.y !== undefined && Math.abs(value.y) > 0.001)) {
warning = `2D node: x,y rotations ignored, only z rotation applied`;
result.x = 0;
result.y = 0;
} else {
result.x = result.x || 0;
result.y = result.y || 0;
}
result.z = result.z || 0;
break;
case 'scale':
if (value.z === undefined) {
result.z = 1; // Default scale for 2D
}
break;
}
} else {
// 3D node - ensure all axes are defined
result.x = result.x !== undefined ? result.x : (type === 'scale' ? 1 : 0);
result.y = result.y !== undefined ? result.y : (type === 'scale' ? 1 : 0);
result.z = result.z !== undefined ? result.z : (type === 'scale' ? 1 : 0);
}
return { value: result, warning };
}
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) => {
// 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<ToolResponse> {
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 });
});
});
}
private async detectNodeType(uuid: string): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
const nodeInfoResponse = await this.getNodeInfo(uuid);
if (!nodeInfoResponse.success || !nodeInfoResponse.data) {
resolve({ success: false, error: 'Failed to get node information' });
return;
}
const nodeInfo = nodeInfoResponse.data;
const is2D = this.is2DNode(nodeInfo);
const components = nodeInfo.components || [];
// Collect detection reasons
const detectionReasons: string[] = [];
// Check for 2D components
const twoDComponents = components.filter((comp: any) =>
comp.type && (
comp.type.includes('cc.Sprite') ||
comp.type.includes('cc.Label') ||
comp.type.includes('cc.Button') ||
comp.type.includes('cc.Layout') ||
comp.type.includes('cc.Widget') ||
comp.type.includes('cc.Mask') ||
comp.type.includes('cc.Graphics')
)
);
// Check for 3D components
const threeDComponents = components.filter((comp: any) =>
comp.type && (
comp.type.includes('cc.MeshRenderer') ||
comp.type.includes('cc.Camera') ||
comp.type.includes('cc.Light') ||
comp.type.includes('cc.DirectionalLight') ||
comp.type.includes('cc.PointLight') ||
comp.type.includes('cc.SpotLight')
)
);
if (twoDComponents.length > 0) {
detectionReasons.push(`Has 2D components: ${twoDComponents.map((c: any) => c.type).join(', ')}`);
}
if (threeDComponents.length > 0) {
detectionReasons.push(`Has 3D components: ${threeDComponents.map((c: any) => c.type).join(', ')}`);
}
// Check position for heuristic
const position = nodeInfo.position;
if (position && Math.abs(position.z) < 0.001) {
detectionReasons.push('Z position is ~0 (likely 2D)');
} else if (position && Math.abs(position.z) > 0.001) {
detectionReasons.push(`Z position is ${position.z} (likely 3D)`);
}
if (detectionReasons.length === 0) {
detectionReasons.push('No specific indicators found, defaulting based on heuristics');
}
resolve({
success: true,
data: {
nodeUuid: uuid,
nodeName: nodeInfo.name,
nodeType: is2D ? '2D' : '3D',
detectionReasons: detectionReasons,
components: components.map((comp: any) => ({
type: comp.type,
category: this.getComponentCategory(comp.type)
})),
position: nodeInfo.position,
transformConstraints: {
position: is2D ? 'x, y only (z ignored)' : 'x, y, z all used',
rotation: is2D ? 'z only (x, y ignored)' : 'x, y, z all used',
scale: is2D ? 'x, y main, z typically 1' : 'x, y, z all used'
}
}
});
} catch (err: any) {
resolve({
success: false,
error: `Failed to detect node type: ${err.message}`
});
}
});
}
private getComponentCategory(componentType: string): string {
if (!componentType) return 'unknown';
if (componentType.includes('cc.Sprite') || componentType.includes('cc.Label') ||
componentType.includes('cc.Button') || componentType.includes('cc.Layout') ||
componentType.includes('cc.Widget') || componentType.includes('cc.Mask') ||
componentType.includes('cc.Graphics')) {
return '2D';
}
if (componentType.includes('cc.MeshRenderer') || componentType.includes('cc.Camera') ||
componentType.includes('cc.Light') || componentType.includes('cc.DirectionalLight') ||
componentType.includes('cc.PointLight') || componentType.includes('cc.SpotLight')) {
return '3D';
}
return 'generic';
}
}
```