From 1af6f08c94e276187d53ed26e7b04dc8f9f7a76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=AB=E7=84=B0=E5=BA=93=E6=8B=89?= Date: Thu, 29 Jan 2026 13:47:38 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(mcp-bridge):=20=E6=B7=BB=E5=8A=A0=20?= =?UTF-8?q?MCP=20=E6=A1=A5=E6=8E=A5=E6=8F=92=E4=BB=B6=E5=AE=9E=E7=8E=B0=20?= =?UTF-8?q?Cocos=20Creator=20=E4=B8=8E=E5=A4=96=E9=83=A8=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 main.js 实现 MCP 服务器,提供 HTTP 接口供外部工具调用 - 实现 5 个核心工具接口:获取选中节点、修改节点名称、保存场景、 获取场景层级结构、更新节点变换属性 - 添加 panel 面板用于测试 MCP 功能,包含节点 ID 获取和名称修改功能 - 实现场景脚本 scene-script.js 处理节点属性修改和层级数据导出 - 配置 package.json 定义插件入口文件和菜单项 - 支持跨域请求便于调试,返回符合 MCP 规范的工具定义格式 ``` --- main.js | 186 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 20 +++++ panel/index.html | 17 +++++ panel/index.js | 59 +++++++++++++++ scene-script.js | 91 +++++++++++++++++++++++ 5 files changed, 373 insertions(+) create mode 100644 main.js create mode 100644 package.json create mode 100644 panel/index.html create mode 100644 panel/index.js create mode 100644 scene-script.js diff --git a/main.js b/main.js new file mode 100644 index 0000000..35eb99e --- /dev/null +++ b/main.js @@ -0,0 +1,186 @@ +"use strict"; + +const http = require("http"); + +module.exports = { + "scene-script": "scene-script.js", + load() { + // 插件加载时启动一个微型服务器供 MCP 使用 (默认端口 3000) + this.startMcpServer(); + }, + + unload() { + if (this.server) this.server.close(); + }, + + // 暴露给 MCP 或面板的 API 封装 + messages: { + "open-test-panel"() { + Editor.Panel.open("mcp-bridge"); + }, + + // 获取当前选中节点信息 + "get-selected-info"(event) { + let selection = Editor.Selection.curSelection("node"); + if (event) event.reply(null, selection); + return selection; + }, + + // 修改场景中的节点(需要通过 scene-script) + "set-node-property"(event, args) { + Editor.log("Calling scene script with:", args); // 打印日志确认 main 进程收到了面板的消息 + + // 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致 + Editor.Scene.callSceneScript("mcp-bridge", "set-property", args, (err, result) => { + if (err) { + Editor.error("Scene Script Error:", err); + } + if (event && event.reply) { + event.reply(err, result); + } + }); + }, + }, + + // 简易 MCP 桥接服务器 + startMcpServer() { + const http = require("http"); + + this.server = 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"); + + if (req.method === "OPTIONS") { + res.end(); + return; + } + + let body = ""; + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + try { + // 简单的路由处理 + if (req.url === "/list-tools" && req.method === "GET") { + // 1. 返回工具定义 (符合 MCP 规范) + const tools = [ + { + 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"], + }, + }, + ]; + res.end(JSON.stringify({ tools })); + } else if (req.url === "/call-tool" && req.method === "POST") { + // 2. 执行工具逻辑 + const { name, arguments: args } = JSON.parse(body); + + if (name === "get_selected_node") { + let ids = Editor.Selection.curSelection("node"); + res.end(JSON.stringify({ content: [{ type: "text", text: JSON.stringify(ids) }] })); + } else if (name === "set_node_name") { + Editor.Scene.callSceneScript( + "mcp-bridge", + "set-property", + { + id: args.id, + path: "name", + value: args.newName, + }, + (err, result) => { + res.end( + JSON.stringify({ + content: [ + { type: "text", text: err ? `Error: ${err}` : `Success: ${result}` }, + ], + }), + ); + }, + ); + } else if (name === "save_scene") { + // 触发编辑器保存指令 + Editor.Ipc.sendToMain("scene:save-scene"); + res.end(JSON.stringify({ content: [{ type: "text", text: "Scene saved successfully" }] })); + } else if (name === "get_scene_hierarchy") { + Editor.Scene.callSceneScript("mcp-bridge", "get-hierarchy", (err, hierarchy) => { + if (err) { + res.end( + JSON.stringify({ + content: [ + { type: "text", text: "Error fetching hierarchy: " + err.message }, + ], + }), + ); + } else { + res.end( + JSON.stringify({ + content: [{ type: "text", text: JSON.stringify(hierarchy, null, 2) }], + }), + ); + } + }); + } else if (name === "update_node_transform") { + Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", args, (err, result) => { + res.end( + JSON.stringify({ + content: [{ type: "text", text: err ? `Error: ${err}` : result }], + }), + ); + }); + } + } else { + res.statusCode = 404; + res.end(JSON.stringify({ error: "Not Found" })); + } + } catch (e) { + res.statusCode = 500; + res.end(JSON.stringify({ error: e.message })); + } + }); + }); + + this.server.listen(3456); + Editor.log("MCP Server standard interface listening on http://localhost:3456"); + }, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..954238b --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "mcp-bridge", + "version": "1.0.0", + "description": "Cocos Creator MCP Bridge", + "author": "User", + "main": "main.js", + "scene-script": "scene-script.js", + "main-menu": { + "Packages/MCP Bridge/Open Test Panel": { + "message": "mcp-bridge:open-test-panel" + } + }, + "panel": { + "main": "panel/index.js", + "type": "dockable", + "title": "MCP Test Panel", + "width": 400, + "height": 300 + } +} diff --git a/panel/index.html b/panel/index.html new file mode 100644 index 0000000..70a9bbd --- /dev/null +++ b/panel/index.html @@ -0,0 +1,17 @@ +
+ + + + + + + +
+ 获取选中节点 ID + 修改节点名称 +
+ +
+ Status: Ready +
+
\ No newline at end of file diff --git a/panel/index.js b/panel/index.js new file mode 100644 index 0000000..0c2276d --- /dev/null +++ b/panel/index.js @@ -0,0 +1,59 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +Editor.Panel.extend({ + // 读取样式和模板 + style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"), + template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"), + + // 面板渲染成功后的回调 + ready() { + // 使用 querySelector 确保能拿到元素,避免依赖可能为 undefined 的 this.$ + const btnGet = this.shadowRoot.querySelector("#btn-get"); + const btnSet = this.shadowRoot.querySelector("#btn-set"); + const nodeIdInput = this.shadowRoot.querySelector("#nodeId"); + const newNameInput = this.shadowRoot.querySelector("#newName"); + const logDiv = this.shadowRoot.querySelector("#log"); + + if (!btnGet || !btnSet) { + Editor.error("Failed to find UI elements. Check if IDs in HTML match."); + return; + } + + // 测试获取信息 + btnGet.addEventListener("confirm", () => { + Editor.Ipc.sendToMain("mcp-bridge:get-selected-info", (err, ids) => { + if (ids && ids.length > 0) { + nodeIdInput.value = ids[0]; + logDiv.innerText = "Status: Selected Node " + ids[0]; + } else { + logDiv.innerText = "Status: No node selected"; + } + }); + }); + + // 测试修改信息 + btnSet.addEventListener("confirm", () => { + let data = { + id: nodeIdInput.value, + path: "name", + value: newNameInput.value, + }; + + if (!data.id) { + logDiv.innerText = "Error: Please get Node ID first"; + return; + } + + Editor.Ipc.sendToMain("mcp-bridge:set-node-property", data, (err, res) => { + if (err) { + logDiv.innerText = "Error: " + err; + } else { + logDiv.innerText = "Success: " + res; + } + }); + }); + }, +}); diff --git a/scene-script.js b/scene-script.js new file mode 100644 index 0000000..615b0ea --- /dev/null +++ b/scene-script.js @@ -0,0 +1,91 @@ +"use strict"; + +module.exports = { + "set-property": function (event, args) { + const { id, path, value } = args; + + // 1. 获取节点 + let node = cc.engine.getInstanceById(id); + + if (node) { + // 2. 修改属性 + if (path === "name") { + node.name = value; + } else { + node[path] = value; + } + + // 3. 【解决报错的关键】告诉编辑器场景变脏了(需要保存) + // 在场景进程中,我们发送 IPC 给主进程 + Editor.Ipc.sendToMain("scene:dirty"); + + // 4. 【额外补丁】通知层级管理器(Hierarchy)同步更新节点名称 + // 否则你修改了名字,层级管理器可能还是显示旧名字 + Editor.Ipc.sendToAll("scene:node-changed", { + uuid: id, + }); + + if (event.reply) { + event.reply(null, `Node ${id} updated to ${value}`); + } + } else { + if (event.reply) { + event.reply(new Error("Scene Script: Node not found " + id)); + } + } + }, + "get-hierarchy": function (event) { + const scene = cc.director.getScene(); + + function dumpNodes(node) { + // 【优化】跳过编辑器内部的私有节点,减少数据量 + if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { + return null; + } + + let nodeData = { + name: node.name, + uuid: node.uuid, + active: node.active, + position: { x: Math.round(node.x), y: Math.round(node.y) }, + scale: { x: node.scaleX, y: node.scaleY }, + size: { width: node.width, height: node.height }, + // 记录组件类型,让 AI 知道这是个什么节点 + components: node._components.map((c) => c.__typename), + children: [], + }; + + for (let i = 0; i < node.childrenCount; i++) { + let childData = dumpNodes(node.children[i]); + if (childData) nodeData.children.push(childData); + } + return nodeData; + } + + const hierarchy = dumpNodes(scene); + if (event.reply) event.reply(null, hierarchy); + }, + + "update-node-transform": function (event, args) { + const { id, x, y, scaleX, scaleY, color } = args; + let node = cc.engine.getInstanceById(id); + + if (node) { + if (x !== undefined) node.x = x; + if (y !== undefined) node.y = y; + if (scaleX !== undefined) node.scaleX = scaleX; + if (scaleY !== undefined) node.scaleY = scaleY; + if (color) { + // color 格式如 "#FF0000" + node.color = new cc.Color().fromHEX(color); + } + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: id }); + + if (event.reply) event.reply(null, "Transform updated"); + } else { + if (event.reply) event.reply(new Error("Node not found")); + } + }, +};