```
feat(main): 添加MCP工具列表API和优化服务器路由 - 新增getToolsList函数,提供完整的编辑器操作工具集 - 包含节点操作、场景管理、预制体创建等9个核心功能 - 重构HTTP服务器路由逻辑,分离工具列表和调用接口 - 移除冗余的CORS头设置,简化请求处理流程 - 统一错误处理和日志记录机制 feat(proxy): 实现MCP协议代理服务 - 创建mcp-proxy.js作为独立的协议转换层 - 支持initialize、tools/list、tools/call方法 - 实现与Cocos编辑器的HTTP通信桥接 - 提供详细的调试日志和错误处理机制 ```
This commit is contained in:
314
main.js
314
main.js
@@ -63,6 +63,111 @@ const getNewSceneTemplate = () => {
|
|||||||
return JSON.stringify(sceneData);
|
return JSON.stringify(sceneData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getToolsList = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "get_selected_node",
|
||||||
|
description: "获取当前编辑器中选中的节点 ID",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set_node_name",
|
||||||
|
description: "修改指定节点的名称",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "节点的 UUID" },
|
||||||
|
newName: { type: "string", description: "新的节点名称" },
|
||||||
|
},
|
||||||
|
required: ["id", "newName"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "save_scene",
|
||||||
|
description: "保存当前场景的修改",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_scene_hierarchy",
|
||||||
|
description: "获取当前场景的完整节点树结构(包括 UUID、名称和层级关系)",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_node_transform",
|
||||||
|
description: "修改节点的坐标、缩放或颜色",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "节点 UUID" },
|
||||||
|
x: { type: "number" },
|
||||||
|
y: { type: "number" },
|
||||||
|
scaleX: { type: "number" },
|
||||||
|
scaleY: { type: "number" },
|
||||||
|
color: { type: "string", description: "HEX 颜色代码如 #FF0000" },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_scene",
|
||||||
|
description: "在 assets 目录下创建一个新的场景文件",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sceneName: { type: "string", description: "场景名称" },
|
||||||
|
},
|
||||||
|
required: ["sceneName"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_prefab",
|
||||||
|
description: "将场景中的某个节点保存为预制体资源",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nodeId: { type: "string", description: "节点 UUID" },
|
||||||
|
prefabName: { type: "string", description: "预制体名称" },
|
||||||
|
},
|
||||||
|
required: ["nodeId", "prefabName"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "open_scene",
|
||||||
|
description: "在编辑器中打开指定的场景文件",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
description: "场景资源路径,如 db://assets/NewScene.fire",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_node",
|
||||||
|
description: "在当前场景中创建一个新节点",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string", description: "节点名称" },
|
||||||
|
parentId: {
|
||||||
|
type: "string",
|
||||||
|
description: "父节点 UUID (可选,不传则挂在场景根部)",
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["empty", "sprite", "label"],
|
||||||
|
description: "节点预设类型",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"scene-script": "scene-script.js",
|
"scene-script": "scene-script.js",
|
||||||
load() {
|
load() {
|
||||||
@@ -94,185 +199,66 @@ module.exports = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
mcpServer = http.createServer((req, res) => {
|
mcpServer = http.createServer((req, res) => {
|
||||||
// 设置 CORS 方便调试
|
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
if (req.method === "OPTIONS") {
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = "";
|
let body = "";
|
||||||
req.on("data", (chunk) => {
|
req.on("data", (chunk) => {
|
||||||
body += chunk;
|
body += chunk;
|
||||||
});
|
});
|
||||||
req.on("end", () => {
|
req.on("end", () => {
|
||||||
try {
|
const url = req.url;
|
||||||
// 简单的路由处理
|
if (url === "/list-tools") {
|
||||||
if (req.url === "/list-tools" && req.method === "GET") {
|
const tools = getToolsList();
|
||||||
// 1. 返回工具定义 (符合 MCP 规范)
|
addLog("info", `AI Client requested tool list`);
|
||||||
const tools = [
|
// 明确返回成功结构
|
||||||
{
|
res.writeHead(200);
|
||||||
name: "get_selected_node",
|
return res.end(JSON.stringify({ tools: tools }));
|
||||||
description: "获取当前编辑器中选中的节点 ID",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "set_node_name",
|
|
||||||
description: "修改指定节点的名称",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
id: { type: "string", description: "节点的 UUID" },
|
|
||||||
newName: { type: "string", description: "新的节点名称" },
|
|
||||||
},
|
|
||||||
required: ["id", "newName"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "save_scene",
|
|
||||||
description: "保存当前场景的修改",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "get_scene_hierarchy",
|
|
||||||
description: "获取当前场景的完整节点树结构(包括 UUID、名称和层级关系)",
|
|
||||||
inputSchema: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "update_node_transform",
|
|
||||||
description: "修改节点的坐标、缩放或颜色",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
id: { type: "string", description: "节点 UUID" },
|
|
||||||
x: { type: "number" },
|
|
||||||
y: { type: "number" },
|
|
||||||
scaleX: { type: "number" },
|
|
||||||
scaleY: { type: "number" },
|
|
||||||
color: { type: "string", description: "HEX 颜色代码如 #FF0000" },
|
|
||||||
},
|
|
||||||
required: ["id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_scene",
|
|
||||||
description: "在 assets 目录下创建一个新的场景文件",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
sceneName: { type: "string", description: "场景名称" },
|
|
||||||
},
|
|
||||||
required: ["sceneName"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_prefab",
|
|
||||||
description: "将场景中的某个节点保存为预制体资源",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
nodeId: { type: "string", description: "节点 UUID" },
|
|
||||||
prefabName: { type: "string", description: "预制体名称" },
|
|
||||||
},
|
|
||||||
required: ["nodeId", "prefabName"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "open_scene",
|
|
||||||
description: "在编辑器中打开指定的场景文件",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
url: {
|
|
||||||
type: "string",
|
|
||||||
description: "场景资源路径,如 db://assets/NewScene.fire",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["url"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_node",
|
|
||||||
description: "在当前场景中创建一个新节点",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
name: { type: "string", description: "节点名称" },
|
|
||||||
parentId: {
|
|
||||||
type: "string",
|
|
||||||
description: "父节点 UUID (可选,不传则挂在场景根部)",
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["empty", "sprite", "label"],
|
|
||||||
description: "节点预设类型",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["name"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return res.end(JSON.stringify({ tools }));
|
|
||||||
}
|
|
||||||
if (req.url === "/call-tool" && req.method === "POST") {
|
|
||||||
try {
|
|
||||||
const { name, arguments: args } = JSON.parse(body);
|
|
||||||
|
|
||||||
addLog("mcp", `REQ -> [${name}] ${JSON.stringify(args)}`);
|
|
||||||
|
|
||||||
this.handleMcpCall(name, args, (err, result) => {
|
|
||||||
// 3. 构建 MCP 标准响应格式
|
|
||||||
const response = {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: err
|
|
||||||
? `Error: ${err}`
|
|
||||||
: typeof result === "object"
|
|
||||||
? JSON.stringify(result, null, 2)
|
|
||||||
: result,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4. 记录返回日志
|
|
||||||
if (err) {
|
|
||||||
addLog("error", `RES <- [${name}] Failed: ${err}`);
|
|
||||||
} else {
|
|
||||||
// 日志里只显示简短的返回值,防止长 JSON(如 hierarchy)刷屏
|
|
||||||
const logRes = typeof result === "object" ? "[Object Data]" : result;
|
|
||||||
addLog("success", `RES <- [${name}] Success: ${logRes}`);
|
|
||||||
}
|
|
||||||
res.end(JSON.stringify(response));
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
addLog("error", `Parse Error: ${e.message}`);
|
|
||||||
res.end(JSON.stringify({ content: [{ type: "text", text: `Error: ${e.message}` }] }));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end(JSON.stringify({ error: "Not Found" }));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end(JSON.stringify({ error: e.message }));
|
|
||||||
}
|
}
|
||||||
|
if (url === "/call-tool") {
|
||||||
|
try {
|
||||||
|
const { name, arguments: args } = JSON.parse(body || "{}");
|
||||||
|
addLog("mcp", `REQ -> [${name}]`);
|
||||||
|
|
||||||
|
this.handleMcpCall(name, args, (err, result) => {
|
||||||
|
const response = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: err
|
||||||
|
? `Error: ${err}`
|
||||||
|
: typeof result === "object"
|
||||||
|
? JSON.stringify(result, null, 2)
|
||||||
|
: result,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
addLog(err ? "error" : "success", `RES <- [${name}]`);
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addLog("error", `JSON Parse Error: ${e.message}`);
|
||||||
|
res.writeHead(400);
|
||||||
|
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 兜底处理 (404) ---
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end(JSON.stringify({ error: "Not Found", url: url }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mcpServer.on("error", (e) => {
|
||||||
|
addLog("error", `Server Error: ${e.message}`);
|
||||||
|
});
|
||||||
mcpServer.listen(port, () => {
|
mcpServer.listen(port, () => {
|
||||||
serverConfig.active = true;
|
serverConfig.active = true;
|
||||||
addLog("success", `Server started on port ${port}`);
|
addLog("success", `MCP Server running at http://127.0.0.1:${port}`);
|
||||||
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig);
|
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
mcpServer.on("error", (err) => {
|
|
||||||
addLog("error", `Server Error: ${err.message}`);
|
|
||||||
this.stopServer();
|
|
||||||
});
|
|
||||||
// 启动成功后顺便存一下端口
|
// 启动成功后顺便存一下端口
|
||||||
this.getProfile().set("last-port", port);
|
this.getProfile().set("last-port", port);
|
||||||
this.getProfile().save();
|
this.getProfile().save();
|
||||||
|
|||||||
101
mcp-proxy.js
Normal file
101
mcp-proxy.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const COCOS_PORT = 3456;
|
||||||
|
|
||||||
|
function debugLog(msg) {
|
||||||
|
process.stderr.write(`[Proxy Debug] ${msg}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdin.on('data', (data) => {
|
||||||
|
const lines = data.toString().split('\n');
|
||||||
|
lines.forEach(line => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
try {
|
||||||
|
const request = JSON.parse(line);
|
||||||
|
handleRequest(request);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleRequest(req) {
|
||||||
|
const { method, id, params } = req;
|
||||||
|
|
||||||
|
if (method === 'initialize') {
|
||||||
|
sendToAI({
|
||||||
|
jsonrpc: "2.0", id: id,
|
||||||
|
result: {
|
||||||
|
protocolVersion: "2024-11-05",
|
||||||
|
capabilities: { tools: {} },
|
||||||
|
serverInfo: { name: "cocos-bridge", version: "1.0.0" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'tools/list') {
|
||||||
|
// 使用 GET 获取列表
|
||||||
|
forwardToCocos('/list-tools', null, id, 'GET');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'tools/call') {
|
||||||
|
// 使用 POST 执行工具
|
||||||
|
forwardToCocos('/call-tool', {
|
||||||
|
name: params.name,
|
||||||
|
arguments: params.arguments
|
||||||
|
}, id, 'POST');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
function forwardToCocos(path, payload, id, method = 'POST') {
|
||||||
|
const postData = payload ? JSON.stringify(payload) : '';
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: COCOS_PORT,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (postData) {
|
||||||
|
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = http.request(options, (res) => {
|
||||||
|
let resData = '';
|
||||||
|
res.on('data', d => resData += d);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const cocosRes = JSON.parse(resData);
|
||||||
|
|
||||||
|
// 检查关键字段
|
||||||
|
if (path === '/list-tools' && !cocosRes.tools) {
|
||||||
|
// 如果报错,把 Cocos 返回的所有内容打印到 Trae 的 stderr 日志里
|
||||||
|
debugLog(`CRITICAL: Cocos returned no tools. Received: ${resData}`);
|
||||||
|
sendError(id, -32603, "Invalid Cocos response: missing tools array");
|
||||||
|
} else {
|
||||||
|
sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugLog(`JSON Parse Error. Cocos Sent: ${resData}`);
|
||||||
|
sendError(id, -32603, "Cocos returned non-JSON data");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', (e) => {
|
||||||
|
debugLog(`Cocos is offline: ${e.message}`);
|
||||||
|
sendError(id, -32000, "Cocos Plugin Offline");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postData) request.write(postData);
|
||||||
|
request.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToAI(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
|
||||||
|
function sendError(id, code, message) {
|
||||||
|
sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user