优化了组件属性的设置的接口,使AI调用起来更简单,更准确。每次更新场景信息、组件信息或者节点信息,端口将会返回最新的信息供AI进行追踪和比对。

This commit is contained in:
root
2025-07-23 18:19:12 +08:00
parent d7ab237707
commit ae604480ab
14 changed files with 2978 additions and 178 deletions

155
dist/mcp-server.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

250
dist/tools/validation-tools.js vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/types/index.js vendored
View File

@@ -1,3 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvdHlwZXMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgTUNQU2VydmVyU2V0dGluZ3Mge1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBhdXRvU3RhcnQ6IGJvb2xlYW47XG4gICAgZW5hYmxlRGVidWdMb2c6IGJvb2xlYW47XG4gICAgYWxsb3dlZE9yaWdpbnM6IHN0cmluZ1tdO1xuICAgIG1heENvbm5lY3Rpb25zOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2VydmVyU3RhdHVzIHtcbiAgICBydW5uaW5nOiBib29sZWFuO1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBjbGllbnRzOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbERlZmluaXRpb24ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBkZXNjcmlwdGlvbjogc3RyaW5nO1xuICAgIGlucHV0U2NoZW1hOiBhbnk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbFJlc3BvbnNlIHtcbiAgICBzdWNjZXNzOiBib29sZWFuO1xuICAgIGRhdGE/OiBhbnk7XG4gICAgbWVzc2FnZT86IHN0cmluZztcbiAgICBlcnJvcj86IHN0cmluZztcbiAgICBpbnN0cnVjdGlvbj86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBOb2RlSW5mbyB7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBhY3RpdmU6IGJvb2xlYW47XG4gICAgcG9zaXRpb24/OiB7IHg6IG51bWJlcjsgeTogbnVtYmVyOyB6OiBudW1iZXIgfTtcbiAgICByb3RhdGlvbj86IHsgeDogbnVtYmVyOyB5OiBudW1iZXI7IHo6IG51bWJlciB9O1xuICAgIHNjYWxlPzogeyB4OiBudW1iZXI7IHk6IG51bWJlcjsgejogbnVtYmVyIH07XG4gICAgcGFyZW50Pzogc3RyaW5nO1xuICAgIGNoaWxkcmVuPzogc3RyaW5nW107XG4gICAgY29tcG9uZW50cz86IENvbXBvbmVudEluZm9bXTtcbiAgICBsYXllcj86IG51bWJlcjtcbiAgICBtb2JpbGl0eT86IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb21wb25lbnRJbmZvIHtcbiAgICB0eXBlOiBzdHJpbmc7XG4gICAgZW5hYmxlZDogYm9vbGVhbjtcbiAgICBwcm9wZXJ0aWVzPzogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBTY2VuZUluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFByZWZhYkluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIGZvbGRlcjogc3RyaW5nO1xuICAgIGNyZWF0ZVRpbWU/OiBzdHJpbmc7XG4gICAgbW9kaWZ5VGltZT86IHN0cmluZztcbiAgICBkZXBlbmRlbmNpZXM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBBc3NldEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIHR5cGU6IHN0cmluZztcbiAgICBzaXplPzogbnVtYmVyO1xuICAgIGlzRGlyZWN0b3J5OiBib29sZWFuO1xuICAgIG1ldGE/OiB7XG4gICAgICAgIHZlcjogc3RyaW5nO1xuICAgICAgICBpbXBvcnRlcjogc3RyaW5nO1xuICAgIH07XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUHJvamVjdEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBwYXRoOiBzdHJpbmc7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIHZlcnNpb246IHN0cmluZztcbiAgICBjb2Nvc1ZlcnNpb246IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb25zb2xlTWVzc2FnZSB7XG4gICAgdGltZXN0YW1wOiBzdHJpbmc7XG4gICAgdHlwZTogJ2xvZycgfCAnd2FybicgfCAnZXJyb3InIHwgJ2luZm8nO1xuICAgIG1lc3NhZ2U6IHN0cmluZztcbiAgICBzdGFjaz86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBQZXJmb3JtYW5jZVN0YXRzIHtcbiAgICBub2RlQ291bnQ6IG51bWJlcjtcbiAgICBjb21wb25lbnRDb3VudDogbnVtYmVyO1xuICAgIGRyYXdDYWxsczogbnVtYmVyO1xuICAgIHRyaWFuZ2xlczogbnVtYmVyO1xuICAgIG1lbW9yeTogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBWYWxpZGF0aW9uSXNzdWUge1xuICAgIHR5cGU6ICdlcnJvcicgfCAnd2FybmluZycgfCAnaW5mbyc7XG4gICAgY2F0ZWdvcnk6IHN0cmluZztcbiAgICBtZXNzYWdlOiBzdHJpbmc7XG4gICAgZGV0YWlscz86IGFueTtcbiAgICBzdWdnZXN0aW9uPzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFZhbGlkYXRpb25SZXN1bHQge1xuICAgIHZhbGlkOiBib29sZWFuO1xuICAgIGlzc3VlQ291bnQ6IG51bWJlcjtcbiAgICBpc3N1ZXM6IFZhbGlkYXRpb25Jc3N1ZVtdO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1DUENsaWVudCB7XG4gICAgaWQ6IHN0cmluZztcbiAgICBsYXN0QWN0aXZpdHk6IERhdGU7XG4gICAgdXNlckFnZW50Pzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFRvb2xFeGVjdXRvciB7XG4gICAgZ2V0VG9vbHMoKTogVG9vbERlZmluaXRpb25bXTtcbiAgICBleGVjdXRlKHRvb2xOYW1lOiBzdHJpbmcsIGFyZ3M6IGFueSk6IFByb21pc2U8VG9vbFJlc3BvbnNlPjtcbn0iXX0=
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvdHlwZXMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgTUNQU2VydmVyU2V0dGluZ3Mge1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBhdXRvU3RhcnQ6IGJvb2xlYW47XG4gICAgZW5hYmxlRGVidWdMb2c6IGJvb2xlYW47XG4gICAgYWxsb3dlZE9yaWdpbnM6IHN0cmluZ1tdO1xuICAgIG1heENvbm5lY3Rpb25zOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2VydmVyU3RhdHVzIHtcbiAgICBydW5uaW5nOiBib29sZWFuO1xuICAgIHBvcnQ6IG51bWJlcjtcbiAgICBjbGllbnRzOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbERlZmluaXRpb24ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBkZXNjcmlwdGlvbjogc3RyaW5nO1xuICAgIGlucHV0U2NoZW1hOiBhbnk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVG9vbFJlc3BvbnNlIHtcbiAgICBzdWNjZXNzOiBib29sZWFuO1xuICAgIGRhdGE/OiBhbnk7XG4gICAgbWVzc2FnZT86IHN0cmluZztcbiAgICBlcnJvcj86IHN0cmluZztcbiAgICBpbnN0cnVjdGlvbj86IHN0cmluZztcbiAgICB3YXJuaW5nPzogc3RyaW5nO1xuICAgIHZlcmlmaWNhdGlvbkRhdGE/OiBhbnk7XG4gICAgdXBkYXRlZFByb3BlcnRpZXM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBOb2RlSW5mbyB7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBhY3RpdmU6IGJvb2xlYW47XG4gICAgcG9zaXRpb24/OiB7IHg6IG51bWJlcjsgeTogbnVtYmVyOyB6OiBudW1iZXIgfTtcbiAgICByb3RhdGlvbj86IHsgeDogbnVtYmVyOyB5OiBudW1iZXI7IHo6IG51bWJlciB9O1xuICAgIHNjYWxlPzogeyB4OiBudW1iZXI7IHk6IG51bWJlcjsgejogbnVtYmVyIH07XG4gICAgcGFyZW50Pzogc3RyaW5nO1xuICAgIGNoaWxkcmVuPzogc3RyaW5nW107XG4gICAgY29tcG9uZW50cz86IENvbXBvbmVudEluZm9bXTtcbiAgICBsYXllcj86IG51bWJlcjtcbiAgICBtb2JpbGl0eT86IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb21wb25lbnRJbmZvIHtcbiAgICB0eXBlOiBzdHJpbmc7XG4gICAgZW5hYmxlZDogYm9vbGVhbjtcbiAgICBwcm9wZXJ0aWVzPzogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBTY2VuZUluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFByZWZhYkluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIGZvbGRlcjogc3RyaW5nO1xuICAgIGNyZWF0ZVRpbWU/OiBzdHJpbmc7XG4gICAgbW9kaWZ5VGltZT86IHN0cmluZztcbiAgICBkZXBlbmRlbmNpZXM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBBc3NldEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB1dWlkOiBzdHJpbmc7XG4gICAgcGF0aDogc3RyaW5nO1xuICAgIHR5cGU6IHN0cmluZztcbiAgICBzaXplPzogbnVtYmVyO1xuICAgIGlzRGlyZWN0b3J5OiBib29sZWFuO1xuICAgIG1ldGE/OiB7XG4gICAgICAgIHZlcjogc3RyaW5nO1xuICAgICAgICBpbXBvcnRlcjogc3RyaW5nO1xuICAgIH07XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUHJvamVjdEluZm8ge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICBwYXRoOiBzdHJpbmc7XG4gICAgdXVpZDogc3RyaW5nO1xuICAgIHZlcnNpb246IHN0cmluZztcbiAgICBjb2Nvc1ZlcnNpb246IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb25zb2xlTWVzc2FnZSB7XG4gICAgdGltZXN0YW1wOiBzdHJpbmc7XG4gICAgdHlwZTogJ2xvZycgfCAnd2FybicgfCAnZXJyb3InIHwgJ2luZm8nO1xuICAgIG1lc3NhZ2U6IHN0cmluZztcbiAgICBzdGFjaz86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBQZXJmb3JtYW5jZVN0YXRzIHtcbiAgICBub2RlQ291bnQ6IG51bWJlcjtcbiAgICBjb21wb25lbnRDb3VudDogbnVtYmVyO1xuICAgIGRyYXdDYWxsczogbnVtYmVyO1xuICAgIHRyaWFuZ2xlczogbnVtYmVyO1xuICAgIG1lbW9yeTogUmVjb3JkPHN0cmluZywgYW55Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBWYWxpZGF0aW9uSXNzdWUge1xuICAgIHR5cGU6ICdlcnJvcicgfCAnd2FybmluZycgfCAnaW5mbyc7XG4gICAgY2F0ZWdvcnk6IHN0cmluZztcbiAgICBtZXNzYWdlOiBzdHJpbmc7XG4gICAgZGV0YWlscz86IGFueTtcbiAgICBzdWdnZXN0aW9uPzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFZhbGlkYXRpb25SZXN1bHQge1xuICAgIHZhbGlkOiBib29sZWFuO1xuICAgIGlzc3VlQ291bnQ6IG51bWJlcjtcbiAgICBpc3N1ZXM6IFZhbGlkYXRpb25Jc3N1ZVtdO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1DUENsaWVudCB7XG4gICAgaWQ6IHN0cmluZztcbiAgICBsYXN0QWN0aXZpdHk6IERhdGU7XG4gICAgdXNlckFnZW50Pzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFRvb2xFeGVjdXRvciB7XG4gICAgZ2V0VG9vbHMoKTogVG9vbERlZmluaXRpb25bXTtcbiAgICBleGVjdXRlKHRvb2xOYW1lOiBzdHJpbmcsIGFyZ3M6IGFueSk6IFByb21pc2U8VG9vbFJlc3BvbnNlPjtcbn0iXX0=

View File

@@ -15,6 +15,7 @@ import { SceneAdvancedTools } from './tools/scene-advanced-tools';
import { SceneViewTools } from './tools/scene-view-tools';
import { ReferenceImageTools } from './tools/reference-image-tools';
import { AssetAdvancedTools } from './tools/asset-advanced-tools';
import { ValidationTools } from './tools/validation-tools';
export class MCPServer {
private settings: MCPServerSettings;
@@ -44,6 +45,7 @@ export class MCPServer {
this.tools.sceneView = new SceneViewTools();
this.tools.referenceImage = new ReferenceImageTools();
this.tools.assetAdvanced = new AssetAdvancedTools();
this.tools.validation = new ValidationTools();
console.log('[MCPServer] Tools initialized successfully');
} catch (error) {
console.error('[MCPServer] Error initializing tools:', error);
@@ -146,6 +148,11 @@ export class MCPServer {
} else if (pathname === '/health' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify({ status: 'ok', tools: this.toolsList.length }));
} else if (pathname?.startsWith('/api/') && req.method === 'POST') {
await this.handleSimpleAPIRequest(req, res, pathname);
} else if (pathname === '/api/tools' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify({ tools: this.getSimplifiedToolsList() }));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
@@ -166,11 +173,25 @@ export class MCPServer {
req.on('end', async () => {
try {
const message = JSON.parse(body);
// Enhanced JSON parsing with better error handling
let message;
try {
message = JSON.parse(body);
} catch (parseError: any) {
// Try to fix common JSON issues
const fixedBody = this.fixCommonJsonIssues(body);
try {
message = JSON.parse(fixedBody);
console.log('[MCPServer] Fixed JSON parsing issue');
} catch (secondError) {
throw new Error(`JSON parsing failed: ${parseError.message}. Original body: ${body.substring(0, 500)}...`);
}
}
const response = await this.handleMessage(message);
res.writeHead(200);
res.end(JSON.stringify(response));
} catch (error) {
} catch (error: any) {
console.error('Error handling MCP request:', error);
res.writeHead(400);
res.end(JSON.stringify({
@@ -178,7 +199,7 @@ export class MCPServer {
id: null,
error: {
code: -32700,
message: 'Parse error'
message: `Parse error: ${error.message}`
}
}));
}
@@ -234,6 +255,27 @@ export class MCPServer {
}
}
private fixCommonJsonIssues(jsonStr: string): string {
let fixed = jsonStr;
// Fix common escape character issues
fixed = fixed
// Fix unescaped quotes in strings
.replace(/([^\\])"([^"]*[^\\])"([^,}\]:])/g, '$1\\"$2\\"$3')
// Fix unescaped backslashes
.replace(/([^\\])\\([^"\\\/bfnrt])/g, '$1\\\\$2')
// Fix trailing commas
.replace(/,(\s*[}\]])/g, '$1')
// Fix single quotes (should be double quotes)
.replace(/'/g, '"')
// Fix common control characters
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
return fixed;
}
public stop(): void {
if (this.httpServer) {
this.httpServer.close();
@@ -252,6 +294,123 @@ export class MCPServer {
};
}
private async handleSimpleAPIRequest(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', async () => {
try {
// Extract tool name from path like /api/node/set_position
const pathParts = pathname.split('/').filter(p => p);
if (pathParts.length < 3) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid API path. Use /api/{category}/{tool_name}' }));
return;
}
const category = pathParts[1];
const toolName = pathParts[2];
const fullToolName = `${category}_${toolName}`;
// Parse parameters with enhanced error handling
let params;
try {
params = body ? JSON.parse(body) : {};
} catch (parseError: any) {
// Try to fix JSON issues
const fixedBody = this.fixCommonJsonIssues(body);
try {
params = JSON.parse(fixedBody);
console.log('[MCPServer] Fixed API JSON parsing issue');
} catch (secondError: any) {
res.writeHead(400);
res.end(JSON.stringify({
error: 'Invalid JSON in request body',
details: parseError.message,
receivedBody: body.substring(0, 200)
}));
return;
}
}
// Execute tool
const result = await this.executeToolCall(fullToolName, params);
res.writeHead(200);
res.end(JSON.stringify({
success: true,
tool: fullToolName,
result: result
}));
} catch (error: any) {
console.error('Simple API error:', error);
res.writeHead(500);
res.end(JSON.stringify({
success: false,
error: error.message,
tool: pathname
}));
}
});
}
private getSimplifiedToolsList(): any[] {
return this.toolsList.map(tool => {
const parts = tool.name.split('_');
const category = parts[0];
const toolName = parts.slice(1).join('_');
return {
name: tool.name,
category: category,
toolName: toolName,
description: tool.description,
apiPath: `/api/${category}/${toolName}`,
curlExample: this.generateCurlExample(category, toolName, tool.inputSchema)
};
});
}
private generateCurlExample(category: string, toolName: string, schema: any): string {
// Generate sample parameters based on schema
const sampleParams = this.generateSampleParams(schema);
const jsonString = JSON.stringify(sampleParams, null, 2);
return `curl -X POST http://127.0.0.1:8585/api/${category}/${toolName} \\
-H "Content-Type: application/json" \\
-d '${jsonString}'`;
}
private generateSampleParams(schema: any): any {
if (!schema || !schema.properties) return {};
const sample: any = {};
for (const [key, prop] of Object.entries(schema.properties as any)) {
const propSchema = prop as any;
switch (propSchema.type) {
case 'string':
sample[key] = propSchema.default || 'example_string';
break;
case 'number':
sample[key] = propSchema.default || 42;
break;
case 'boolean':
sample[key] = propSchema.default || true;
break;
case 'object':
sample[key] = propSchema.default || { x: 0, y: 0, z: 0 };
break;
default:
sample[key] = 'example_value';
}
}
return sample;
}
public updateSettings(settings: MCPServerSettings) {
this.settings = settings;
if (this.httpServer) {

View File

@@ -73,24 +73,25 @@ export class ComponentTools implements ToolExecutor {
},
{
name: 'set_component_property',
description: 'Set component property value',
description: 'Set component property value - AI只需提供4个简单参数节点UUID、组件名称、属性名称、属性值',
inputSchema: {
type: 'object',
properties: {
nodeUuid: {
type: 'string',
description: 'Node UUID'
description: 'Node UUID - 节点的UUID'
},
componentType: {
type: 'string',
description: 'Component type'
description: 'Component type - 组件类型',
enum: ['cc.Label', 'cc.Sprite', 'cc.Button', 'cc.Toggle', 'cc.Slider', 'cc.ScrollView', 'cc.EditBox', 'cc.ProgressBar', 'cc.RichText', 'cc.Mask', 'cc.Graphics', 'cc.Layout', 'cc.Widget', 'cc.UITransform']
},
property: {
type: 'string',
description: 'Property name'
description: 'Property name - 属性名称,常见值: string(文本), color(颜色), fontSize(字体大小), spriteFrame(精灵帧), enabled(启用状态), position(位置), scale(缩放), rotation(旋转)'
},
value: {
description: 'Property value'
description: 'Property value - 属性值,支持的类型:\n• 字符串: "Hello World"\n• 数字: 32, 1.5\n• 布尔值: true, false\n• 颜色对象: {"r":255,"g":0,"b":0,"a":255} 或 "#FF0000"\n• 向量对象: {"x":100,"y":50} 或 {"x":1,"y":2,"z":3}\n• 尺寸对象: {"width":100,"height":50}\n• 资源UUID: "asset-uuid-string"'
}
},
required: ['nodeUuid', 'componentType', 'property', 'value']
@@ -160,12 +161,41 @@ export class ComponentTools implements ToolExecutor {
uuid: nodeUuid,
component: componentType
}).then((result: any) => {
resolve({
success: true,
data: {
componentId: result,
message: `Component '${componentType}' added successfully`
}
// Get comprehensive verification data including node info and all components
Promise.all([
this.getComponents(nodeUuid),
this.getComponentInfo(nodeUuid, componentType)
]).then(([allComponentsInfo, newComponentInfo]) => {
const addedComponent = allComponentsInfo.data?.components?.find((comp: any) => comp.type === componentType);
resolve({
success: true,
data: {
componentId: result,
nodeUuid: nodeUuid,
componentType: componentType,
message: `Component '${componentType}' added successfully`,
componentVerified: !!addedComponent
},
verificationData: {
addedComponent: newComponentInfo.data,
allNodeComponents: allComponentsInfo.data,
componentCount: allComponentsInfo.data?.components?.length || 0,
verificationStatus: {
componentExists: !!addedComponent,
componentDetails: addedComponent || null
}
}
});
}).catch(() => {
resolve({
success: true,
data: {
componentId: result,
nodeUuid: nodeUuid,
componentType: componentType,
message: `Component '${componentType}' added successfully (verification failed)`
}
});
});
}).catch((err: Error) => {
// 备用方案:使用场景脚本
@@ -206,7 +236,7 @@ export class ComponentTools implements ToolExecutor {
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
if (nodeData && nodeData.__comps__) {
const components = nodeData.__comps__.map((comp: any) => ({
type: comp.__type__ || 'Unknown',
type: comp.__type__ || comp.cid || comp.type || 'Unknown',
enabled: comp.enabled !== undefined ? comp.enabled : true,
properties: this.extractComponentProperties(comp)
}));
@@ -250,7 +280,10 @@ export class ComponentTools implements ToolExecutor {
// 优先尝试直接使用 Editor API 查询节点信息
Editor.Message.request('scene', 'query-node', nodeUuid).then((nodeData: any) => {
if (nodeData && nodeData.__comps__) {
const component = nodeData.__comps__.find((comp: any) => comp.__type__ === componentType);
const component = nodeData.__comps__.find((comp: any) => {
const compType = comp.__type__ || comp.cid || comp.type;
return compType === componentType;
});
if (component) {
resolve({
@@ -315,58 +348,182 @@ export class ComponentTools implements ToolExecutor {
}
private async setComponentProperty(args: any): Promise<ToolResponse> {
return new Promise((resolve) => {
// 首先获取节点信息以找到正确的组件索引
Editor.Message.request('scene', 'query-node', args.nodeUuid).then((nodeData: any) => {
if (!nodeData || !nodeData.__comps__) {
throw new Error('Node not found or no components data');
const { nodeUuid, componentType, property, value } = args;
return new Promise(async (resolve) => {
try {
console.log(`[ComponentTools] Setting ${componentType}.${property} = ${JSON.stringify(value)} on node ${nodeUuid}`);
// Step 1: 获取组件信息使用与getComponents相同的方法
const componentsResponse = await this.getComponents(nodeUuid);
if (!componentsResponse.success || !componentsResponse.data) {
resolve({
success: false,
error: `Failed to get components for node '${nodeUuid}': ${componentsResponse.error}`
});
return;
}
// 查找组件索引
let componentIndex = -1;
for (let i = 0; i < nodeData.__comps__.length; i++) {
const comp = nodeData.__comps__[i];
if (comp.__type__ === args.componentType) {
componentIndex = i;
const allComponents = componentsResponse.data.components;
// Step 2: 查找目标组件
let targetComponent = null;
const availableTypes: string[] = [];
for (let i = 0; i < allComponents.length; i++) {
const comp = allComponents[i];
availableTypes.push(comp.type);
if (comp.type === componentType) {
targetComponent = comp;
break;
}
}
if (componentIndex === -1) {
throw new Error(`Component '${args.componentType}' not found on node`);
if (!targetComponent) {
resolve({
success: false,
error: `Component '${componentType}' not found on node. Available components: ${availableTypes.join(', ')}`
});
return;
}
// 使用正确的组件索引路径
const propertyPath = `__comps__.${componentIndex}.${args.property}`;
// Step 3: 自动检测和转换属性值
const propertyInfo = this.analyzeProperty(targetComponent, property);
if (!propertyInfo.exists) {
resolve({
success: false,
error: `Property '${property}' not found on component '${componentType}'. Available properties: ${propertyInfo.availableProperties.join(', ')}`
});
return;
}
return Editor.Message.request('scene', 'set-property', {
uuid: args.nodeUuid,
path: propertyPath,
dump: {
value: args.value
const processedValue = this.smartConvertValue(value, propertyInfo);
const originalValue = propertyInfo.originalValue;
console.log(`[ComponentTools] Converting value: ${JSON.stringify(value)} -> ${JSON.stringify(processedValue)} (type: ${propertyInfo.type})`);
// Step 4: 设置属性值
// 需要重新获取原始节点数据来构建正确的路径
const rawNodeData = await Editor.Message.request('scene', 'query-node', nodeUuid);
if (!rawNodeData || !rawNodeData.__comps__) {
resolve({
success: false,
error: `Failed to get raw node data for property setting`
});
return;
}
// 找到原始组件的索引
let rawComponentIndex = -1;
for (let i = 0; i < rawNodeData.__comps__.length; i++) {
const comp = rawNodeData.__comps__[i] as any;
const compType = comp.__type__ || comp.cid || comp.type || 'Unknown';
if (compType === componentType) {
rawComponentIndex = i;
break;
}
});
}).then(() => {
}
if (rawComponentIndex === -1) {
resolve({
success: false,
error: `Could not find component index for setting property`
});
return;
}
// 构建正确的属性路径
let propertyPath = `__comps__.${rawComponentIndex}.${property}`;
// 特殊处理资源类属性
if (propertyInfo.type === 'asset') {
// 对于资源类属性,需要特殊处理
const assetValue = typeof processedValue === 'string' ?
{ uuid: processedValue } : processedValue;
// Determine asset type based on property name
let assetType = 'cc.SpriteFrame'; // default
if (property.toLowerCase().includes('texture')) {
assetType = 'cc.Texture2D';
} else if (property.toLowerCase().includes('material')) {
assetType = 'cc.Material';
} else if (property.toLowerCase().includes('font')) {
assetType = 'cc.Font';
} else if (property.toLowerCase().includes('clip')) {
assetType = 'cc.AudioClip';
}
// Try multiple approaches for setting asset properties
try {
// Approach 1: Direct property setting with asset structure
await Editor.Message.request('scene', 'set-property', {
uuid: nodeUuid,
path: propertyPath,
dump: {
value: assetValue,
type: assetType
}
});
} catch (error1) {
try {
// Approach 2: Try with different structure
await Editor.Message.request('scene', 'set-property', {
uuid: nodeUuid,
path: propertyPath,
dump: {
value: {
__uuid__: assetValue.uuid || assetValue
}
}
});
} catch (error2) {
// Approach 3: Try direct UUID assignment
await Editor.Message.request('scene', 'set-property', {
uuid: nodeUuid,
path: propertyPath,
dump: { value: assetValue.uuid || assetValue }
});
}
}
} else {
// Normal property setting for non-asset properties
await Editor.Message.request('scene', 'set-property', {
uuid: nodeUuid,
path: propertyPath,
dump: { value: processedValue }
});
}
// Step 5: 验证设置结果
const verification = await this.verifyPropertyChange(nodeUuid, componentType, property, originalValue, processedValue);
resolve({
success: true,
message: `Component property '${args.property}' updated successfully`
message: `Successfully set ${componentType}.${property} = ${JSON.stringify(processedValue)}`,
data: {
nodeUuid,
componentType,
property,
originalValue,
newValue: processedValue,
actualValue: verification.actualValue,
changeVerified: verification.verified
},
verificationData: verification.fullData
});
}).catch((err: Error) => {
// 备用方案:使用场景脚本
const options = {
name: 'cocos-mcp-server',
method: 'setComponentProperty',
args: [args.nodeUuid, args.componentType, args.property, args.value]
};
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
resolve(result);
}).catch((err2: Error) => {
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
} catch (error: any) {
console.error(`[ComponentTools] Error setting property:`, error);
resolve({
success: false,
error: `Failed to set property: ${error.message}`
});
});
}
});
}
private async attachScript(nodeUuid: string, scriptPath: string): Promise<ToolResponse> {
return new Promise((resolve) => {
// 从脚本路径提取组件类名
@@ -400,7 +557,7 @@ export class ComponentTools implements ToolExecutor {
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
resolve(result);
}).catch((err2: Error) => {
}).catch(() => {
resolve({
success: false,
error: `Failed to attach script '${scriptName}': ${err.message}`,
@@ -442,4 +599,245 @@ export class ComponentTools implements ToolExecutor {
}
};
}
private analyzeProperty(component: any, propertyName: string): { exists: boolean; type: string; availableProperties: string[]; originalValue: any } {
// 从复杂的组件结构中提取可用属性
const availableProperties: string[] = [];
let propertyValue: any = undefined;
let propertyExists = false;
// 尝试多种方式查找属性:
// 1. 直接属性访问
if (propertyName in component) {
propertyValue = component[propertyName];
propertyExists = true;
}
// 2. 从嵌套结构中查找 (如从测试数据看到的复杂结构)
if (!propertyExists && component.properties && typeof component.properties === 'object') {
// 首先检查properties.value是否存在这是我们在getComponents中看到的结构
if (component.properties.value && typeof component.properties.value === 'object') {
const valueObj = component.properties.value;
for (const [key, propData] of Object.entries(valueObj)) {
if (typeof propData === 'object' && propData && 'value' in propData) {
const propInfo = propData as any;
availableProperties.push(key);
if (key === propertyName) {
propertyValue = propInfo.value;
propertyExists = true;
}
}
}
} else {
// 备用方案直接从properties查找
for (const [key, propData] of Object.entries(component.properties)) {
if (typeof propData === 'object' && propData && 'value' in propData) {
const propInfo = propData as any;
availableProperties.push(key);
if (key === propertyName) {
propertyValue = propInfo.value;
propertyExists = true;
}
}
}
}
}
// 3. 从直接属性中提取简单属性名
if (availableProperties.length === 0) {
for (const key of Object.keys(component)) {
if (!key.startsWith('_') && !['__type__', 'cid', 'node', 'uuid', 'name', 'enabled', 'type', 'readonly', 'visible'].includes(key)) {
availableProperties.push(key);
}
}
}
if (!propertyExists) {
return {
exists: false,
type: 'unknown',
availableProperties,
originalValue: undefined
};
}
let type = 'unknown';
// 智能类型检测
if (typeof propertyValue === 'string') {
// Check if property name suggests it's an asset
if (['spriteFrame', 'texture', 'material', 'font', 'clip', 'prefab'].includes(propertyName.toLowerCase())) {
type = 'asset';
} else {
type = 'string';
}
} else if (typeof propertyValue === 'number') {
type = 'number';
} else if (typeof propertyValue === 'boolean') {
type = 'boolean';
} else if (propertyValue && typeof propertyValue === 'object') {
if ('r' in propertyValue && 'g' in propertyValue && 'b' in propertyValue) {
type = 'color';
} else if ('x' in propertyValue && 'y' in propertyValue) {
type = propertyValue.z !== undefined ? 'vec3' : 'vec2';
} else if ('width' in propertyValue && 'height' in propertyValue) {
type = 'size';
} else if ('uuid' in propertyValue || '__uuid__' in propertyValue) {
type = 'asset';
} else {
type = 'object';
}
} else if (propertyValue === null || propertyValue === undefined) {
// For null/undefined values, check property name to determine type
if (['spriteFrame', 'texture', 'material', 'font', 'clip', 'prefab'].includes(propertyName.toLowerCase())) {
type = 'asset';
} else {
type = 'unknown';
}
}
return {
exists: true,
type,
availableProperties,
originalValue: propertyValue
};
}
private smartConvertValue(inputValue: any, propertyInfo: any): any {
const { type, originalValue } = propertyInfo;
console.log(`[smartConvertValue] Converting ${JSON.stringify(inputValue)} to type: ${type}`);
switch (type) {
case 'string':
return String(inputValue);
case 'number':
return Number(inputValue);
case 'boolean':
if (typeof inputValue === 'boolean') return inputValue;
if (typeof inputValue === 'string') {
return inputValue.toLowerCase() === 'true' || inputValue === '1';
}
return Boolean(inputValue);
case 'color':
if (typeof inputValue === 'object' && inputValue !== null) {
// 如果输入是颜色对象,直接使用
if ('r' in inputValue || 'g' in inputValue || 'b' in inputValue) {
return {
r: Number(inputValue.r) || 0,
g: Number(inputValue.g) || 0,
b: Number(inputValue.b) || 0,
a: Number(inputValue.a) !== undefined ? Number(inputValue.a) : 255
};
}
} else if (typeof inputValue === 'string') {
// 如果是字符串,尝试解析为十六进制颜色
return this.parseColorString(inputValue);
}
// 保持原值结构,只更新提供的值
return {
r: Number(inputValue.r) || originalValue.r || 255,
g: Number(inputValue.g) || originalValue.g || 255,
b: Number(inputValue.b) || originalValue.b || 255,
a: Number(inputValue.a) !== undefined ? Number(inputValue.a) : (originalValue.a || 255)
};
case 'vec2':
if (typeof inputValue === 'object' && inputValue !== null) {
return {
x: Number(inputValue.x) || originalValue.x || 0,
y: Number(inputValue.y) || originalValue.y || 0
};
}
return originalValue;
case 'vec3':
if (typeof inputValue === 'object' && inputValue !== null) {
return {
x: Number(inputValue.x) || originalValue.x || 0,
y: Number(inputValue.y) || originalValue.y || 0,
z: Number(inputValue.z) || originalValue.z || 0
};
}
return originalValue;
case 'size':
if (typeof inputValue === 'object' && inputValue !== null) {
return {
width: Number(inputValue.width) || originalValue.width || 100,
height: Number(inputValue.height) || originalValue.height || 100
};
}
return originalValue;
case 'asset':
if (typeof inputValue === 'string') {
// 如果输入是字符串路径转换为asset对象
return { uuid: inputValue };
} else if (typeof inputValue === 'object' && inputValue !== null) {
return inputValue;
}
return originalValue;
default:
// 对于未知类型,尽量保持原有结构
if (typeof inputValue === typeof originalValue) {
return inputValue;
}
return originalValue;
}
}
private parseColorString(colorStr: string): { r: number; g: number; b: number; a: number } {
// 简单的颜色字符串解析(支持#RRGGBB格式 // cSpell:ignore RRGGBB
if (colorStr.startsWith('#') && colorStr.length === 7) {
const r = parseInt(colorStr.substring(1, 3), 16);
const g = parseInt(colorStr.substring(3, 5), 16);
const b = parseInt(colorStr.substring(5, 7), 16);
return { r, g, b, a: 255 };
}
// 默认返回白色
return { r: 255, g: 255, b: 255, a: 255 };
}
private async verifyPropertyChange(nodeUuid: string, componentType: string, property: string, originalValue: any, expectedValue: any): Promise<{ verified: boolean; actualValue: any; fullData: any }> {
try {
// 重新获取组件信息进行验证
const componentInfo = await this.getComponentInfo(nodeUuid, componentType);
const allComponents = await this.getComponents(nodeUuid);
if (componentInfo.success && componentInfo.data) {
const actualValue = componentInfo.data.properties?.[property];
const verified = JSON.stringify(actualValue) !== JSON.stringify(originalValue);
return {
verified,
actualValue,
fullData: {
updatedComponent: componentInfo.data,
allNodeComponents: allComponents.data,
changeDetails: {
property,
before: originalValue,
expected: expectedValue,
actual: actualValue,
verified
}
}
};
}
} catch (error) {
console.warn('[verifyPropertyChange] Verification failed:', error);
}
return {
verified: false,
actualValue: undefined,
fullData: null
};
}
}

View File

@@ -89,7 +89,7 @@ export class NodeTools implements ToolExecutor {
},
{
name: 'set_node_property',
description: 'Set node property value',
description: 'Set node property value (prefer using set_node_transform for position/rotation/scale)',
inputSchema: {
type: 'object',
properties: {
@@ -99,7 +99,7 @@ export class NodeTools implements ToolExecutor {
},
property: {
type: 'string',
description: 'Property name (e.g., position, rotation, scale, active)'
description: 'Property name (e.g., active, name, layer)'
},
value: {
description: 'Property value'
@@ -108,6 +108,47 @@ export class NodeTools implements ToolExecutor {
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',
@@ -163,6 +204,20 @@ export class NodeTools implements ToolExecutor {
},
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']
}
}
];
}
@@ -181,12 +236,16 @@ export class NodeTools implements ToolExecutor {
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}`);
}
@@ -254,44 +313,67 @@ export class NodeTools implements ToolExecutor {
Editor.Message.request('scene', 'create-node', createNodeOptions).then((nodeUuid: any) => {
// 如果需要设置特定的兄弟索引使用set-parent API
// 添加延迟以避免内部状态竞争
if (args.siblingIndex !== undefined && args.siblingIndex >= 0 && nodeUuid) {
Editor.Message.request('scene', 'set-parent', {
parent: targetParentUuid,
uuids: [nodeUuid],
keepWorldTransform: false
}).then(() => {
setTimeout(() => {
Editor.Message.request('scene', 'set-parent', {
parent: targetParentUuid,
uuids: [nodeUuid],
keepWorldTransform: false
}).then(() => {
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
parentUuid: targetParentUuid,
message: args.parentUuid
? `Node '${args.name}' created under specified parent`
: `Node '${args.name}' created at scene root (no parent specified)`
}
});
}).catch(() => {
// 即使移动失败,节点已创建,返回成功但带警告
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
message: `Node '${args.name}' created but may not be under intended parent`,
warning: 'Failed to move node to specified parent'
}
});
});
}, 100); // 100ms延迟
} else {
// Get complete node info for verification
this.getNodeInfo(nodeUuid).then((nodeInfo) => {
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
parentUuid: targetParentUuid,
message: args.parentUuid
? `Node '${args.name}' created under specified parent`
: `Node '${args.name}' created at scene root (no parent specified)`
message: `Node '${args.name}' created successfully`
},
verificationData: {
nodeInfo: nodeInfo.data,
creationDetails: {
parentUuid: targetParentUuid,
nodeType: args.nodeType || 'Node',
timestamp: new Date().toISOString()
}
}
});
}).catch(() => {
// 即使移动失败,节点已创建,返回成功但带警告
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
message: `Node '${args.name}' created but may not be under intended parent`,
warning: 'Failed to move node to specified parent'
message: `Node '${args.name}' created successfully (verification failed)`
}
});
});
} else {
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
message: `Node '${args.name}' created successfully`
}
});
}
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
@@ -527,9 +609,30 @@ export class NodeTools implements ToolExecutor {
value: value
}
}).then(() => {
resolve({
success: true,
message: `Property '${property}' updated successfully`
// 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) => {
// 如果直接设置失败,尝试使用场景脚本
@@ -548,6 +651,215 @@ export class NodeTools implements ToolExecutor {
});
}
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(() => {
@@ -595,4 +907,113 @@ export class NodeTools implements ToolExecutor {
});
});
}
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';
}
}

View File

@@ -331,6 +331,62 @@ export class ProjectTools implements ToolExecutor {
},
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']
}
}
];
}
@@ -381,6 +437,10 @@ export class ProjectTools implements ToolExecutor {
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}`);
}
@@ -896,4 +956,143 @@ export class ProjectTools implements ToolExecutor {
});
});
}
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}`
});
}
});
}
}

View File

@@ -364,14 +364,30 @@ export class SceneTools implements ToolExecutor {
], null, 2);
Editor.Message.request('asset-db', 'create-asset', fullPath, sceneContent).then((result: any) => {
resolve({
success: true,
data: {
uuid: result.uuid,
url: result.url,
name: sceneName,
message: `Scene '${sceneName}' created successfully`
}
// Verify scene creation by checking if it exists
this.getSceneList().then((sceneList) => {
const createdScene = sceneList.data?.find((scene: any) => scene.uuid === result.uuid);
resolve({
success: true,
data: {
uuid: result.uuid,
url: result.url,
name: sceneName,
message: `Scene '${sceneName}' created successfully`,
sceneVerified: !!createdScene
},
verificationData: createdScene
});
}).catch(() => {
resolve({
success: true,
data: {
uuid: result.uuid,
url: result.url,
name: sceneName,
message: `Scene '${sceneName}' created successfully (verification failed)`
}
});
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });

View File

@@ -0,0 +1,263 @@
import { ToolDefinition, ToolResponse, ToolExecutor } from '../types';
export class ValidationTools implements ToolExecutor {
getTools(): ToolDefinition[] {
return [
{
name: 'validate_json_params',
description: 'Validate and fix JSON parameters before sending to other tools',
inputSchema: {
type: 'object',
properties: {
jsonString: {
type: 'string',
description: 'JSON string to validate and fix'
},
expectedSchema: {
type: 'object',
description: 'Expected parameter schema (optional)'
}
},
required: ['jsonString']
}
},
{
name: 'safe_string_value',
description: 'Create a safe string value that won\'t cause JSON parsing issues',
inputSchema: {
type: 'object',
properties: {
value: {
type: 'string',
description: 'String value to make safe'
}
},
required: ['value']
}
},
{
name: 'format_mcp_request',
description: 'Format a complete MCP request with proper JSON escaping',
inputSchema: {
type: 'object',
properties: {
toolName: {
type: 'string',
description: 'Tool name to call'
},
arguments: {
type: 'object',
description: 'Tool arguments'
}
},
required: ['toolName', 'arguments']
}
}
];
}
async execute(toolName: string, args: any): Promise<ToolResponse> {
switch (toolName) {
case 'validate_json_params':
return await this.validateJsonParams(args.jsonString, args.expectedSchema);
case 'safe_string_value':
return await this.createSafeStringValue(args.value);
case 'format_mcp_request':
return await this.formatMcpRequest(args.toolName, args.arguments);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
private async validateJsonParams(jsonString: string, expectedSchema?: any): Promise<ToolResponse> {
try {
// First try to parse as-is
let parsed;
try {
parsed = JSON.parse(jsonString);
} catch (error: any) {
// Try to fix common issues
const fixed = this.fixJsonString(jsonString);
try {
parsed = JSON.parse(fixed);
} catch (secondError) {
return {
success: false,
error: `Cannot fix JSON: ${error.message}`,
data: {
originalJson: jsonString,
fixedAttempt: fixed,
suggestions: this.getJsonFixSuggestions(jsonString)
}
};
}
}
// Validate against schema if provided
if (expectedSchema) {
const validation = this.validateAgainstSchema(parsed, expectedSchema);
if (!validation.valid) {
return {
success: false,
error: 'Schema validation failed',
data: {
parsedJson: parsed,
validationErrors: validation.errors,
suggestions: validation.suggestions
}
};
}
}
return {
success: true,
data: {
parsedJson: parsed,
fixedJson: JSON.stringify(parsed, null, 2),
isValid: true
}
};
} catch (error: any) {
return {
success: false,
error: error.message
};
}
}
private async createSafeStringValue(value: string): Promise<ToolResponse> {
const safeValue = this.escapJsonString(value);
return {
success: true,
data: {
originalValue: value,
safeValue: safeValue,
jsonReady: JSON.stringify(safeValue),
usage: `Use "${safeValue}" in your JSON parameters`
}
};
}
private async formatMcpRequest(toolName: string, toolArgs: any): Promise<ToolResponse> {
try {
const mcpRequest = {
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: {
name: toolName,
arguments: toolArgs
}
};
const formattedJson = JSON.stringify(mcpRequest, null, 2);
const compactJson = JSON.stringify(mcpRequest);
return {
success: true,
data: {
request: mcpRequest,
formattedJson: formattedJson,
compactJson: compactJson,
curlCommand: this.generateCurlCommand(compactJson)
}
};
} catch (error: any) {
return {
success: false,
error: `Failed to format MCP request: ${error.message}`
};
}
}
private fixJsonString(jsonStr: string): string {
let fixed = jsonStr;
// Fix common escape character issues
fixed = fixed
// Fix unescaped quotes in string values
.replace(/(\{[^}]*"[^"]*":\s*")([^"]*")([^"]*")([^}]*\})/g, (match, prefix, content, suffix, end) => {
const escapedContent = content.replace(/"/g, '\\"');
return prefix + escapedContent + suffix + end;
})
// Fix unescaped backslashes
.replace(/([^\\])\\([^"\\\/bfnrtu])/g, '$1\\\\$2')
// Fix trailing commas
.replace(/,(\s*[}\]])/g, '$1')
// Fix control characters
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
// Fix single quotes to double quotes
.replace(/'/g, '"');
return fixed;
}
private escapJsonString(str: string): string {
return str
.replace(/\\/g, '\\\\') // Escape backslashes first
.replace(/"/g, '\\"') // Escape quotes
.replace(/\n/g, '\\n') // Escape newlines
.replace(/\r/g, '\\r') // Escape carriage returns
.replace(/\t/g, '\\t') // Escape tabs
.replace(/\f/g, '\\f') // Escape form feeds
.replace(/\b/g, '\\b'); // Escape backspaces
}
private validateAgainstSchema(data: any, schema: any): { valid: boolean; errors: string[]; suggestions: string[] } {
const errors: string[] = [];
const suggestions: string[] = [];
// Basic type checking
if (schema.type) {
const actualType = Array.isArray(data) ? 'array' : typeof data;
if (actualType !== schema.type) {
errors.push(`Expected type ${schema.type}, got ${actualType}`);
suggestions.push(`Convert value to ${schema.type}`);
}
}
// Required fields checking
if (schema.required && Array.isArray(schema.required)) {
for (const field of schema.required) {
if (!(field in data)) {
errors.push(`Missing required field: ${field}`);
suggestions.push(`Add required field "${field}"`);
}
}
}
return {
valid: errors.length === 0,
errors,
suggestions
};
}
private getJsonFixSuggestions(jsonStr: string): string[] {
const suggestions: string[] = [];
if (jsonStr.includes('\\"')) {
suggestions.push('Check for improperly escaped quotes');
}
if (jsonStr.includes("'")) {
suggestions.push('Replace single quotes with double quotes');
}
if (jsonStr.includes('\n') || jsonStr.includes('\t')) {
suggestions.push('Escape newlines and tabs properly');
}
if (jsonStr.match(/,\s*[}\]]/)) {
suggestions.push('Remove trailing commas');
}
return suggestions;
}
private generateCurlCommand(jsonStr: string): string {
const escapedJson = jsonStr.replace(/'/g, "'\"'\"'");
return `curl -X POST http://127.0.0.1:8585/mcp \\
-H "Content-Type: application/json" \\
-d '${escapedJson}'`;
}
}

View File

@@ -24,6 +24,9 @@ export interface ToolResponse {
message?: string;
error?: string;
instruction?: string;
warning?: string;
verificationData?: any;
updatedProperties?: string[];
}
export interface NodeInfo {