2026-01-29 13:47:38 +08:00
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
|
|
const http = require("http");
|
2026-01-29 14:26:28 +08:00
|
|
|
|
const path = require("path");
|
|
|
|
|
|
|
|
|
|
|
|
const getNewSceneTemplate = () => {
|
|
|
|
|
|
// 尝试获取 UUID 生成函数
|
|
|
|
|
|
let newId = "";
|
|
|
|
|
|
if (Editor.Utils && Editor.Utils.uuid) {
|
|
|
|
|
|
newId = Editor.Utils.uuid();
|
|
|
|
|
|
} else if (Editor.Utils && Editor.Utils.UuidUtils && Editor.Utils.UuidUtils.uuid) {
|
|
|
|
|
|
newId = Editor.Utils.UuidUtils.uuid();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 兜底方案:如果找不到编辑器 API,生成一个随机字符串
|
|
|
|
|
|
newId = Math.random().toString(36).substring(2, 15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sceneData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
__type__: "cc.SceneAsset",
|
|
|
|
|
|
_name: "",
|
|
|
|
|
|
_objFlags: 0,
|
|
|
|
|
|
_native: "",
|
|
|
|
|
|
scene: { __id__: 1 },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
__id__: 1,
|
|
|
|
|
|
__type__: "cc.Scene",
|
|
|
|
|
|
_name: "",
|
|
|
|
|
|
_objFlags: 0,
|
|
|
|
|
|
_parent: null,
|
|
|
|
|
|
_children: [],
|
|
|
|
|
|
_active: true,
|
|
|
|
|
|
_level: 0,
|
|
|
|
|
|
_components: [],
|
|
|
|
|
|
autoReleaseAssets: false,
|
|
|
|
|
|
_id: newId,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
return JSON.stringify(sceneData);
|
|
|
|
|
|
};
|
2026-01-29 13:47:38 +08:00
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
|
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"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-29 14:26:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
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"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-29 13:47:38 +08:00
|
|
|
|
];
|
2026-01-29 14:26:28 +08:00
|
|
|
|
return res.end(JSON.stringify({ tools }));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (req.url === "/call-tool" && req.method === "POST") {
|
2026-01-29 13:47:38 +08:00
|
|
|
|
// 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 }],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
2026-01-29 14:26:28 +08:00
|
|
|
|
} else if (name === "create_scene") {
|
|
|
|
|
|
const url = `db://assets/${args.sceneName}.fire`;
|
|
|
|
|
|
if (Editor.assetdb.exists(url)) {
|
|
|
|
|
|
return res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
content: [{ type: "text", text: "Error: Scene already exists" }],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成标准场景内容
|
|
|
|
|
|
const sceneJson = getNewSceneTemplate();
|
|
|
|
|
|
|
|
|
|
|
|
Editor.assetdb.create(url, sceneJson, (err, results) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
content: [{ type: "text", text: "Error creating scene: " + err }],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
content: [{ type: "text", text: `Standard Scene created at ${url}` }],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (name === "create_prefab") {
|
|
|
|
|
|
const url = `db://assets/${args.prefabName}.prefab`;
|
|
|
|
|
|
// 2.4.x 创建预制体的 IPC 消息
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:create-prefab", args.nodeId, url);
|
|
|
|
|
|
res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{ type: "text", text: `Command sent: Creating prefab '${args.prefabName}'` },
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (name === "open_scene") {
|
|
|
|
|
|
const url = args.url;
|
|
|
|
|
|
// 1. 将 db:// 路径转换为 UUID
|
|
|
|
|
|
const uuid = Editor.assetdb.urlToUuid(url);
|
|
|
|
|
|
|
|
|
|
|
|
if (uuid) {
|
|
|
|
|
|
// 2. 发送核心 IPC 消息给主进程
|
|
|
|
|
|
// scene:open-by-uuid 是编辑器内置的场景打开逻辑
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:open-by-uuid", uuid);
|
|
|
|
|
|
|
|
|
|
|
|
res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{ type: "text", text: `Success: Opening scene ${url} (UUID: ${uuid})` },
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{ type: "text", text: `Error: Could not find asset with URL ${url}` },
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (name === "create_node") {
|
|
|
|
|
|
// 转发给场景脚本处理
|
|
|
|
|
|
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => {
|
|
|
|
|
|
res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{ type: "text", text: err ? `Error: ${err}` : `Node created: ${result}` },
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
2026-01-29 13:47:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
} 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");
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|