Files
T

5388 lines
191 KiB
JavaScript
Executable File

"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// packages/cocos-ext/src/main.ts
var main_exports = {};
__export(main_exports, {
load: () => load,
methods: () => methods,
unload: () => unload
});
module.exports = __toCommonJS(main_exports);
var crypto2 = __toESM(require("crypto"));
var fs3 = __toESM(require("fs"));
var path3 = __toESM(require("path"));
var url = __toESM(require("url"));
var import_child_process = require("child_process");
var import_ws = require("ws");
// packages/cocos-ext/src/client-config.ts
var crypto = __toESM(require("crypto"));
var fs = __toESM(require("fs"));
var os = __toESM(require("os"));
var path = __toESM(require("path"));
function resolveAppSupportPath(platform, homeDir, appDataDir, ...segments) {
const pathApi = platform === "win32" ? path.win32 : path.posix;
if (platform === "darwin") {
return pathApi.join(homeDir, "Library", "Application Support", ...segments);
}
if (platform === "win32") {
return pathApi.join(appDataDir ?? pathApi.join(homeDir, "AppData", "Roaming"), ...segments);
}
return pathApi.join(homeDir, ".config", ...segments);
}
function firstExistingOrDefault(candidates) {
return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
}
function getClientSpecs(options = {}) {
const platform = options.platform ?? process.platform;
const homeDir = options.homeDir ?? os.homedir();
const appDataDir = options.appDataDir ?? process.env.APPDATA;
const pathApi = platform === "win32" ? path.win32 : path.posix;
const appSupport = (...segments) => resolveAppSupportPath(platform, homeDir, appDataDir, ...segments);
const extensionStorageCandidates = (extensionId, fileName = "mcp_settings.json") => ["Code", "Cursor", "Trae", "Windsurf"].map(
(host) => appSupport(host, "User", "globalStorage", extensionId, "settings", fileName)
);
return [
{
clientType: "cursor",
name: "Cursor",
configPath: pathApi.join(homeDir, ".cursor", "mcp.json"),
mode: "json"
},
{
clientType: "windsurf",
name: "Windsurf",
configPath: pathApi.join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
mode: "json"
},
{
clientType: "trae-cn",
name: "Trae CN",
configPath: appSupport("Trae CN", "User", "mcp.json"),
mode: "json"
},
{
clientType: "trae-international",
name: "Trae International",
configPath: appSupport("Trae", "User", "mcp.json"),
mode: "json"
},
{
clientType: "zoo-code",
name: "Zoo Code",
configPath: firstExistingOrDefault([
appSupport(
"Trae",
"User",
"globalStorage",
"zoocodeorganization.zoo-code",
"settings",
"mcp_settings.json"
),
...extensionStorageCandidates("zoocodeorganization.zoo-code")
]),
mode: "json"
},
{
clientType: "cline",
name: "Cline",
configPath: firstExistingOrDefault(
extensionStorageCandidates("saoudrizwan.claude-dev", "cline_mcp_settings.json")
),
mode: "json"
},
{
clientType: "roo-code",
name: "Roo Code",
configPath: firstExistingOrDefault(extensionStorageCandidates("rooveterinaryinc.roo-cline")),
mode: "json"
},
{
clientType: "codex-cli",
name: "Codex CLI",
configPath: pathApi.join(homeDir, ".codex", "config.toml"),
mode: "toml"
}
];
}
function ensureParentDir(filePath) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
function writeFileAtomicWithBackup(filePath, content) {
ensureParentDir(filePath);
const existed = fs.existsSync(filePath);
const backupPath = `${filePath}.cocos-mcp.bak`;
const tempPath = `${filePath}.cocos-mcp.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
if (existed) {
fs.copyFileSync(filePath, backupPath);
}
try {
fs.writeFileSync(tempPath, content, "utf-8");
const temporaryContent = fs.readFileSync(tempPath, "utf-8");
if (temporaryContent !== content) {
throw new Error("Temporary config verification failed.");
}
fs.renameSync(tempPath, filePath);
return existed ? { backupPath } : {};
} catch (error) {
try {
fs.rmSync(tempPath, { force: true });
} catch {
}
throw error;
}
}
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function escapeTomlBasicString(value) {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n");
}
function readJsonObject(filePath, strict) {
if (!fs.existsSync(filePath)) {
return {};
}
try {
const raw = fs.readFileSync(filePath, "utf-8").trim();
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error(`Config root must be a JSON object: ${filePath}`);
}
return parsed;
} catch (error) {
if (strict) {
throw error;
}
return {};
}
}
function writeJsonConfig(filePath, serverName, bridge) {
try {
const root = readJsonObject(filePath, true);
const rawServers = root.mcpServers;
if (rawServers !== void 0 && (typeof rawServers !== "object" || rawServers === null || Array.isArray(rawServers))) {
throw new Error(`mcpServers must be a JSON object: ${filePath}`);
}
const mcpServers = typeof rawServers === "object" && rawServers !== null && !Array.isArray(rawServers) ? rawServers : {};
mcpServers[serverName] = { command: bridge.command, args: bridge.args };
root.mcpServers = mcpServers;
const content = `${JSON.stringify(root, null, 2)}
`;
JSON.parse(content);
const result = writeFileAtomicWithBackup(filePath, content);
if (!hasServerInJson(filePath, serverName, bridge)) {
restoreConfigAfterFailedVerification(filePath, result.backupPath);
throw new Error(`Written JSON config did not pass verification: ${filePath}`);
}
return {
ok: true,
message: `updated ${filePath}${result.backupPath ? ` (backup: ${result.backupPath})` : ""}`
};
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : String(error) };
}
}
function codexSection(serverName, bridge) {
const key = serverName.replace(/[^a-zA-Z0-9_-]/g, "-");
const args = bridge.args.map((arg) => `"${escapeTomlBasicString(arg)}"`).join(", ");
return {
key,
block: `[mcp_servers.${key}]
command = "${escapeTomlBasicString(bridge.command)}"
args = [${args}]
`
};
}
function restoreConfigAfterFailedVerification(filePath, backupPath) {
if (backupPath && fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, filePath);
return;
}
fs.rmSync(filePath, { force: true });
}
function hasServerInCodexTomlText(raw, serverName, bridge) {
const { key } = codexSection(serverName, bridge);
const section = new RegExp(
`\\[mcp_servers\\.${escapeRegex(key)}\\][\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`
).exec(raw)?.[0];
if (!section) {
return false;
}
const commandLine = `command = "${escapeTomlBasicString(bridge.command)}"`;
const expectedArgs = bridge.args.map((arg) => `"${escapeTomlBasicString(arg)}"`).join(", ");
const argsLine = `args = [${expectedArgs}]`;
const normalizedLines = section.split(/\r?\n/).map((line) => line.trim());
return normalizedLines.includes(commandLine) && normalizedLines.includes(argsLine);
}
function writeCodexToml(filePath, serverName, bridge) {
try {
const { key, block } = codexSection(serverName, bridge);
let raw = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
const sectionRe = new RegExp(
`\\[mcp_servers\\.${escapeRegex(key)}\\][\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`
);
raw = sectionRe.test(raw) ? raw.replace(sectionRe, block.trimEnd()) : `${raw.trimEnd()}
${block}`;
const content = `${raw.trimStart().trimEnd()}
`;
if (!hasServerInCodexTomlText(content, serverName, bridge)) {
throw new Error(`Generated Codex config did not pass verification: ${filePath}`);
}
const result = writeFileAtomicWithBackup(filePath, content);
if (!hasServerInCodexToml(filePath, serverName, bridge)) {
restoreConfigAfterFailedVerification(filePath, result.backupPath);
throw new Error(`Written Codex config did not pass verification: ${filePath}`);
}
return {
ok: true,
message: `updated ${filePath}${result.backupPath ? ` (backup: ${result.backupPath})` : ""}`
};
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : String(error) };
}
}
function hasServerInJson(filePath, serverName, bridge) {
const root = readJsonObject(filePath, false);
const servers = root.mcpServers;
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
return false;
}
const entry = servers[serverName];
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
return false;
}
const record = entry;
return record.command === bridge.command && Array.isArray(record.args) && record.args.length === bridge.args.length && record.args.every((arg, index) => arg === bridge.args[index]);
}
function hasServerInCodexToml(filePath, serverName, bridge) {
if (!fs.existsSync(filePath)) {
return false;
}
try {
const raw = fs.readFileSync(filePath, "utf-8");
return hasServerInCodexTomlText(raw, serverName, bridge);
} catch {
return false;
}
}
// packages/cocos-ext/src/ui-assets.ts
var fs2 = __toESM(require("fs"));
var path2 = __toESM(require("path"));
var zlib = __toESM(require("zlib"));
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
function crc32(buffer) {
let crc = 4294967295;
for (const byte of buffer) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
}
}
return (crc ^ 4294967295) >>> 0;
}
function pngChunk(type, data) {
const typeBuffer = Buffer.from(type, "ascii");
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length, 0);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0);
return Buffer.concat([length, typeBuffer, data, crc]);
}
function pixelVisible(shape, x, y, size) {
if (shape === "rectangle") {
return true;
}
if (shape === "circle") {
const center = (size - 1) / 2;
const radius2 = center - 1;
const dx2 = x - center;
const dy2 = y - center;
return dx2 * dx2 + dy2 * dy2 <= radius2 * radius2;
}
const radius = Math.max(4, Math.floor(size * 0.18));
const left = radius;
const right = size - radius - 1;
const top = radius;
const bottom = size - radius - 1;
if (x >= left && x <= right || y >= top && y <= bottom) {
return true;
}
const cornerX = x < left ? left : right;
const cornerY = y < top ? top : bottom;
const dx = x - cornerX;
const dy = y - cornerY;
return dx * dx + dy * dy <= radius * radius;
}
function createManagedShapePng(shape, size = 128) {
if (!Number.isInteger(size) || size < 16 || size > 512) {
throw new Error("Managed shape PNG size must be an integer from 16 to 512.");
}
const stride = size * 4 + 1;
const raw = Buffer.alloc(stride * size);
for (let y = 0; y < size; y += 1) {
const rowOffset = y * stride;
raw[rowOffset] = 0;
for (let x = 0; x < size; x += 1) {
const offset = rowOffset + 1 + x * 4;
raw[offset] = 255;
raw[offset + 1] = 255;
raw[offset + 2] = 255;
raw[offset + 3] = pixelVisible(shape, x, y, size) ? 255 : 0;
}
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(size, 0);
ihdr.writeUInt32BE(size, 4);
ihdr[8] = 8;
ihdr[9] = 6;
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
return Buffer.concat([
PNG_SIGNATURE,
pngChunk("IHDR", ihdr),
pngChunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
pngChunk("IEND", Buffer.alloc(0))
]);
}
function ensureManagedShapeAsset(projectRoot, shape) {
const relativePath = `assets/cocos-mcp-generated/shapes/${shape}.png`;
const absolutePath = path2.join(projectRoot, ...relativePath.split("/"));
const expected = createManagedShapePng(shape);
fs2.mkdirSync(path2.dirname(absolutePath), { recursive: true });
if (fs2.existsSync(absolutePath)) {
const existing = fs2.readFileSync(absolutePath);
if (existing.equals(expected)) {
return { relativePath, absolutePath, created: false };
}
}
const temporary = `${absolutePath}.cocos-mcp.tmp`;
fs2.writeFileSync(temporary, expected);
fs2.renameSync(temporary, absolutePath);
return { relativePath, absolutePath, created: true };
}
function prepareManagedShapeSpriteFrameMeta(rawMeta) {
let parsed = rawMeta;
if (typeof rawMeta === "string") {
try {
parsed = JSON.parse(rawMeta);
} catch {
throw new Error("Managed shape asset meta is not valid JSON.");
}
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Managed shape asset meta is missing or invalid.");
}
const meta = JSON.parse(JSON.stringify(parsed));
const existingUserData = meta.userData;
const userData = existingUserData && typeof existingUserData === "object" && !Array.isArray(existingUserData) ? existingUserData : {};
const changed = userData.type !== "sprite-frame";
userData.type = "sprite-frame";
meta.userData = userData;
return { meta, changed };
}
// packages/cocos-ext/src/main.ts
var SUPPORTED_PROTOCOL_VERSION = 1;
var DEFAULT_LOG_RELATIVE = "temp/logs/project.log";
var wss = null;
var serverToken = "";
var discoveryPath = "";
var activeWsClients = /* @__PURE__ */ new Set();
var wsRequestQueue = Promise.resolve();
var serverStopping = false;
var startServerPromise = null;
var pendingWss = null;
var cancelPendingStart = null;
var stopServerPromise = null;
var panelLogs = [];
var PANEL_LOG_MAX = 200;
function panelLog(level, message) {
panelLogs.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, message });
if (panelLogs.length > PANEL_LOG_MAX) {
panelLogs.splice(0, panelLogs.length - PANEL_LOG_MAX);
}
const prefix = "[cocos-mcp panel]";
if (level === "error") {
console.error(prefix, message);
} else if (level === "warn") {
console.warn(prefix, message);
} else {
console.log(prefix, message);
}
}
var MCP_UNDO_MAX = 2048;
var mcpUndoStack = [];
var mcpUndoSceneUuid = "";
async function clearMcpUndoStack() {
mcpUndoStack.length = 0;
mcpUndoSceneUuid = "";
try {
await executeSceneMethod("discardDetachedNodesForMcpUndo", []);
} catch {
}
}
var VEC_EPS = 1e-5;
function cloneVec3(v) {
return { x: v.x, y: v.y, z: v.z };
}
function vec3NearlyEqual(a, b) {
return Math.abs(a.x - b.x) < VEC_EPS && Math.abs(a.y - b.y) < VEC_EPS && Math.abs(a.z - b.z) < VEC_EPS;
}
function readVec3FromPropertyDump(dump) {
const v = dump.value;
if (v && typeof v === "object" && !Array.isArray(v)) {
const o = v;
if (typeof o.x === "number" && typeof o.y === "number" && typeof o.z === "number") {
return { x: o.x, y: o.y, z: o.z };
}
}
return null;
}
function applyVec3ToPropertyDump(dump, vec) {
const v = dump.value;
if (v && typeof v === "object" && !Array.isArray(v)) {
const o = v;
if (typeof o.x === "number" && typeof o.y === "number" && typeof o.z === "number") {
o.x = vec.x;
o.y = vec.y;
o.z = vec.z;
return;
}
}
dump.value = { x: vec.x, y: vec.y, z: vec.z };
}
function getDiscoveryPath() {
return path3.join(Editor.Project.path, "temp", ".cocos-mcp.json");
}
function writeDiscovery(port, token) {
const dir = path3.join(Editor.Project.path, "temp");
if (!fs3.existsSync(dir)) {
fs3.mkdirSync(dir, { recursive: true });
}
const payload = { port, token };
fs3.writeFileSync(getDiscoveryPath(), JSON.stringify(payload), "utf-8");
discoveryPath = getDiscoveryPath();
}
function removeDiscovery() {
const candidates = /* @__PURE__ */ new Set();
if (discoveryPath) {
candidates.add(discoveryPath);
}
try {
candidates.add(getDiscoveryPath());
} catch {
}
for (const candidate of candidates) {
try {
if (candidate && fs3.existsSync(candidate)) {
fs3.unlinkSync(candidate);
}
} catch {
}
}
discoveryPath = "";
}
function randomToken() {
return crypto2.randomBytes(32).toString("hex");
}
var NODE_PROBE_CACHE_TTL_MS = 5 * 60 * 1e3;
var nodeProbeCache = null;
function detectNodeAbsolutePath() {
const now = Date.now();
if (nodeProbeCache && now < nodeProbeCache.until) {
return nodeProbeCache.result;
}
const tryValidate = (raw) => {
const line = raw.split(/\r?\n/).map((l) => l.trim())[0];
if (!line) {
return null;
}
const candidate = line.replace(/^['"]|['"]$/g, "");
if (!candidate) {
return null;
}
try {
if (!fs3.existsSync(candidate)) {
return null;
}
const st = fs3.statSync(candidate);
if (!st.isFile()) {
return null;
}
if (process.platform !== "win32") {
try {
fs3.accessSync(candidate, fs3.constants.X_OK);
} catch {
return null;
}
}
return candidate;
} catch {
return null;
}
};
const runProbe = (file, args, timeoutMs) => {
const r = (0, import_child_process.spawnSync)(file, args, {
encoding: "utf-8",
timeout: timeoutMs,
maxBuffer: 512 * 1024,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true
});
if (r.error || r.signal) {
return null;
}
const out = `${r.stdout ?? ""}`.trim();
if (!out) {
return null;
}
if (process.platform === "win32") {
const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
return first ? tryValidate(first) : null;
}
return tryValidate(out);
};
let resolved = null;
for (const environmentCandidate of [process.env.NODE_BINARY, process.env.NODE]) {
if (!environmentCandidate) {
continue;
}
resolved = tryValidate(environmentCandidate);
if (resolved) {
break;
}
}
if (!resolved && process.platform === "win32") {
const whereExe = path3.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "where.exe");
if (fs3.existsSync(whereExe)) {
resolved = runProbe(whereExe, ["node"], 6e3);
}
if (!resolved) {
resolved = runProbe("cmd.exe", ["/d", "/s", "/c", "where node"], 6e3);
}
if (!resolved) {
const windowsCandidates = [
process.env.NVM_SYMLINK ? path3.join(process.env.NVM_SYMLINK, "node.exe") : "",
process.env.VOLTA_HOME ? path3.join(process.env.VOLTA_HOME, "bin", "node.exe") : "",
process.env.ProgramFiles ? path3.join(process.env.ProgramFiles, "nodejs", "node.exe") : "",
process.env["ProgramFiles(x86)"] ? path3.join(process.env["ProgramFiles(x86)"], "nodejs", "node.exe") : "",
process.env.LOCALAPPDATA ? path3.join(process.env.LOCALAPPDATA, "Programs", "nodejs", "node.exe") : ""
];
for (const candidate of windowsCandidates) {
if (!candidate) {
continue;
}
resolved = tryValidate(candidate);
if (resolved) {
break;
}
}
}
} else if (!resolved) {
const timeoutMs = 6e3;
const shells = [];
const userShell = process.env.SHELL;
if (userShell && fs3.existsSync(userShell)) {
shells.push({ file: userShell, args: ["-lc", "command -v node"] });
}
shells.push(
{ file: "/bin/zsh", args: ["-lc", "command -v node"] },
{ file: "/bin/bash", args: ["-lc", "command -v node"] }
);
for (const s of shells) {
if (!fs3.existsSync(s.file)) {
continue;
}
resolved = runProbe(s.file, s.args, timeoutMs);
if (resolved) {
break;
}
}
if (!resolved) {
for (const s of shells) {
if (!fs3.existsSync(s.file)) {
continue;
}
resolved = runProbe(s.file, ["-ilc", "command -v node"], Math.min(timeoutMs, 4e3));
if (resolved) {
break;
}
}
}
if (!resolved) {
const fallbacks = ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"];
for (const p of fallbacks) {
const v = tryValidate(p);
if (v) {
resolved = v;
break;
}
}
}
}
nodeProbeCache = { result: resolved, until: now + NODE_PROBE_CACHE_TTL_MS };
if (!resolved) {
panelLog(
"warn",
"Could not resolve an absolute Node executable. Auto Config will stop instead of writing a fragile PATH-dependent entry."
);
}
return resolved;
}
function getBundledBridgeCliPath() {
const extRoot = path3.resolve(__dirname, "..");
return path3.join(extRoot, "dist", "bridge-cli.js");
}
function getBridgeLaunchConfig() {
const projectRoot = Editor.Project.path;
const bridgeCliPath = getBundledBridgeCliPath();
if (!fs3.existsSync(bridgeCliPath)) {
return {
ok: false,
message: `bundled bridge cli not found: ${bridgeCliPath}. Please reinstall the extension package with dist/bridge-cli.js included.`
};
}
const nodeBin = detectNodeAbsolutePath();
if (!nodeBin) {
return {
ok: false,
message: "Node.js was not found as an absolute executable path. Install Node.js 18-24, restart Creator, and run Auto Config again."
};
}
if (!fs3.existsSync(nodeBin)) {
return {
ok: false,
message: `Node executable not found: ${nodeBin}. Install Node.js 18-24 and run Auto Config again.`
};
}
const versionProbe = (0, import_child_process.spawnSync)(nodeBin, ["--version"], {
encoding: "utf-8",
timeout: 5e3,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"]
});
const versionText = `${versionProbe.stdout ?? ""}`.trim();
const major = Number(/^v?(\d+)/.exec(versionText)?.[1]);
if (versionProbe.error || versionProbe.status !== 0 || !Number.isInteger(major)) {
return {
ok: false,
message: `Node executable could not be verified: ${nodeBin}. ` + (versionProbe.error?.message ?? `${versionProbe.stderr ?? ""}`.trim() ?? "unknown error")
};
}
if (major < 18 || major > 24) {
return {
ok: false,
message: `Unsupported Node.js ${versionText}; install Node.js 18-24 and run Auto Config again.`
};
}
return {
ok: true,
value: {
command: nodeBin,
args: [bridgeCliPath, "--project", path3.resolve(projectRoot)],
nodeBin,
bridgeCliPath,
projectRoot: path3.resolve(projectRoot)
}
};
}
function openFileInSystem(filePath) {
try {
let cmd = "";
if (process.platform === "darwin") {
try {
(0, import_child_process.execSync)(`open -a "Cursor" "${filePath}"`, { stdio: "ignore" });
return { ok: true, message: `opened in Cursor: ${filePath}` };
} catch {
cmd = `open "${filePath}"`;
}
} else if (process.platform === "win32") {
cmd = `start "" "${filePath}"`;
} else {
cmd = `xdg-open "${filePath}"`;
}
(0, import_child_process.exec)(cmd, (err) => {
if (err) {
panelLog("warn", `open config failed: ${filePath} :: ${err.message}`);
}
});
return { ok: true, message: `opened ${filePath}` };
} catch (e) {
return { ok: false, message: e instanceof Error ? e.message : String(e) };
}
}
async function executeSceneMethod(method, args) {
return Editor.Message.request(
"scene",
"execute-scene-script",
{
name: "cocos-mcp",
script: "scene",
method,
args
}
);
}
async function handleScenePing(id) {
try {
const result = await executeSceneMethod("ping", []);
return { op: "scene_pong", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
function isSetNodeActivePrepareOk(v) {
return typeof v === "object" && v !== null && v.ok === true && typeof v.uuid === "string";
}
function isSetNodeActivePrepareErr(v) {
return typeof v === "object" && v !== null && v.ok === false && typeof v.message === "string";
}
function isPrepareSetComponentPropertyOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.uuid === "string" && typeof o.path === "string" && typeof o.componentType === "string";
}
function isPrepareSetNodeNameOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.uuid === "string" && typeof o.name === "string";
}
function isPrepareCreateNodeOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.parentUuid === "string" && typeof o.name === "string";
}
function isRegisterNidForNewNodeOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.nid === "number" && Number.isInteger(o.nid);
}
function isPrepareDeleteNodeOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.uuid === "string";
}
function isPrepareInstantiatePrefabOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.parentUuid === "string";
}
function isPrepareAddComponentOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.uuid === "string" && typeof o.componentType === "string";
}
function isPrepareRemoveComponentOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.uuid === "string" && (o.componentUuid === void 0 || typeof o.componentUuid === "string" && o.componentUuid.trim().length > 0) && typeof o.componentIndex === "number" && Number.isInteger(o.componentIndex) && o.componentIndex >= 0 && typeof o.componentType === "string";
}
function isPrepareDuplicateNodeOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.uuid === "string" && typeof o.parentUuid === "string";
}
function isPrepareSetNodeSiblingIndexOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.uuid === "string" && typeof o.parentUuid === "string" && typeof o.index === "number" && Number.isInteger(o.index) && typeof o.previousIndex === "number" && Number.isInteger(o.previousIndex) && typeof o.noOp === "boolean";
}
function isPrepareReparentNodeOk(v) {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v;
return o.ok === true && typeof o.childUuid === "string" && typeof o.newParentUuid === "string" && typeof o.previousParentUuid === "string" && typeof o.keepWorldTransform === "boolean" && typeof o.noOp === "boolean";
}
function inodeRecord(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function getDumpAtInodePath(inode, dotPath) {
const segments = dotPath.split(".");
let cur = inode;
for (const seg of segments) {
if (cur === null || cur === void 0) {
return null;
}
if (/^\d+$/.test(seg)) {
const idx = Number(seg);
if (!Array.isArray(cur) || idx < 0 || idx >= cur.length) {
return null;
}
cur = cur[idx];
} else {
if (!inodeRecord(cur)) {
return null;
}
cur = cur[seg];
}
}
return inodeRecord(cur) ? cur : null;
}
function resolveComponentPropertyPathFallback(inode, requestedPath, componentType, property) {
const prop = property.trim();
const propCandidates = [prop, `_${prop}`];
const compsRaw = inode.__comps__;
if (!Array.isArray(compsRaw) || prop.length === 0) {
return null;
}
const normType = normalizeComponentTypeName(componentType);
const typedMatchesByProp = /* @__PURE__ */ new Map();
const propOnlyMatchesByProp = /* @__PURE__ */ new Map();
for (const p of propCandidates) {
typedMatchesByProp.set(p, []);
propOnlyMatchesByProp.set(p, []);
}
for (let i = 0; i < compsRaw.length; i += 1) {
const comp = compsRaw[i];
if (!inodeRecord(comp)) {
continue;
}
for (const p of propCandidates) {
if (!(p in comp)) {
continue;
}
propOnlyMatchesByProp.get(p)?.push(i);
const dumpType = readComponentTypeFromDump(comp);
if (dumpType && normalizeComponentTypeName(dumpType) === normType) {
typedMatchesByProp.get(p)?.push(i);
}
}
}
for (const p of propCandidates) {
const typed = typedMatchesByProp.get(p) ?? [];
const propOnly = propOnlyMatchesByProp.get(p) ?? [];
const candidates = typed.length > 0 ? typed : propOnly;
if (candidates.length === 0) {
continue;
}
const m = /^__comps__\.(\d+)\./.exec(requestedPath);
const requestedIdx = m ? Number(m[1]) : -1;
if (requestedIdx >= 0 && candidates.includes(requestedIdx)) {
return `__comps__.${requestedIdx}.${p}`;
}
const picked = candidates[candidates.length - 1];
return `__comps__.${picked}.${p}`;
}
return null;
}
function readComponentDumpCount(inode) {
const comps = inode.__comps__;
return Array.isArray(comps) ? comps.length : 0;
}
function readComponentDumps(inode) {
const comps = inode.__comps__;
if (!Array.isArray(comps)) {
return [];
}
return comps.filter((c) => inodeRecord(c));
}
function normalizeComponentTypeName(raw) {
return raw.trim().replace(/^cc\./, "").toLowerCase();
}
function readComponentTypeFromDump(dump) {
const typeCandidates = [dump.__type__, dump.type, dump._type];
for (const c of typeCandidates) {
if (typeof c === "string" && c.trim().length > 0) {
return c.trim();
}
}
return null;
}
function readComponentIdentityFromDump(dump) {
const idCandidates = [dump.__id__, dump._id, dump.id, dump.uuid, dump.__uuid__];
for (const c of idCandidates) {
if (typeof c === "string" && c.trim().length > 0) {
return c.trim();
}
if (typeof c === "number" && Number.isFinite(c)) {
return String(c);
}
}
return null;
}
function readComponentUuidFromDump(dump) {
const readStringValue = (candidate) => {
if (typeof candidate === "string" && candidate.trim().length > 0) {
return candidate.trim();
}
if (inodeRecord(candidate)) {
const nested = candidate.value;
if (typeof nested === "string" && nested.trim().length > 0) {
return nested.trim();
}
}
return null;
};
const value = inodeRecord(dump.value) ? dump.value : null;
for (const candidate of [value?.uuid, value?.__uuid__, dump.componentUuid, dump.uuid, dump.__uuid__]) {
const uuid = readStringValue(candidate);
if (uuid) {
return uuid;
}
}
return null;
}
function pickUndoAddComponentIndex(comps, entry) {
if (comps.length === 0) {
return 0;
}
const safeIndex = Math.max(0, Math.min(entry.componentIndex, comps.length - 1));
const typeAtOriginal = readComponentTypeFromDump(comps[safeIndex] ?? {});
if (typeAtOriginal && normalizeComponentTypeName(typeAtOriginal) === normalizeComponentTypeName(entry.componentType)) {
return safeIndex;
}
const targetId = readComponentIdentityFromDump(entry.addedComponentDump);
if (targetId) {
const byId = comps.findIndex((c) => readComponentIdentityFromDump(c) === targetId);
if (byId >= 0) {
return byId;
}
}
const sameTypeIndexes = [];
for (let i = 0; i < comps.length; i += 1) {
const t = readComponentTypeFromDump(comps[i]);
if (t && normalizeComponentTypeName(t) === normalizeComponentTypeName(entry.componentType)) {
sameTypeIndexes.push(i);
}
}
if (sameTypeIndexes.length > 0) {
return sameTypeIndexes[sameTypeIndexes.length - 1];
}
const targetSig = safeJsonSignature(entry.addedComponentDump);
const matched = comps.findIndex((c) => safeJsonSignature(c) === targetSig);
if (matched >= 0) {
return matched;
}
return safeIndex;
}
async function restoreComponentTypeBaselineAfterAdd(entry) {
const cleanupTypes = [...entry.addedComponentTypes].reverse();
if (!cleanupTypes.some((type) => normalizeComponentTypeName(type) === normalizeComponentTypeName(entry.componentType))) {
cleanupTypes.unshift(entry.componentType);
}
const processed = /* @__PURE__ */ new Set();
for (const componentType of cleanupTypes) {
const normalized = normalizeComponentTypeName(componentType);
if (!normalized || processed.has(normalized)) {
continue;
}
processed.add(normalized);
const baseline = entry.beforeComponentTypeCounts[normalized] ?? 0;
for (let attempt = 0; attempt < 16; attempt += 1) {
const stats = await executeSceneMethod("getComponentStatsByType", [
entry.uuid,
componentType
]);
if (!inodeRecord(stats) || stats.ok !== true || typeof stats.typeCount !== "number" || !Number.isFinite(stats.typeCount)) {
throw new Error(`cannot verify component baseline for ${componentType}`);
}
if (stats.typeCount <= baseline) {
break;
}
const removed = await executeSceneMethod("removeLastComponentByType", [
entry.uuid,
componentType
]);
if (!inodeRecord(removed) || removed.ok !== true) {
throw new Error(`failed to restore component baseline for ${componentType}`);
}
const removedAfterWait = await waitRuntimeTypeCountBelow(
entry.uuid,
componentType,
stats.typeCount
);
if (removed.removed !== true && !removedAfterWait) {
throw new Error(`failed to restore component baseline for ${componentType}`);
}
}
const finalStats = await executeSceneMethod("getComponentStatsByType", [
entry.uuid,
componentType
]);
if (!inodeRecord(finalStats) || finalStats.ok !== true || typeof finalStats.typeCount !== "number" || finalStats.typeCount !== baseline) {
throw new Error(
`component baseline mismatch for ${componentType} (expected=${baseline}, actual=${inodeRecord(finalStats) ? String(finalStats.typeCount) : "unknown"})`
);
}
}
}
function isLikelyUuidString(v) {
if (typeof v !== "string") {
return false;
}
const s = v.trim();
return s.length >= 16 && /^[A-Za-z0-9_-]+$/.test(s);
}
function readUuidFromUnknown(v) {
if (isLikelyUuidString(v)) {
return v;
}
if (v && typeof v === "object" && !Array.isArray(v)) {
const o = v;
const candidates = [o.uuid, o.__uuid__, o.value];
for (const c of candidates) {
if (isLikelyUuidString(c)) {
return c;
}
}
}
return null;
}
function collectChildUuidsFromParentInode(inode) {
const out = /* @__PURE__ */ new Set();
const keys = ["children", "__children__", "_children", "childrens"];
for (const k of keys) {
const raw = inode[k];
if (!Array.isArray(raw)) {
continue;
}
for (const item of raw) {
const u = readUuidFromUnknown(item);
if (u) {
out.add(u);
}
}
}
return out;
}
async function readParentChildUuidSet(parentUuid) {
try {
const inode = await Editor.Message.request("scene", "query-node", parentUuid);
if (!inodeRecord(inode)) {
return /* @__PURE__ */ new Set();
}
return collectChildUuidsFromParentInode(inode);
} catch {
return /* @__PURE__ */ new Set();
}
}
async function listChildUuidsFromSceneScript(parentUuid) {
try {
const raw = await executeSceneMethod("listChildUuidsUnderParent", [parentUuid]);
if (!inodeRecord(raw) || raw.ok !== true || !Array.isArray(raw.uuids)) {
return null;
}
const uuids = raw.uuids;
if (!uuids.every((u) => typeof u === "string")) {
return null;
}
return uuids;
} catch {
return null;
}
}
async function getParentChildUuidSetForDuplicate(parentUuid) {
const list = await listChildUuidsFromSceneScript(parentUuid);
if (list) {
return new Set(list);
}
panelLog("warn", "listChildUuidsUnderParent unavailable; falling back to query-node inode children");
return readParentChildUuidSet(parentUuid);
}
function safeJsonSignature(v) {
try {
return JSON.stringify(v);
} catch {
return "";
}
}
async function getRuntimeComponentTypeCount(uuid, componentType) {
const s = await getRuntimeComponentStats(uuid, componentType);
return s?.typeCount ?? null;
}
async function getRuntimeComponentStats(uuid, componentType) {
try {
const raw = await executeSceneMethod("getComponentStatsByType", [uuid, componentType]);
if (inodeRecord(raw) && raw.ok === true && typeof raw.totalCount === "number" && Number.isFinite(raw.totalCount) && typeof raw.typeCount === "number" && Number.isFinite(raw.typeCount)) {
return { totalCount: raw.totalCount, typeCount: raw.typeCount };
}
} catch {
}
return null;
}
async function getRuntimeComponentUuidState(nodeUuid, componentUuid) {
try {
const raw = await executeSceneMethod("getComponentUuidState", [
nodeUuid,
componentUuid
]);
if (inodeRecord(raw) && raw.ok === true && typeof raw.present === "boolean") {
return {
present: raw.present,
...typeof raw.componentIndex === "number" ? { componentIndex: raw.componentIndex } : {},
...typeof raw.componentType === "string" ? { componentType: raw.componentType } : {}
};
}
} catch {
}
return null;
}
function sleep(ms) {
return new Promise((resolve2) => setTimeout(resolve2, ms));
}
async function waitRuntimeTypeCountBelow(uuid, componentType, before, retries = 3, delayMs = 20) {
if (before === null) {
return false;
}
for (let i = 0; i <= retries; i += 1) {
const now = await getRuntimeComponentTypeCount(uuid, componentType);
if (now !== null && now < before) {
return true;
}
if (i < retries) {
await sleep(delayMs);
}
}
return false;
}
async function waitRuntimeTypeCountAbove(uuid, componentType, before, retries = 3, delayMs = 20) {
if (before === null) {
return false;
}
for (let i = 0; i <= retries; i += 1) {
const now = await getRuntimeComponentTypeCount(uuid, componentType);
if (now !== null && now > before) {
return true;
}
if (i < retries) {
await sleep(delayMs);
}
}
return false;
}
async function rollbackCreatedNodeWithVerification(uuid) {
try {
await Editor.Message.request("scene", "remove-node", { uuid });
await sceneCommitUndoSnapshot();
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { ok: false, message: `rollback remove-node failed: ${message}` };
}
try {
await executeSceneMethod("forgetNodeNid", [uuid]);
} catch {
}
try {
const inode = await Editor.Message.request("scene", "query-node", uuid);
if (inodeRecord(inode)) {
return { ok: false, message: "rollback verification failed: node still queryable after remove-node" };
}
} catch {
}
return { ok: true };
}
function readScalarFromPropertyDump(dump) {
const inner = dump.value;
if (typeof inner === "boolean" || typeof inner === "number" || typeof inner === "string") {
return inner;
}
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
const v = inner.value;
if (typeof v === "boolean" || typeof v === "number" || typeof v === "string") {
return v;
}
}
return null;
}
function applyScalarToPropertyDump(dump, v) {
const inner = dump.value;
if (typeof inner === "boolean" && typeof v === "boolean") {
dump.value = v;
return;
}
if (typeof inner === "number" && typeof v === "number") {
dump.value = v;
return;
}
if (typeof inner === "string" && typeof v === "string") {
dump.value = v;
return;
}
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
const o = inner;
if (typeof o.value === "boolean" && typeof v === "boolean") {
o.value = v;
return;
}
if (typeof o.value === "number" && typeof v === "number") {
o.value = v;
return;
}
if (typeof o.value === "string" && typeof v === "string") {
o.value = v;
return;
}
}
dump.value = v;
}
function readComparableValueFromPropertyDump(dump) {
const inner = dump.value;
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
const v = inner.value;
if (v !== void 0) {
return v;
}
}
return inner;
}
function valuesEqualForChange(a, b) {
if (Object.is(a, b)) {
return true;
}
try {
return JSON.stringify(a) === JSON.stringify(b);
} catch {
return false;
}
}
function toDebugScalar(v) {
try {
const s = JSON.stringify(v);
return s.length > 180 ? `${s.slice(0, 180)}...` : s;
} catch {
return String(v);
}
}
async function applyInspectorComponentProperty(uuid, propPath, dump, record) {
try {
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid,
path: propPath,
dump,
record
}
);
if (!ok) {
return { ok: false, message: "set-property returned false" };
}
return { ok: true };
} catch (e) {
return { ok: false, message: e instanceof Error ? e.message : String(e) };
}
}
function cloneActiveDump(active) {
try {
return structuredClone(active);
} catch {
return JSON.parse(JSON.stringify(active));
}
}
function readActiveFromPropertyDump(dump) {
const inner = dump.value;
if (inner === true || inner === false) {
return inner;
}
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
const o = inner;
if (typeof o.value === "boolean") {
return o.value;
}
}
return null;
}
function applyActiveValueToPropertyDump(dump, next) {
const inner = dump.value;
if (inner === true || inner === false) {
dump.value = next;
return;
}
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
const o = inner;
if (typeof o.value === "boolean") {
o.value = next;
return;
}
}
dump.value = next;
}
async function sceneCommitUndoSnapshot() {
try {
await Editor.Message.request("scene", "snapshot");
} catch {
}
}
async function applyInspectorNodeTransform(uuid, target, record) {
try {
const inode = await Editor.Message.request("scene", "query-node", uuid);
const paths = ["position", "rotation", "scale"];
const keys = ["position", "rotation", "scale"];
for (let i = 0; i < 3; i += 1) {
const p = paths[i];
const raw = inode[p];
if (!raw || typeof raw !== "object") {
return { ok: false, message: `query-node: missing ${p} dump` };
}
}
for (let i = 0; i < 3; i += 1) {
const p = paths[i];
const key = keys[i];
const raw = inode[p];
const dump = cloneActiveDump(raw);
applyVec3ToPropertyDump(dump, target[key]);
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid,
path: p,
dump,
record
}
);
if (!ok) {
return { ok: false, message: `set-property returned false for ${p}` };
}
}
return { ok: true };
} catch (e) {
return { ok: false, message: e instanceof Error ? e.message : String(e) };
}
}
async function applyInspectorNodeActive(uuid, nextActive, record) {
try {
const inode = await Editor.Message.request("scene", "query-node", uuid);
if (!inode?.active || typeof inode.active !== "object") {
return { ok: false, message: "query-node: missing active property dump" };
}
const dump = cloneActiveDump(inode.active);
applyActiveValueToPropertyDump(dump, nextActive);
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid,
path: "active",
dump,
record
}
);
if (!ok) {
return { ok: false, message: "set-property returned false" };
}
return { ok: true };
} catch (e) {
return { ok: false, message: e instanceof Error ? e.message : String(e) };
}
}
async function applyInspectorNodeName(uuid, nextName, record) {
try {
const inode = await Editor.Message.request("scene", "query-node", uuid);
if (!inode?.name || typeof inode.name !== "object") {
return { ok: false, message: "query-node: missing name property dump" };
}
const dump = cloneActiveDump(inode.name);
applyScalarToPropertyDump(dump, nextName);
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid,
path: "name",
dump,
record
}
);
if (!ok) {
return { ok: false, message: "set-property returned false" };
}
return { ok: true };
} catch (e) {
return { ok: false, message: e instanceof Error ? e.message : String(e) };
}
}
async function handleEditorUndo(id) {
const Ed = Editor;
if (typeof Ed.Undo?.undo !== "function") {
return {
op: "editor_undo_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "Editor.Undo.undo is not available in this editor build. Use MCP tool `cocos_mcp_undo_last` for the last MCP write (active/name/create/instantiate_prefab/delete/reparent/transform/component property)."
};
}
try {
await Ed.Undo.undo();
const result = { ok: true, via: "Editor.Undo" };
return { op: "editor_undo_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "editor_undo_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
async function handleMcpUndoLast(id) {
let activeSceneUuid = "";
try {
const meta = await executeSceneMethod("getCurrentSceneMeta", []);
if (inodeRecord(meta) && meta.ok === true && meta.hasScene === true && typeof meta.sceneAssetUuid === "string") {
activeSceneUuid = meta.sceneAssetUuid;
}
} catch {
}
if (!activeSceneUuid) {
return {
op: "mcp_undo_last_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "The active scene is temporarily unavailable. The MCP undo stack was preserved; retry after the scene loads."
};
}
if (!mcpUndoSceneUuid && mcpUndoStack.length > 0) {
return {
op: "mcp_undo_last_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "The MCP undo stack is not bound to a scene. It was preserved; refresh hierarchy before retrying."
};
}
if (mcpUndoSceneUuid && activeSceneUuid !== mcpUndoSceneUuid) {
await clearMcpUndoStack();
return {
op: "mcp_undo_last_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "The active scene changed. The stale MCP undo stack was cleared; refresh hierarchy before writing again."
};
}
mcpUndoSceneUuid = activeSceneUuid;
const entry = mcpUndoStack.pop();
if (!entry) {
return {
op: "mcp_undo_last_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "No MCP write to undo (stack empty, or only no-op writes were recorded)."
};
}
if (entry.kind === "ui_transaction") {
const removed = await removeUiTransactionCreatedNodes(entry.entries, entry.sceneUuid);
if (!removed.ok) {
if (!removed.terminal && removed.residualEntries.length > 0) {
mcpUndoStack.push({ ...entry, entries: removed.residualEntries });
}
return {
op: "mcp_undo_last_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `UI transaction undo failed: ${removed.message}`
};
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "ui_transaction"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "set_node_active") {
const applied = await applyInspectorNodeActive(entry.uuid, entry.previousActive, false);
if (!applied.ok) {
mcpUndoStack.push(entry);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: applied.message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "set_node_active",
active: entry.previousActive
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "set_node_name") {
const appliedN = await applyInspectorNodeName(entry.uuid, entry.previousName, false);
if (!appliedN.ok) {
mcpUndoStack.push(entry);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: appliedN.message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "set_node_name",
name: entry.previousName
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "create_node") {
try {
await Editor.Message.request("scene", "remove-node", { uuid: entry.uuid });
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
try {
await executeSceneMethod("forgetNodeNid", [entry.uuid]);
} catch {
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "create_node"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "instantiate_prefab") {
try {
await Editor.Message.request("scene", "remove-node", { uuid: entry.uuid });
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
try {
await executeSceneMethod("forgetNodeNid", [entry.uuid]);
} catch {
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "instantiate_prefab"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "add_component") {
try {
const inode = await Editor.Message.request("scene", "query-node", entry.uuid);
if (!inodeRecord(inode)) {
throw new Error("query-node failed for add_component undo target");
}
const beforeComps = readComponentDumps(inode);
const beforeCount = beforeComps.length;
const normalizedType = normalizeComponentTypeName(entry.componentType);
const beforeTypeCount = beforeComps.filter((c) => {
const t = readComponentTypeFromDump(c);
return Boolean(t && normalizeComponentTypeName(t) === normalizedType);
}).length;
if (beforeTypeCount === 0) {
await restoreComponentTypeBaselineAfterAdd(entry);
await sceneCommitUndoSnapshot();
const result2 = { ok: true, nid: entry.nid, via: "mcp_local_stack", kind: "add_component" };
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
const preferredIndex = pickUndoAddComponentIndex(beforeComps, entry);
const preferredDump = beforeComps[preferredIndex] ?? entry.addedComponentDump;
const componentUuid = entry.componentUuid ?? readComponentUuidFromDump(entry.addedComponentDump) ?? readComponentUuidFromDump(preferredDump);
let removed = false;
let lastAfterCount = beforeCount;
let lastAfterTypeCount = beforeTypeCount;
const tried = componentUuid ? `componentUuid=${componentUuid}` : "componentUuid=unavailable";
if (componentUuid) {
const exactBefore = await getRuntimeComponentUuidState(entry.uuid, componentUuid);
if (!exactBefore) {
throw new Error(`cannot verify exact component UUID before undo (${componentUuid})`);
}
if (!exactBefore.present) {
removed = true;
} else {
try {
await Editor.Message.request("scene", "remove-component", { uuid: componentUuid });
} catch {
}
const exactAfter = await getRuntimeComponentUuidState(entry.uuid, componentUuid);
if (!exactAfter) {
throw new Error(`component removal outcome is ambiguous for UUID ${componentUuid}`);
}
removed = !exactAfter.present;
try {
const inodeAfterTry = await Editor.Message.request("scene", "query-node", entry.uuid);
if (inodeRecord(inodeAfterTry)) {
const afterComps = readComponentDumps(inodeAfterTry);
lastAfterCount = afterComps.length;
lastAfterTypeCount = afterComps.filter((c) => {
const t = readComponentTypeFromDump(c);
return Boolean(t && normalizeComponentTypeName(t) === normalizedType);
}).length;
}
} catch {
}
}
}
if (!removed && !componentUuid) {
try {
const fb = await executeSceneMethod("removeLastComponentByType", [
entry.uuid,
entry.componentType
]);
if (inodeRecord(fb) && fb.ok === true && typeof fb.removed === "boolean" && fb.removed === true) {
removed = true;
}
} catch {
}
}
if (!removed && !componentUuid) {
try {
const stats = await executeSceneMethod("getComponentStatsByType", [
entry.uuid,
entry.componentType
]);
if (inodeRecord(stats) && stats.ok === true && typeof stats.typeCount === "number" && Number.isFinite(stats.typeCount) && stats.typeCount < beforeTypeCount) {
removed = true;
}
} catch {
}
}
if (!removed) {
throw new Error(
`undo add_component made no structural change (tried=[${tried}], beforeCount=${beforeCount}, afterCount=${lastAfterCount}, beforeTypeCount=${beforeTypeCount}, afterTypeCount=${lastAfterTypeCount})`
);
}
await restoreComponentTypeBaselineAfterAdd(entry);
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
const result = { ok: true, nid: entry.nid, via: "mcp_local_stack", kind: "add_component" };
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "remove_component") {
try {
if (entry.componentUuid) {
const exactState = await getRuntimeComponentUuidState(entry.uuid, entry.componentUuid);
if (!exactState) {
throw new Error(`cannot verify removed component UUID before undo (${entry.componentUuid})`);
}
if (exactState.present) {
const result2 = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "remove_component"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
}
const beforeRaw = await Editor.Message.request("scene", "query-node", entry.uuid);
const beforeComps = inodeRecord(beforeRaw) ? readComponentDumps(beforeRaw) : [];
const beforeCount = beforeComps.length;
const normalizedType = normalizeComponentTypeName(entry.componentType);
const beforeTypeCount = beforeComps.filter((c) => {
const t = readComponentTypeFromDump(c);
return Boolean(t && normalizeComponentTypeName(t) === normalizedType);
}).length;
const componentClassName = entry.componentType.startsWith("cc.") ? entry.componentType : `cc.${entry.componentType}`;
const beforeRuntimeStats = await getRuntimeComponentStats(entry.uuid, componentClassName);
const beforeRuntimeTypeCount = beforeRuntimeStats?.typeCount ?? null;
let createError = null;
let fallbackError = null;
try {
await Editor.Message.request("scene", "create-component", {
uuid: entry.uuid,
component: componentClassName,
record: true
});
} catch (e) {
createError = e instanceof Error ? e.message : String(e);
}
let restored = await waitRuntimeTypeCountAbove(entry.uuid, entry.componentType, beforeRuntimeTypeCount);
if (!restored) {
try {
const fb = await executeSceneMethod("addLastComponentByType", [entry.uuid, componentClassName]);
if (inodeRecord(fb) && fb.ok === true && typeof fb.added === "boolean" && fb.added === true) {
restored = true;
} else if (inodeRecord(fb) && fb.ok === false && typeof fb.message === "string") {
fallbackError = fb.message;
}
} catch (e) {
fallbackError = e instanceof Error ? e.message : String(e);
}
}
if (!restored) {
restored = await waitRuntimeTypeCountAbove(entry.uuid, entry.componentType, beforeRuntimeTypeCount);
}
const inode = await Editor.Message.request("scene", "query-node", entry.uuid);
if (inodeRecord(inode)) {
const comps = readComponentDumps(inode);
const idx = Math.max(0, Math.min(entry.removedComponentIndex, Math.max(0, comps.length - 1)));
const restore = entry.removedComponentDump;
for (const [prop, dump] of Object.entries(restore)) {
if (prop.startsWith("__")) {
continue;
}
if (!inodeRecord(dump)) {
continue;
}
try {
await Editor.Message.request("scene", "set-property", {
uuid: entry.uuid,
path: `__comps__.${idx}.${prop}`,
dump: cloneActiveDump(dump),
record: false
});
} catch {
}
}
}
const afterRaw = await Editor.Message.request("scene", "query-node", entry.uuid);
const afterComps = inodeRecord(afterRaw) ? readComponentDumps(afterRaw) : [];
const afterCount = afterComps.length;
const afterTypeCount = afterComps.filter((c) => {
const t = readComponentTypeFromDump(c);
return Boolean(t && normalizeComponentTypeName(t) === normalizedType);
}).length;
const afterRuntimeStats = await getRuntimeComponentStats(entry.uuid, componentClassName);
const runtimeRestored = beforeRuntimeStats !== null && afterRuntimeStats !== null && (afterRuntimeStats.totalCount > beforeRuntimeStats.totalCount || afterRuntimeStats.typeCount > beforeRuntimeStats.typeCount);
if (!(afterCount > beforeCount || afterTypeCount > beforeTypeCount || runtimeRestored)) {
throw new Error(
`undo remove_component produced no observable restore (type=${entry.componentType}, beforeCount=${beforeCount}, afterCount=${afterCount}, beforeTypeCount=${beforeTypeCount}, afterTypeCount=${afterTypeCount}, createError=${createError ?? "none"}, fallbackError=${fallbackError ?? "none"})`
);
}
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "remove_component"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "duplicate_node") {
try {
await Editor.Message.request("scene", "remove-node", { uuid: entry.uuid });
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
try {
await executeSceneMethod("forgetNodeNid", [entry.uuid]);
} catch {
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "duplicate_node"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "delete_node") {
try {
const restoredRaw = await executeSceneMethod("restoreDetachedNodeForMcpUndo", [entry.uuid]);
if (!(inodeRecord(restoredRaw) && restoredRaw.ok === true && restoredRaw.uuid === entry.uuid)) {
const detail = inodeRecord(restoredRaw) && typeof restoredRaw.message === "string" ? restoredRaw.message : "unexpected response";
const error = new Error(`restoreDetachedNodeForMcpUndo failed: ${detail}`);
error.terminal = inodeRecord(restoredRaw) && restoredRaw.terminal === true;
throw error;
}
await sceneCommitUndoSnapshot();
} catch (e) {
if (!(e instanceof Error && e.terminal === true)) {
mcpUndoStack.push(entry);
}
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
try {
for (const item of entry.subtreeMappings) {
await executeSceneMethod("restoreNidMapping", [item.uuid, item.nid]);
}
} catch {
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "delete_node"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "reparent_node") {
try {
await Editor.Message.request("scene", "set-parent", {
parent: entry.previousParentUuid,
uuids: entry.childUuid,
keepWorldTransform: entry.keepWorldTransform
});
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
const result = {
ok: true,
nid: entry.childNid,
via: "mcp_local_stack",
kind: "reparent_node"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "set_node_sibling_index") {
try {
try {
await Editor.Message.request("scene", "move-node", {
uuid: entry.uuid,
parentUuid: entry.parentUuid,
insertIndex: entry.previousIndex,
keepWorldTransform: false
});
} catch {
const fb = await executeSceneMethod("setSiblingIndexFallback", [
entry.uuid,
entry.previousIndex
]);
if (!(inodeRecord(fb) && fb.ok === true)) {
throw new Error("setSiblingIndexFallback failed during undo");
}
}
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "set_node_sibling_index"
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "set_component_property") {
const appliedC = await applyInspectorComponentProperty(
entry.uuid,
entry.path,
entry.previousDump,
false
);
if (!appliedC.ok) {
mcpUndoStack.push(entry);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: appliedC.message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "set_component_property",
propertyPath: entry.path
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "set_component_scalar_direct") {
try {
const restored = await executeSceneMethod("setComponentScalarPropertyDirect", [
entry.uuid,
entry.componentIndex,
entry.property,
entry.previousValue
]);
if (!inodeRecord(restored) || restored.ok !== true || !valuesEqualForChange(restored.afterValue, entry.previousValue)) {
throw new Error(`runtime-direct scalar restore failed for ${entry.property}`);
}
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "set_component_property",
propertyPath: `[runtime-direct-value].${entry.property}`
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "set_component_asset_direct") {
try {
if (entry.previousAssetUuid) {
const restore = await executeSceneMethod("setComponentAssetRefDirect", [
entry.uuid,
entry.componentType,
entry.property,
entry.previousAssetUuid
]);
if (!(inodeRecord(restore) && restore.ok === true)) {
throw new Error("setComponentAssetRefDirect restore failed");
}
} else {
const cleared = await executeSceneMethod("clearComponentAssetRef", [
entry.uuid,
entry.componentType,
entry.property
]);
if (!(inodeRecord(cleared) && cleared.ok === true)) {
throw new Error("clearComponentAssetRef restore failed");
}
}
const verifyRaw = await executeSceneMethod("getComponentAssetRefUuid", [
entry.uuid,
entry.componentType,
entry.property
]);
if (inodeRecord(verifyRaw) && verifyRaw.ok === true) {
const actual = typeof verifyRaw.uuid === "string" ? verifyRaw.uuid : null;
const expected = entry.previousAssetUuid ?? null;
if (actual !== expected) {
throw new Error(
`undo set_component_property runtime mismatch (expected=${String(expected)}, actual=${String(actual)})`
);
}
}
await sceneCommitUndoSnapshot();
} catch (e) {
mcpUndoStack.push(entry);
const message = e instanceof Error ? e.message : String(e);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "set_component_property",
propertyPath: `${entry.componentType}.${entry.property}`
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
if (entry.kind === "set_node_transform") {
const appliedT = await applyInspectorNodeTransform(entry.uuid, entry.previous, false);
if (!appliedT.ok) {
mcpUndoStack.push(entry);
return { op: "mcp_undo_last_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: appliedT.message };
}
const result = {
ok: true,
nid: entry.nid,
via: "mcp_local_stack",
kind: "set_node_transform",
reverted: ["position", "rotation", "scale"]
};
return { op: "mcp_undo_last_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
mcpUndoStack.push(entry);
return {
op: "mcp_undo_last_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "Internal error: unknown MCP undo entry kind."
};
}
async function handleSetNodeActive(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareSetNodeActive", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_active_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_set_node_active_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isSetNodeActivePrepareOk(prepRaw)) {
return {
op: "scene_set_node_active_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareSetNodeActive returned unexpected shape"
};
}
try {
const inode = await Editor.Message.request("scene", "query-node", prepRaw.uuid);
if (!inode?.active || typeof inode.active !== "object") {
return {
op: "scene_set_node_active_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "query-node: missing active property dump"
};
}
const dump = cloneActiveDump(inode.active);
let previousActive = readActiveFromPropertyDump(dump);
if (previousActive === null) {
try {
const live = await executeSceneMethod("getNodeActiveState", [prepRaw.uuid]);
if (inodeRecord(live) && live.ok === true && typeof live.active === "boolean") {
previousActive = live.active;
}
} catch {
}
}
applyActiveValueToPropertyDump(dump, payload.active);
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid: prepRaw.uuid,
path: "active",
dump,
record: true
}
);
if (!ok) {
return {
op: "scene_set_node_active_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "set-property returned false"
};
}
await sceneCommitUndoSnapshot();
if (previousActive !== null && previousActive !== payload.active) {
mcpUndoStack.push({
kind: "set_node_active",
uuid: prepRaw.uuid,
nid: payload.nid,
previousActive
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
const result = { ok: true, nid: payload.nid, active: payload.active };
return { op: "scene_set_node_active_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_active_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
async function handleSetNodeName(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareSetNodeName", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_name_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_set_node_name_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareSetNodeNameOk(prepRaw)) {
return {
op: "scene_set_node_name_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareSetNodeName returned unexpected shape"
};
}
const prep = prepRaw;
try {
const inode = await Editor.Message.request("scene", "query-node", prep.uuid);
if (!inode?.name || typeof inode.name !== "object") {
return {
op: "scene_set_node_name_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "query-node: missing name property dump"
};
}
const nameDump = inode.name;
const previousScalar = readScalarFromPropertyDump(nameDump);
if (typeof previousScalar !== "string") {
return {
op: "scene_set_node_name_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "query-node: name is not a string scalar"
};
}
const dump = cloneActiveDump(nameDump);
applyScalarToPropertyDump(dump, prep.name);
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid: prep.uuid,
path: "name",
dump,
record: true
}
);
if (!ok) {
return {
op: "scene_set_node_name_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "set-property returned false"
};
}
await sceneCommitUndoSnapshot();
if (previousScalar !== prep.name) {
mcpUndoStack.push({
kind: "set_node_name",
uuid: prep.uuid,
nid: payload.nid,
previousName: previousScalar
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
const result = { ok: true, nid: payload.nid, name: prep.name };
return { op: "scene_set_node_name_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_name_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
async function handleCreateNode(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareCreateNode", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_create_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_create_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareCreateNodeOk(prepRaw)) {
return {
op: "scene_create_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareCreateNode returned unexpected shape"
};
}
const prep = prepRaw;
let newUuid;
try {
newUuid = await Editor.Message.request(
"scene",
"create-node",
{
parent: prep.parentUuid,
name: prep.name,
snapshot: true
}
);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_create_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (typeof newUuid !== "string" || newUuid.length === 0) {
return {
op: "scene_create_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "create-node did not return a non-empty uuid string"
};
}
await sceneCommitUndoSnapshot();
let regRaw;
try {
regRaw = await executeSceneMethod("registerNidForNewNode", [newUuid]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
const rollback = await rollbackCreatedNodeWithVerification(newUuid);
if (!rollback.ok) {
return {
op: "scene_create_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `partial_failure: create-node succeeded, register failed (${message}), and rollback failed (${rollback.message}).`
};
}
return {
op: "scene_create_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `registerNidForNewNode failed: ${message}`
};
}
if (!isRegisterNidForNewNodeOk(regRaw)) {
const rollback = await rollbackCreatedNodeWithVerification(newUuid);
if (!rollback.ok) {
return {
op: "scene_create_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `partial_failure: create-node succeeded, register returned unexpected shape, and rollback failed (${rollback.message}).`
};
}
return {
op: "scene_create_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "registerNidForNewNode returned unexpected shape"
};
}
mcpUndoStack.push({ kind: "create_node", uuid: newUuid, nid: regRaw.nid });
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const result = {
ok: true,
parentNid: payload.parentNid,
nid: regRaw.nid,
name: prep.name
};
return { op: "scene_create_node_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
async function handleDeleteNode(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareDeleteNode", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_delete_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_delete_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareDeleteNodeOk(prepRaw)) {
return {
op: "scene_delete_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareDeleteNode returned unexpected shape"
};
}
const prep = prepRaw;
let mappedEntriesRaw;
try {
mappedEntriesRaw = await executeSceneMethod("snapshotNidMappingsInSubtree", [prep.uuid]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return {
op: "scene_delete_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `snapshotNidMappingsInSubtree failed: ${message}`
};
}
const mappedEntries = inodeRecord(mappedEntriesRaw) && Array.isArray(mappedEntriesRaw.entries) ? mappedEntriesRaw.entries.filter((x) => typeof x.uuid === "string" && typeof x.nid === "number").map((x) => ({ uuid: x.uuid, nid: x.nid })) : [];
try {
const detached = await executeSceneMethod("detachNodeForMcpUndo", [prep.uuid]);
if (!(inodeRecord(detached) && detached.ok === true && detached.uuid === prep.uuid)) {
const detail = inodeRecord(detached) && typeof detached.message === "string" ? detached.message : "unexpected response";
return {
op: "scene_delete_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `detachNodeForMcpUndo failed: ${detail}`
};
}
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_delete_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
try {
await executeSceneMethod(
"forgetNidMappingsByUuids",
[mappedEntries.map((e) => e.uuid)]
);
} catch {
}
await sceneCommitUndoSnapshot();
mcpUndoStack.push({
kind: "delete_node",
uuid: prep.uuid,
nid: payload.nid,
subtreeMappings: mappedEntries
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const result = { ok: true, nid: payload.nid };
return { op: "scene_delete_node_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
async function handleReparentNode(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareReparentNode", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_reparent_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_reparent_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareReparentNodeOk(prepRaw)) {
return {
op: "scene_reparent_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareReparentNode returned unexpected shape"
};
}
const prep = prepRaw;
if (!prep.noOp) {
try {
await Editor.Message.request("scene", "set-parent", {
parent: prep.newParentUuid,
uuids: prep.childUuid,
keepWorldTransform: prep.keepWorldTransform
});
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_reparent_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
await sceneCommitUndoSnapshot();
mcpUndoStack.push({
kind: "reparent_node",
childUuid: prep.childUuid,
childNid: payload.childNid,
previousParentUuid: prep.previousParentUuid,
keepWorldTransform: prep.keepWorldTransform
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
const result = {
ok: true,
childNid: payload.childNid,
parentNid: payload.parentNid
};
if (prep.noOp) {
result.unchanged = true;
}
return { op: "scene_reparent_node_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
async function handleSetNodeSiblingIndex(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareSetNodeSiblingIndex", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_sibling_index_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_set_node_sibling_index_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareSetNodeSiblingIndexOk(prepRaw)) {
return {
op: "scene_set_node_sibling_index_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareSetNodeSiblingIndex returned unexpected shape"
};
}
const prep = prepRaw;
if (!prep.noOp) {
try {
try {
await Editor.Message.request("scene", "move-node", {
uuid: prep.uuid,
parentUuid: prep.parentUuid,
insertIndex: prep.index,
keepWorldTransform: false
});
} catch {
const fb = await executeSceneMethod("setSiblingIndexFallback", [
prep.uuid,
prep.index
]);
if (!(inodeRecord(fb) && fb.ok === true)) {
throw new Error("setSiblingIndexFallback failed");
}
}
await sceneCommitUndoSnapshot();
mcpUndoStack.push({
kind: "set_node_sibling_index",
uuid: prep.uuid,
nid: payload.nid,
parentUuid: prep.parentUuid,
previousIndex: prep.previousIndex
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_sibling_index_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
const result = { ok: true, nid: payload.nid, index: prep.index };
if (prep.noOp) {
result.unchanged = true;
}
return { op: "scene_set_node_sibling_index_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
async function readSceneNodeDisplayName(nodeUuid) {
try {
const inode = await Editor.Message.request("scene", "query-node", nodeUuid);
if (inode?.name && typeof inode.name === "object") {
const s = readScalarFromPropertyDump(inode.name);
if (typeof s === "string" && s.trim().length > 0) {
return s.trim();
}
}
} catch {
}
return "Instance";
}
async function queryPrefabAssetUuidForInstantiate(dbUrl) {
try {
const info = await Editor.Message.request("asset-db", "query-asset-info", dbUrl);
if (!info || typeof info.uuid !== "string" || info.uuid.length === 0) {
return { ok: false, message: "Prefab asset not found in asset-db (check path and import)." };
}
if (info.isDirectory === true) {
return { ok: false, message: "prefabPath must be a file, not a directory." };
}
if (info.invalid === true) {
return { ok: false, message: "Prefab asset is invalid or not finished importing." };
}
const ty = (info.type || "").toLowerCase();
const im = (info.importer || "").toLowerCase();
if (!ty.includes("prefab") && !im.includes("prefab")) {
return { ok: false, message: "Asset is not a prefab (unexpected type/importer)." };
}
return { ok: true, uuid: info.uuid };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { ok: false, message: `asset-db query failed: ${message}` };
}
}
async function handleInstantiatePrefab(id, payload) {
const pathNorm = normalizePrefabAssetRelativePath(payload.prefabPath);
if (!pathNorm.ok) {
return { op: "scene_instantiate_prefab_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: pathNorm.message };
}
const dbUrl = `db://${pathNorm.rel}`;
const assetRes = await queryPrefabAssetUuidForInstantiate(dbUrl);
if (!assetRes.ok) {
return { op: "scene_instantiate_prefab_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: assetRes.message };
}
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareInstantiatePrefab", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_instantiate_prefab_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_instantiate_prefab_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareInstantiatePrefabOk(prepRaw)) {
return {
op: "scene_instantiate_prefab_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareInstantiatePrefab returned unexpected shape"
};
}
const prep = prepRaw;
let newUuid;
try {
newUuid = await Editor.Message.request(
"scene",
"create-node",
{
parent: prep.parentUuid,
assetUuid: assetRes.uuid,
snapshot: true
}
);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_instantiate_prefab_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (typeof newUuid !== "string" || newUuid.length === 0) {
return {
op: "scene_instantiate_prefab_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "create-node did not return a non-empty uuid string"
};
}
await sceneCommitUndoSnapshot();
let regRaw;
try {
regRaw = await executeSceneMethod("registerNidForNewNode", [newUuid]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
const rollback = await rollbackCreatedNodeWithVerification(newUuid);
if (!rollback.ok) {
return {
op: "scene_instantiate_prefab_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `partial_failure: instantiate succeeded, register failed (${message}), and rollback failed (${rollback.message}).`
};
}
return {
op: "scene_instantiate_prefab_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `registerNidForNewNode failed: ${message}`
};
}
if (!isRegisterNidForNewNodeOk(regRaw)) {
const rollback = await rollbackCreatedNodeWithVerification(newUuid);
if (!rollback.ok) {
return {
op: "scene_instantiate_prefab_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `partial_failure: instantiate succeeded, register returned unexpected shape, and rollback failed (${rollback.message}).`
};
}
return {
op: "scene_instantiate_prefab_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "registerNidForNewNode returned unexpected shape"
};
}
mcpUndoStack.push({ kind: "instantiate_prefab", uuid: newUuid, nid: regRaw.nid });
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const nodeName = await readSceneNodeDisplayName(newUuid);
const result = {
ok: true,
parentNid: payload.parentNid,
nid: regRaw.nid,
prefabPath: pathNorm.rel,
nodeName
};
return { op: "scene_instantiate_prefab_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
async function handleAddComponent(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareAddComponent", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_add_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_add_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareAddComponentOk(prepRaw)) {
return {
op: "scene_add_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareAddComponent returned unexpected shape"
};
}
try {
const inodeBefore = await Editor.Message.request("scene", "query-node", prepRaw.uuid);
if (!inodeRecord(inodeBefore)) {
return { op: "scene_add_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: "query-node failed for target node" };
}
const beforeComps = readComponentDumps(inodeBefore);
const beforeSigs = new Set(beforeComps.map((c) => safeJsonSignature(c)));
const beforeCount = readComponentDumpCount(inodeBefore);
const beforeComponentTypeCounts = {};
for (const component of beforeComps) {
const type = readComponentTypeFromDump(component);
if (!type) continue;
const normalized = normalizeComponentTypeName(type);
beforeComponentTypeCounts[normalized] = (beforeComponentTypeCounts[normalized] ?? 0) + 1;
}
const requestedType = normalizeComponentTypeName(prepRaw.componentType);
const beforeRequestedTypeCount = beforeComps.filter((component) => {
const type = readComponentTypeFromDump(component);
return Boolean(type && normalizeComponentTypeName(type) === requestedType);
}).length;
const createRet = await Editor.Message.request("scene", "create-component", {
uuid: prepRaw.uuid,
component: prepRaw.componentType,
record: true
});
await sceneCommitUndoSnapshot();
const inodeAfter = await Editor.Message.request("scene", "query-node", prepRaw.uuid);
const afterCount = inodeRecord(inodeAfter) ? readComponentDumpCount(inodeAfter) : beforeCount;
const afterComps = inodeRecord(inodeAfter) ? readComponentDumps(inodeAfter) : [];
const afterRequestedTypeIndexes = afterComps.map((component, index) => ({ component, index })).filter(({ component }) => {
const type = readComponentTypeFromDump(component);
return Boolean(type && normalizeComponentTypeName(type) === requestedType);
}).map(({ index }) => index);
let addedComponentDump = {};
const addedCandidates = afterComps.filter((component) => !beforeSigs.has(safeJsonSignature(component)));
const requestedCandidate = addedCandidates.find((component) => {
const type = readComponentTypeFromDump(component);
return Boolean(type && normalizeComponentTypeName(type) === requestedType);
});
const hasStructuralChange = afterCount > beforeCount || addedCandidates.length > 0;
if (!hasStructuralChange) {
return {
op: "scene_add_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `create-component produced no observable component change (requested=${prepRaw.componentType}, return=${String(createRet)}).`
};
}
if (afterRequestedTypeIndexes.length <= beforeRequestedTypeCount) {
const addedTypes = addedCandidates.map((component) => readComponentTypeFromDump(component) ?? "unknown").join(",");
return {
op: "scene_add_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `create-component changed the node but did not add the requested type (requested=${prepRaw.componentType}, added=[${addedTypes || "unknown"}], return=${String(createRet)}).`
};
}
const componentIndex = requestedCandidate ? Math.max(
0,
afterComps.findIndex(
(component) => safeJsonSignature(component) === safeJsonSignature(requestedCandidate)
)
) : afterRequestedTypeIndexes[afterRequestedTypeIndexes.length - 1];
if (requestedCandidate) {
addedComponentDump = cloneActiveDump(requestedCandidate);
} else if (afterComps[componentIndex]) {
addedComponentDump = cloneActiveDump(afterComps[componentIndex]);
}
const actualType = readComponentTypeFromDump(addedComponentDump);
if (actualType && normalizeComponentTypeName(actualType) !== normalizeComponentTypeName(prepRaw.componentType)) {
return {
op: "scene_add_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `component type mismatch after create-component (expected=${prepRaw.componentType}, actual=${actualType})`
};
}
mcpUndoStack.push({
kind: "add_component",
uuid: prepRaw.uuid,
nid: payload.nid,
componentType: prepRaw.componentType,
componentIndex,
componentUuid: readComponentUuidFromDump(addedComponentDump) ?? void 0,
addedComponentDump,
addedComponentTypes: addedCandidates.map((component) => readComponentTypeFromDump(component)).filter((type) => Boolean(type)),
beforeComponentTypeCounts
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const result = {
ok: true,
nid: payload.nid,
componentType: prepRaw.componentType,
componentIndex
};
return { op: "scene_add_component_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_add_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
async function handleRemoveComponent(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareRemoveComponent", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_remove_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_remove_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareRemoveComponentOk(prepRaw)) {
return {
op: "scene_remove_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareRemoveComponent returned unexpected shape"
};
}
if (typeof payload.expectedComponentType === "string" && payload.expectedComponentType.trim()) {
const expectedNorm = normalizeComponentTypeName(payload.expectedComponentType);
const actualNorm = normalizeComponentTypeName(prepRaw.componentType);
if (expectedNorm !== actualNorm) {
return {
op: "scene_remove_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `remove target type mismatch at index ${prepRaw.componentIndex}: expected=${payload.expectedComponentType}, actual=${prepRaw.componentType}. Refresh inspect and use the latest index.`
};
}
}
try {
const inodeBefore = await Editor.Message.request("scene", "query-node", prepRaw.uuid);
if (!inodeRecord(inodeBefore)) {
return { op: "scene_remove_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: "query-node failed for target node" };
}
const beforeComps = readComponentDumps(inodeBefore);
const beforeCount = beforeComps.length;
const normalizedType = normalizeComponentTypeName(prepRaw.componentType);
const beforeTypeCount = beforeComps.filter((c) => {
const t = readComponentTypeFromDump(c);
return Boolean(t && normalizeComponentTypeName(t) === normalizedType);
}).length;
const beforeRuntimeTypeCount = await getRuntimeComponentTypeCount(prepRaw.uuid, prepRaw.componentType);
const removedComponentDump = beforeComps[prepRaw.componentIndex] && inodeRecord(beforeComps[prepRaw.componentIndex]) ? cloneActiveDump(beforeComps[prepRaw.componentIndex]) : {};
const componentUuid = prepRaw.componentUuid ?? readComponentUuidFromDump(removedComponentDump);
let removeRet = void 0;
let removeIpcError = null;
if (componentUuid) {
try {
removeRet = await Editor.Message.request("scene", "remove-component", {
uuid: componentUuid
});
} catch (e) {
removeIpcError = e instanceof Error ? e.message : String(e);
}
} else {
removeIpcError = "component uuid unavailable; skipped official remove-component message";
}
const exactAfter = componentUuid ? await getRuntimeComponentUuidState(prepRaw.uuid, componentUuid) : null;
if (componentUuid && !exactAfter) {
mcpUndoStack.push({
kind: "remove_component",
uuid: prepRaw.uuid,
nid: payload.nid,
componentUuid,
componentType: prepRaw.componentType,
removedComponentIndex: prepRaw.componentIndex,
removedComponentDump
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
return {
op: "scene_remove_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `remove-component outcome is ambiguous for component UUID ${componentUuid}; a conservative cocos_mcp_undo_last entry was retained.`
};
}
let inodeAfter = null;
try {
inodeAfter = await Editor.Message.request("scene", "query-node", prepRaw.uuid);
} catch {
inodeAfter = null;
}
const afterComps = inodeRecord(inodeAfter) ? readComponentDumps(inodeAfter) : beforeComps;
const afterCount = afterComps.length;
const afterTypeCount = afterComps.filter((c) => {
const t = readComponentTypeFromDump(c);
return Boolean(t && normalizeComponentTypeName(t) === normalizedType);
}).length;
const afterRuntimeTypeCount = await getRuntimeComponentTypeCount(prepRaw.uuid, prepRaw.componentType);
const removedObserved = componentUuid ? exactAfter?.present === false : afterCount < beforeCount || afterTypeCount < beforeTypeCount || beforeRuntimeTypeCount !== null && afterRuntimeTypeCount !== null && afterRuntimeTypeCount < beforeRuntimeTypeCount;
const removedObservedAfterWait = removedObserved ? true : componentUuid ? false : await waitRuntimeTypeCountBelow(prepRaw.uuid, prepRaw.componentType, beforeRuntimeTypeCount);
if (!removedObservedAfterWait) {
const protectedNoOpTypes = /* @__PURE__ */ new Set(["sprite", "uitransform", "canvas", "camera", "directionallight"]);
if (!componentUuid && !protectedNoOpTypes.has(normalizedType)) {
try {
const fb = await executeSceneMethod("removeLastComponentByType", [
prepRaw.uuid,
prepRaw.componentType
]);
if (inodeRecord(fb) && fb.ok === true && typeof fb.removed === "boolean" && fb.removed === true) {
await sceneCommitUndoSnapshot();
mcpUndoStack.push({
kind: "remove_component",
uuid: prepRaw.uuid,
nid: payload.nid,
componentUuid: componentUuid ?? void 0,
componentType: prepRaw.componentType,
removedComponentIndex: prepRaw.componentIndex,
removedComponentDump
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const result2 = {
ok: true,
nid: payload.nid,
componentIndex: prepRaw.componentIndex,
componentType: prepRaw.componentType
};
return { op: "scene_remove_component_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
const removedAfterFallbackWait = await waitRuntimeTypeCountBelow(
prepRaw.uuid,
prepRaw.componentType,
beforeRuntimeTypeCount
);
if (removedAfterFallbackWait) {
await sceneCommitUndoSnapshot();
mcpUndoStack.push({
kind: "remove_component",
uuid: prepRaw.uuid,
nid: payload.nid,
componentUuid: componentUuid ?? void 0,
componentType: prepRaw.componentType,
removedComponentIndex: prepRaw.componentIndex,
removedComponentDump
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const result2 = {
ok: true,
nid: payload.nid,
componentIndex: prepRaw.componentIndex,
componentType: prepRaw.componentType
};
return { op: "scene_remove_component_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
} catch {
}
}
return {
op: "scene_remove_component_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `remove-component produced no observable change (index=${prepRaw.componentIndex}, type=${prepRaw.componentType}, beforeCount=${beforeCount}, afterCount=${afterCount}, beforeTypeCount=${beforeTypeCount}, afterTypeCount=${afterTypeCount}, componentUuid=${componentUuid ?? "unavailable"}, removeRet=${String(removeRet)}, removeIpcError=${removeIpcError ?? "none"}).`
};
}
await sceneCommitUndoSnapshot();
mcpUndoStack.push({
kind: "remove_component",
uuid: prepRaw.uuid,
nid: payload.nid,
componentUuid: componentUuid ?? void 0,
componentType: prepRaw.componentType,
removedComponentIndex: prepRaw.componentIndex,
removedComponentDump
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const result = {
ok: true,
nid: payload.nid,
componentIndex: prepRaw.componentIndex,
componentType: prepRaw.componentType
};
return { op: "scene_remove_component_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_remove_component_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
async function handleDuplicateNode(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareDuplicateNode", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_duplicate_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_duplicate_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareDuplicateNodeOk(prepRaw)) {
return {
op: "scene_duplicate_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareDuplicateNode returned unexpected shape"
};
}
const childBefore = await getParentChildUuidSetForDuplicate(prepRaw.parentUuid);
let duplicateRet;
try {
duplicateRet = await Editor.Message.request("scene", "duplicate-node", {
uuid: prepRaw.uuid,
record: true
});
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_duplicate_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
let newUuid = typeof duplicateRet === "string" ? duplicateRet : typeof duplicateRet === "object" && duplicateRet !== null ? typeof duplicateRet.uuid === "string" ? duplicateRet.uuid ?? "" : Array.isArray(duplicateRet) && typeof duplicateRet[0] === "string" ? duplicateRet[0] : "" : "";
if (!newUuid) {
const childAfter = await getParentChildUuidSetForDuplicate(prepRaw.parentUuid);
const created = [...childAfter].filter((u) => !childBefore.has(u));
if (created.length === 1) {
newUuid = created[0];
panelLog(
"warn",
`duplicate-node returned non-standard value; recovered uuid by parent child-diff (uuid=${newUuid}).`
);
} else if (created.length > 1) {
const prefer = [...created].reverse().find((u) => u !== prepRaw.uuid);
newUuid = prefer ?? created[created.length - 1];
panelLog(
"warn",
`duplicate-node ambiguous child-diff (${created.length} new); picked last non-source uuid=${newUuid} raw=${safeJsonSignature(duplicateRet)}`
);
} else {
panelLog(
"warn",
`duplicate-node unresolved return; raw=${safeJsonSignature(duplicateRet)} recoveredCandidates=${safeJsonSignature(created)} beforeCount=${childBefore.size} afterCount=${childAfter.size}`
);
}
}
if (!newUuid) {
try {
const fb = await executeSceneMethod("duplicateNodeFallback", [prepRaw.uuid]);
if (inodeRecord(fb) && fb.ok === true && typeof fb.uuid === "string" && fb.uuid.length > 0) {
newUuid = fb.uuid;
panelLog(
"warn",
`duplicate-node IPC was a no-op; used scene duplicateNodeFallback (uuid=${newUuid}).`
);
}
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
panelLog("warn", `duplicateNodeFallback failed: ${message}`);
}
}
if (!newUuid) {
return {
op: "scene_duplicate_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `duplicate-node produced no new child under parent (ipc=${String(duplicateRet)}); and fallback duplicateNodeFallback failed. This Creator build likely does not support duplication.`
};
}
await sceneCommitUndoSnapshot();
let regRaw;
try {
regRaw = await executeSceneMethod("registerNidForNewNode", [newUuid]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
const rollback = await rollbackCreatedNodeWithVerification(newUuid);
if (!rollback.ok) {
return {
op: "scene_duplicate_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `partial_failure: duplicate succeeded, register failed (${message}), and rollback failed (${rollback.message}).`
};
}
return {
op: "scene_duplicate_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `registerNidForNewNode failed: ${message}`
};
}
if (!isRegisterNidForNewNodeOk(regRaw)) {
const rollback = await rollbackCreatedNodeWithVerification(newUuid);
if (!rollback.ok) {
return {
op: "scene_duplicate_node_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `partial_failure: duplicate succeeded, register returned unexpected shape, and rollback failed (${rollback.message}).`
};
}
return { op: "scene_duplicate_node_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: "registerNidForNewNode returned unexpected shape" };
}
let nodeName = "DuplicatedNode";
try {
const inode = await Editor.Message.request("scene", "query-node", newUuid);
if (inodeRecord(inode) && typeof inode.name === "string") {
nodeName = inode.name;
}
} catch {
}
mcpUndoStack.push({ kind: "duplicate_node", uuid: newUuid, nid: regRaw.nid });
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const result = { ok: true, sourceNid: payload.nid, nid: regRaw.nid, name: nodeName };
return { op: "scene_duplicate_node_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
async function handleSaveScene(id) {
try {
const meta = await executeSceneMethod("getCurrentSceneMeta", []);
if (!meta || meta.ok !== true || meta.hasScene !== true || typeof meta.sceneAssetUuid !== "string") {
return {
op: "scene_save_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "No active named scene to save."
};
}
const info = await Editor.Message.request(
"asset-db",
"query-asset-info",
meta.sceneAssetUuid
);
if (!info || typeof info.file !== "string" || info.file.length === 0) {
return {
op: "scene_save_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "\u5F53\u524D\u4E3A\u672A\u547D\u540D\u7684\u4E34\u65F6\u573A\u666F\u3002\u4E3A\u9632\u6B62\u7CFB\u7EDF\u6B7B\u9501\uFF0C\u8BF7\u4EBA\u7C7B\u7528\u6237\u5148\u6309 Ctrl+S \u624B\u52A8\u4FDD\u5B58\u4E00\u6B21\uFF0C\u4E4B\u540E\u5373\u53EF\u7531 AI \u81EA\u52A8\u4FDD\u5B58\u3002"
};
}
await Editor.Message.request("scene", "save-scene");
const result = { ok: true };
return { op: "scene_save_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_save_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
function parseFiniteVec3(v) {
if (!isRecord(v)) {
return null;
}
const x = v.x;
const y = v.y;
const z = v.z;
if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number") {
return null;
}
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
return null;
}
return { x, y, z };
}
async function handleSetNodeTransform(id, payload) {
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareSetNodeTransform", [{ nid: payload.nid }]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_transform_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_set_node_transform_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isSetNodeActivePrepareOk(prepRaw)) {
return {
op: "scene_set_node_transform_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareSetNodeTransform returned unexpected shape"
};
}
try {
const inode = await Editor.Message.request("scene", "query-node", prepRaw.uuid);
const posR = inode.position ? readVec3FromPropertyDump(inode.position) : null;
const rotR = inode.rotation ? readVec3FromPropertyDump(inode.rotation) : null;
const scR = inode.scale ? readVec3FromPropertyDump(inode.scale) : null;
if (!posR || !rotR || !scR) {
return {
op: "scene_set_node_transform_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "query-node: could not read position, rotation, and scale as vec3"
};
}
const previous = {
position: cloneVec3(posR),
rotation: cloneVec3(rotR),
scale: cloneVec3(scR)
};
const toApply = [];
if (payload.position) {
toApply.push({ path: "position", vec: payload.position });
}
if (payload.rotationEuler) {
toApply.push({ path: "rotation", vec: payload.rotationEuler });
}
if (payload.scale) {
toApply.push({ path: "scale", vec: payload.scale });
}
const applied = [];
let anyChange = false;
for (const item of toApply) {
const rawProp = inode[item.path];
if (!rawProp || typeof rawProp !== "object") {
return {
op: "scene_set_node_transform_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `query-node: missing dump for ${item.path}`
};
}
const dump = cloneActiveDump(rawProp);
const before = readVec3FromPropertyDump(dump);
applyVec3ToPropertyDump(dump, item.vec);
const after = readVec3FromPropertyDump(dump);
if (before && after && !vec3NearlyEqual(before, after)) {
anyChange = true;
}
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid: prepRaw.uuid,
path: item.path,
dump,
record: true
}
);
if (!ok) {
return {
op: "scene_set_node_transform_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "set-property returned false"
};
}
applied.push(item.path);
}
await sceneCommitUndoSnapshot();
if (anyChange && applied.length > 0) {
mcpUndoStack.push({
kind: "set_node_transform",
uuid: prepRaw.uuid,
nid: payload.nid,
previous
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
const result = { ok: true, nid: payload.nid, applied };
return { op: "scene_set_node_transform_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_node_transform_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
async function handleSetComponentProperty(id, payload) {
if (typeof payload.value === "string" && payload.value.trim().replace(/\\/g, "/").toLowerCase().startsWith("assets/") && (payload.property.trim() === "spriteFrame" || payload.property.trim() === "font")) {
const pathAsset = await resolveAssetUuidFromPath(payload.value);
if (!pathAsset.ok) {
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: pathAsset.message
};
}
payload = { ...payload, value: pathAsset.uuid };
}
let prepRaw;
try {
prepRaw = await executeSceneMethod("prepareSetComponentProperty", [payload]);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_component_property_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (isSetNodeActivePrepareErr(prepRaw)) {
return { op: "scene_set_component_property_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: prepRaw.message };
}
if (!isPrepareSetComponentPropertyOk(prepRaw)) {
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prepareSetComponentProperty returned unexpected shape"
};
}
const prep = prepRaw;
try {
let valueForDump = prep.value;
const propTrim = payload.property.trim();
const isAssetRefProp = prep.componentType === "Sprite" && propTrim === "spriteFrame" || prep.componentType === "Label" && propTrim === "font";
let targetAssetUuid = null;
if (prep.value && typeof prep.value === "object" && !Array.isArray(prep.value) && typeof prep.value.__uuid__ === "string") {
const uuidRaw = String(prep.value.__uuid__).trim();
if (prep.componentType === "Sprite" && propTrim === "spriteFrame") {
const resolved = await resolveSpriteFrameUuidFromAnyAssetUuid(uuidRaw);
if (!resolved.ok) {
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: resolved.message
};
}
valueForDump = { __uuid__: resolved.uuid };
targetAssetUuid = resolved.uuid;
} else if (prep.componentType === "Label" && propTrim === "font") {
const resolved = await resolveFontUuid(uuidRaw);
if (!resolved.ok) {
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: resolved.message
};
}
valueForDump = { __uuid__: resolved.uuid };
targetAssetUuid = resolved.uuid;
}
}
const inode = await Editor.Message.request("scene", "query-node", prep.uuid);
if (!inodeRecord(inode)) {
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "query-node: expected object root"
};
}
let effectivePath = prep.path;
let rawDump = getDumpAtInodePath(inode, effectivePath);
if (!rawDump) {
const alt = resolveComponentPropertyPathFallback(
inode,
prep.path,
prep.componentType,
payload.property.trim()
);
if (alt) {
effectivePath = alt;
rawDump = getDumpAtInodePath(inode, effectivePath);
}
}
if (!rawDump) {
if (isAssetRefProp && targetAssetUuid) {
let directErr = null;
let directRaw = await executeSceneMethod("setComponentAssetRefDirect", [
prep.uuid,
prep.componentType,
propTrim,
targetAssetUuid
]);
if (inodeRecord(directRaw) && directRaw.ok === false && typeof directRaw.message === "string" && (directRaw.message.includes("assetManager.assets.get is unavailable") || directRaw.message.includes("assetManager.loadAny is unavailable"))) {
directRaw = await executeSceneMethod("setComponentAssetRefFromScene", [
prep.uuid,
prep.componentType,
propTrim,
targetAssetUuid
]);
}
if (inodeRecord(directRaw) && directRaw.ok === true) {
const changedDirect = directRaw.changed === true;
if (changedDirect) {
mcpUndoStack.push({
kind: "set_component_asset_direct",
uuid: prep.uuid,
nid: payload.nid,
componentType: prep.componentType,
property: propTrim,
previousAssetUuid: typeof directRaw.beforeUuid === "string" ? directRaw.beforeUuid : null
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
await sceneCommitUndoSnapshot();
const result2 = {
ok: true,
nid: payload.nid,
componentIndex: payload.componentIndex,
componentType: prep.componentType,
property: propTrim,
changed: changedDirect,
effectivePath: "[runtime-direct]",
resolvedAssetUuid: targetAssetUuid,
beforeValue: toDebugScalar(directRaw.beforeUuid),
afterValue: toDebugScalar(directRaw.afterUuid)
};
return { op: "scene_set_component_property_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
if (inodeRecord(directRaw) && directRaw.ok === false && typeof directRaw.message === "string") {
directErr = directRaw.message;
}
if (directErr) {
return { op: "scene_set_component_property_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: directErr };
}
}
if (!isAssetRefProp) {
const directRaw = await executeSceneMethod("setComponentScalarPropertyDirect", [
prep.uuid,
payload.componentIndex,
propTrim,
valueForDump
]);
if (inodeRecord(directRaw) && directRaw.ok === true) {
const changedDirect = directRaw.changed === true;
if (changedDirect) {
mcpUndoStack.push({
kind: "set_component_scalar_direct",
uuid: prep.uuid,
nid: payload.nid,
componentIndex: payload.componentIndex,
property: propTrim,
previousValue: directRaw.beforeValue
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
await sceneCommitUndoSnapshot();
const result2 = {
ok: true,
nid: payload.nid,
componentIndex: payload.componentIndex,
componentType: prep.componentType,
property: propTrim,
changed: changedDirect,
effectivePath: "[runtime-direct-value]",
beforeValue: toDebugScalar(directRaw.beforeValue),
afterValue: toDebugScalar(directRaw.afterValue)
};
return { op: "scene_set_component_property_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
if (inodeRecord(directRaw) && directRaw.ok === false && typeof directRaw.message === "string") {
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: directRaw.message
};
}
}
return {
// Final fallback: direct runtime component assignment (requires asset to be loaded).
// This path is used when query-node dump is unavailable in some Creator builds.
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `query-node: no dump at path ${prep.path} (fallback unresolved)`
};
}
const previousDump = cloneActiveDump(rawDump);
const beforeValue = readComparableValueFromPropertyDump(rawDump);
const dump = cloneActiveDump(rawDump);
applyScalarToPropertyDump(dump, valueForDump);
const afterValue = readComparableValueFromPropertyDump(dump);
const changed = !valuesEqualForChange(beforeValue, afterValue);
const ok = await Editor.Message.request(
"scene",
"set-property",
{
uuid: prep.uuid,
path: effectivePath,
dump,
record: true
}
);
if (!ok) {
if (isAssetRefProp && targetAssetUuid) {
let directErr = null;
let directRaw = await executeSceneMethod("setComponentAssetRefDirect", [
prep.uuid,
prep.componentType,
propTrim,
targetAssetUuid
]);
if (inodeRecord(directRaw) && directRaw.ok === false && typeof directRaw.message === "string" && (directRaw.message.includes("assetManager.assets.get is unavailable") || directRaw.message.includes("assetManager.loadAny is unavailable"))) {
directRaw = await executeSceneMethod("setComponentAssetRefFromScene", [
prep.uuid,
prep.componentType,
propTrim,
targetAssetUuid
]);
}
if (inodeRecord(directRaw) && directRaw.ok === true) {
const changedDirect = directRaw.changed === true;
if (changedDirect) {
mcpUndoStack.push({
kind: "set_component_asset_direct",
uuid: prep.uuid,
nid: payload.nid,
componentType: prep.componentType,
property: propTrim,
previousAssetUuid: typeof directRaw.beforeUuid === "string" ? directRaw.beforeUuid : null
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
await sceneCommitUndoSnapshot();
const result2 = {
ok: true,
nid: payload.nid,
componentIndex: payload.componentIndex,
componentType: prep.componentType,
property: payload.property.trim(),
changed: changedDirect,
effectivePath: "[runtime-direct]",
resolvedAssetUuid: targetAssetUuid,
beforeValue: toDebugScalar(directRaw.beforeUuid),
afterValue: toDebugScalar(directRaw.afterUuid)
};
return { op: "scene_set_component_property_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
if (inodeRecord(directRaw) && directRaw.ok === false && typeof directRaw.message === "string") {
directErr = directRaw.message;
}
if (directErr) {
return { op: "scene_set_component_property_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: directErr };
}
}
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "set-property returned false"
};
}
if (isAssetRefProp && targetAssetUuid) {
try {
const afterRaw = await executeSceneMethod("getComponentAssetRefUuid", [
prep.uuid,
prep.componentType,
propTrim
]);
if (inodeRecord(afterRaw) && afterRaw.ok === true) {
const actualUuid = typeof afterRaw.uuid === "string" ? afterRaw.uuid : null;
if (actualUuid !== targetAssetUuid) {
return {
op: "scene_set_component_property_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `asset write did not apply to runtime component (target=${targetAssetUuid}, actual=${String(actualUuid)}). Please provide a valid SpriteFrame/Font UUID (not texture/other asset UUID).`
};
}
}
} catch {
}
}
await sceneCommitUndoSnapshot();
if (changed) {
mcpUndoStack.push({
kind: "set_component_property",
uuid: prep.uuid,
nid: payload.nid,
path: effectivePath,
previousDump
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
}
const result = {
ok: true,
nid: payload.nid,
componentIndex: payload.componentIndex,
componentType: prep.componentType,
property: payload.property.trim(),
changed,
effectivePath,
resolvedAssetUuid: valueForDump && typeof valueForDump === "object" && !Array.isArray(valueForDump) && typeof valueForDump.__uuid__ === "string" ? String(valueForDump.__uuid__) : void 0,
beforeValue: toDebugScalar(beforeValue),
afterValue: toDebugScalar(afterValue)
};
return { op: "scene_set_component_property_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_set_component_property_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
function uiSpecStats(nodes) {
let count = 0;
let depth = 0;
const pending = [{ items: nodes, level: 1 }];
while (pending.length > 0) {
const current = pending.pop();
const { items, level } = current;
depth = Math.max(depth, level);
for (const item of items) {
count += item.kind === "button" ? 2 : 1;
if (item.kind === "button") {
depth = Math.max(depth, level + 1);
}
if (item.children && item.children.length > 0) {
pending.push({ items: item.children, level: level + 1 });
}
}
}
return { count, depth };
}
function requireWsResult(response, expectedOp) {
if (response.op !== expectedOp) {
const message = "message" in response && typeof response.message === "string" ? response.message : `unexpected response: ${response.op}`;
throw new Error(message);
}
return response.result;
}
async function removeUiTransactionCreatedNodes(entries, sceneUuid) {
const createEntries = entries.filter(
(entry) => entry.kind === "create_node"
);
const createdUuids = createEntries.map((entry) => entry.uuid);
if (createEntries.length === 0) {
return {
ok: false,
message: "UI transaction contains no created nodes to remove.",
residualEntries: [],
terminal: true
};
}
try {
const result = await executeSceneMethod("removeCreatedNodesForMcpUndo", [
createdUuids,
sceneUuid
]);
if (!(inodeRecord(result) && result.ok === true && Array.isArray(result.removed))) {
const detail = inodeRecord(result) && typeof result.message === "string" ? result.message : "unexpected response";
const remaining = inodeRecord(result) && Array.isArray(result.remaining) ? new Set(result.remaining.filter((value) => typeof value === "string")) : new Set(createdUuids);
return {
ok: false,
message: detail,
residualEntries: createEntries.filter((entry) => remaining.has(entry.uuid)),
terminal: inodeRecord(result) && result.terminal === true
};
}
await sceneCommitUndoSnapshot();
return { ok: true };
} catch (error) {
return {
ok: false,
message: error instanceof Error ? error.message : String(error),
residualEntries: createEntries,
terminal: false
};
}
}
async function rollbackUiTransaction(startIndex, sceneUuid) {
const entries = mcpUndoStack.splice(startIndex);
if (entries.length === 0) {
return [];
}
const removed = await removeUiTransactionCreatedNodes(entries, sceneUuid);
if (!removed.ok && !removed.terminal && removed.residualEntries.length > 0) {
const residualRoot = removed.residualEntries.find((entry) => entry.kind === "create_node");
mcpUndoStack.push({
kind: "ui_transaction",
nid: residualRoot?.nid ?? 0,
sceneUuid,
entries: removed.residualEntries
});
}
return removed.ok ? [] : [removed.message];
}
async function addUiComponent(nid, componentType) {
const response = await handleAddComponent(`ui-add-${nid}-${componentType}`, { nid, componentType });
const result = requireWsResult(response, "scene_add_component_ok");
return result.componentIndex;
}
async function findUiComponentIndex(nid, componentType) {
const inspected = await executeSceneMethod("inspectNode", [{ nid }]);
if (!inspected || inspected.ok !== true) {
const message = inspected && "message" in inspected ? inspected.message : "inspectNode returned no result";
throw new Error(`Unable to inspect UI node ${nid}: ${message}`);
}
const requestedType = normalizeComponentTypeName(componentType);
const index = inspected.data.components.findIndex(
(existingType) => normalizeComponentTypeName(existingType) === requestedType
);
return index >= 0 ? index : null;
}
async function ensureUiComponent(nid, componentType) {
const existingIndex = await findUiComponentIndex(nid, componentType);
if (existingIndex !== null) {
return existingIndex;
}
try {
return await addUiComponent(nid, componentType);
} catch (error) {
const racedIndex = await findUiComponentIndex(nid, componentType);
if (racedIndex !== null) {
return racedIndex;
}
throw error;
}
}
async function setUiComponentProperty(nid, componentIndex, property, value) {
const response = await handleSetComponentProperty(`ui-prop-${nid}-${componentIndex}-${property}`, {
nid,
componentIndex,
property,
value
});
requireWsResult(response, "scene_set_component_property_ok");
}
async function applyUiTransform(nid, node) {
const uiTransform = await ensureUiComponent(nid, "UITransform");
const defaults = node.kind === "label" ? { width: 220, height: 48 } : node.kind === "button" ? { width: 180, height: 56 } : node.kind === "container" ? { width: 320, height: 180 } : { width: 100, height: 100 };
const size = node.size ?? defaults;
await setUiComponentProperty(nid, uiTransform, "width", size.width);
await setUiComponentProperty(nid, uiTransform, "height", size.height);
return uiTransform;
}
async function applyUiWidget(nid, widget) {
const componentIndex = await ensureUiComponent(nid, "Widget");
const alignments = [
{ key: "top", enabledProperty: "isAlignTop", valueProperty: "top" },
{ key: "bottom", enabledProperty: "isAlignBottom", valueProperty: "bottom" },
{ key: "left", enabledProperty: "isAlignLeft", valueProperty: "left" },
{ key: "right", enabledProperty: "isAlignRight", valueProperty: "right" },
{
key: "horizontalCenter",
enabledProperty: "isAlignHorizontalCenter",
valueProperty: "horizontalCenter"
},
{
key: "verticalCenter",
enabledProperty: "isAlignVerticalCenter",
valueProperty: "verticalCenter"
}
];
for (const alignment of alignments) {
const value = widget[alignment.key];
if (typeof value === "number") {
await setUiComponentProperty(nid, componentIndex, alignment.enabledProperty, true);
await setUiComponentProperty(nid, componentIndex, alignment.valueProperty, value);
}
}
}
async function applyUiLayout(nid, layout) {
const componentIndex = await ensureUiComponent(nid, "Layout");
await setUiComponentProperty(nid, componentIndex, "type", layout.type.toUpperCase());
await setUiComponentProperty(
nid,
componentIndex,
"resizeMode",
(layout.resizeMode ?? "none").toUpperCase()
);
const requestedConstraint = layout.constraint ?? (layout.type === "grid" && layout.constraintNum ? "fixed-column" : void 0);
if (requestedConstraint) {
const constraint = requestedConstraint === "fixed-column" ? "FIXED_COL" : requestedConstraint === "fixed-row" ? "FIXED_ROW" : "NONE";
await setUiComponentProperty(nid, componentIndex, "constraint", constraint);
}
const scalarProperties = [
"spacingX",
"spacingY",
"paddingLeft",
"paddingRight",
"paddingTop",
"paddingBottom",
"constraintNum"
];
for (const property of scalarProperties) {
const value = layout[property];
if (typeof value === "number") {
await setUiComponentProperty(nid, componentIndex, property, value);
}
}
if (layout.cellSize) {
await setUiComponentProperty(nid, componentIndex, "cellSize", layout.cellSize);
}
}
async function managedShapeSpriteFrameUuid(shape) {
const asset = ensureManagedShapeAsset(Editor.Project.path, shape);
const resolved = await resolveAssetUuidFromPath(asset.relativePath, asset.created);
if (!resolved.ok) {
throw new Error(resolved.message);
}
const existingSpriteFrame = await resolveSpriteFrameUuidFromAnyAssetUuid(resolved.uuid);
if (existingSpriteFrame.ok) {
return existingSpriteFrame.uuid;
}
const normalized = normalizeProjectAssetRelativePath(asset.relativePath);
if (!normalized.ok) {
throw new Error(normalized.message);
}
const rawMeta = await Editor.Message.request(
"asset-db",
"query-asset-meta",
normalized.dbUrl
);
const prepared = prepareManagedShapeSpriteFrameMeta(rawMeta);
if (prepared.changed) {
await Editor.Message.request(
"asset-db",
"save-asset-meta",
normalized.dbUrl,
JSON.stringify(prepared.meta)
);
}
await Editor.Message.request("asset-db", "reimport-asset", normalized.dbUrl);
let lastMessage = existingSpriteFrame.message;
for (let attempt = 0; attempt < 50; attempt += 1) {
const spriteFrame = await resolveSpriteFrameUuidFromAnyAssetUuid(resolved.uuid);
if (spriteFrame.ok) {
return spriteFrame.uuid;
}
lastMessage = spriteFrame.message;
await sleep(100);
}
throw new Error(`Managed shape SpriteFrame import did not finish within 5 seconds: ${lastMessage}`);
}
async function applyUiSprite(nid, options) {
const componentIndex = await ensureUiComponent(nid, "Sprite");
await setUiComponentProperty(nid, componentIndex, "sizeMode", "CUSTOM");
let spriteFrameValue;
if (options.managedShape) {
spriteFrameValue = await managedShapeSpriteFrameUuid(options.managedShape);
} else if (options.assetPath) {
spriteFrameValue = options.assetPath;
}
if (spriteFrameValue) {
await setUiComponentProperty(nid, componentIndex, "spriteFrame", spriteFrameValue);
}
if (options.color) {
await setUiComponentProperty(nid, componentIndex, "color", options.color);
}
if (options.nineSlice) {
await setUiComponentProperty(nid, componentIndex, "type", "SLICED");
}
}
async function applyUiLabel(nid, node) {
const componentIndex = await ensureUiComponent(nid, "Label");
await setUiComponentProperty(nid, componentIndex, "overflow", "CLAMP");
await setUiComponentProperty(nid, componentIndex, "string", node.text ?? node.buttonLabel ?? "Label");
await setUiComponentProperty(nid, componentIndex, "fontSize", node.fontSize ?? 28);
await setUiComponentProperty(nid, componentIndex, "lineHeight", node.lineHeight ?? node.fontSize ?? 32);
await setUiComponentProperty(
nid,
componentIndex,
"horizontalAlign",
(node.horizontalAlign ?? "center").toUpperCase()
);
await setUiComponentProperty(
nid,
componentIndex,
"verticalAlign",
(node.verticalAlign ?? "center").toUpperCase()
);
if (node.color) {
await setUiComponentProperty(nid, componentIndex, "color", node.color);
}
await applyUiTransform(nid, node);
}
async function createUiNodeRecursive(parentNid, node, created, verification, sequence) {
sequence.value += 1;
const defaultName = `${node.kind[0].toUpperCase()}${node.kind.slice(1)}_${sequence.value}`;
const name = node.name?.trim() || defaultName;
const createResponse = await handleCreateNode(`ui-create-${sequence.value}`, { parentNid, name });
const createdNode = requireWsResult(createResponse, "scene_create_node_ok");
const nid = createdNode.nid;
created.push({ ...node.id ? { id: node.id } : {}, nid, name: createdNode.name, kind: node.kind });
verification.push({ nid, expected: { ...node, name: createdNode.name } });
await applyUiTransform(nid, node);
if (node.position) {
const transformResponse = await handleSetNodeTransform(`ui-transform-${nid}`, {
nid,
position: node.position
});
requireWsResult(transformResponse, "scene_set_node_transform_ok");
}
if (node.active === false) {
const activeResponse = await handleSetNodeActive(`ui-active-${nid}`, { nid, active: false });
requireWsResult(activeResponse, "scene_set_node_active_ok");
}
if (node.kind === "shape") {
await applyUiSprite(nid, {
managedShape: node.shape ?? "rectangle",
color: node.color ?? "#FFFFFFFF"
});
} else if (node.kind === "label") {
await applyUiLabel(nid, node);
} else if (node.kind === "sprite") {
if (!node.assetPath) {
throw new Error(`Sprite node "${name}" requires assetPath.`);
}
await applyUiSprite(nid, {
assetPath: node.assetPath,
color: node.color,
nineSlice: node.nineSlice
});
} else if (node.kind === "button") {
await applyUiSprite(nid, {
managedShape: "rounded-rectangle",
color: node.color ?? "#3478F6FF"
});
const buttonIndex = await ensureUiComponent(nid, "Button");
await setUiComponentProperty(nid, buttonIndex, "interactable", node.interactable !== false);
await setUiComponentProperty(nid, buttonIndex, "transition", "SCALE");
await setUiComponentProperty(nid, buttonIndex, "zoomScale", 0.92);
const labelNode = {
kind: "label",
name: `${name}_Label`,
text: node.buttonLabel ?? node.text ?? "Button",
fontSize: node.fontSize ?? 24,
size: node.size ?? { width: 180, height: 56 },
horizontalAlign: "center",
verticalAlign: "center"
};
await createUiNodeRecursive(nid, labelNode, created, verification, sequence);
}
if (node.widget) {
await applyUiWidget(nid, node.widget);
}
for (const child of node.children ?? []) {
await createUiNodeRecursive(nid, child, created, verification, sequence);
}
if (node.layout) {
await applyUiLayout(nid, node.layout);
}
return nid;
}
async function handleApplyUiSpec(id, payload) {
const stats = uiSpecStats(payload.nodes);
if (stats.count < 1 || stats.count > 64 || stats.depth > 12) {
return {
op: "scene_apply_ui_spec_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `UI spec exceeds limits (nodes=${stats.count}/64, depth=${stats.depth}/12).`
};
}
const startIndex = mcpUndoStack.length;
let transactionSceneUuid = "";
const created = [];
const verification = [];
try {
const sceneMeta = await executeSceneMethod("getCurrentSceneMeta", []);
if (!inodeRecord(sceneMeta) || sceneMeta.ok !== true || sceneMeta.hasScene !== true || typeof sceneMeta.sceneAssetUuid !== "string" || sceneMeta.sceneAssetUuid.length === 0) {
throw new Error("Unable to bind the UI transaction to the active scene.");
}
transactionSceneUuid = sceneMeta.sceneAssetUuid;
const sequence = { value: 0 };
for (const node of payload.nodes) {
await createUiNodeRecursive(payload.parentNid, node, created, verification, sequence);
}
const settleAndVerify = async () => {
const settled = await executeSceneMethod("settleUiNodes", [
verification.map((entry) => entry.nid)
]);
if (!settled || settled.ok !== true) {
throw new Error(settled?.message ?? "Runtime UI settle returned no result.");
}
for (const entry of verification) {
const verified = await executeSceneMethod("verifyUiNode", [entry]);
if (!verified || verified.ok !== true) {
throw new Error(
verified?.message ?? `Runtime verification failed for nid ${entry.nid} (${entry.expected.name}).`
);
}
}
};
await settleAndVerify();
await sleep(50);
await settleAndVerify();
if (payload.save !== false) {
const saveResponse = await handleSaveScene(`${id}:save`);
requireWsResult(saveResponse, "scene_save_ok");
}
const entries = mcpUndoStack.splice(startIndex);
const transactionNid = created[0]?.nid ?? payload.parentNid;
mcpUndoStack.push({
kind: "ui_transaction",
nid: transactionNid,
sceneUuid: transactionSceneUuid,
entries
});
while (mcpUndoStack.length > MCP_UNDO_MAX) {
mcpUndoStack.shift();
}
const diagnostics = [
{
level: "info",
code: "runtime_verified",
message: `${created.length} UI node(s) and their final components, settled sizes, colors, text, layout, and widget properties were read back from the Creator Scene runtime.`
}
];
const layoutOwnedSizes = verification.filter(
(entry) => entry.expected.layout?.resizeMode === "container"
).length;
if (layoutOwnedSizes > 0) {
diagnostics.push({
level: "info",
code: "layout_owns_container_size",
message: `${layoutOwnedSizes} container size(s) were settled from children, padding, and spacing because Layout.resizeMode is container.`
});
}
const result = {
status: "ok",
changed: created.length > 0,
verified: true,
undoAvailable: entries.length > 0,
diagnostics,
nextAction: payload.save === false ? "Call cocos_save_scene when the layout is ready to persist." : null,
parentNid: payload.parentNid,
created
};
return { op: "scene_apply_ui_spec_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (error) {
const rollbackStepErrors = await rollbackUiTransaction(startIndex, transactionSceneUuid);
const residualNodes = [];
for (const receipt of created) {
try {
const inspected = await executeSceneMethod("inspectNode", [{ nid: receipt.nid }]);
if (inspected?.ok === true) {
residualNodes.push(`${receipt.name} (nid ${receipt.nid})`);
}
} catch {
}
}
const rollbackErrors = [
...mcpUndoStack.length > startIndex ? [`${mcpUndoStack.length - startIndex} undo entry(s) remain after rollback.`] : [],
...residualNodes.length > 0 ? [`Residual UI nodes: ${residualNodes.join(", ")}`] : []
];
const message = error instanceof Error ? error.message : String(error);
const diagnostics = [
{ level: "error", code: "ui_apply_failed", message },
...rollbackStepErrors.map((rollbackWarning) => ({
level: rollbackErrors.length === 0 ? "warning" : "error",
code: "rollback_step_failed",
message: rollbackWarning
})),
...rollbackErrors.length === 0 ? [{ level: "info", code: "rollback_complete", message: "All partial scene changes were rolled back." }] : rollbackErrors.map((rollbackError) => ({
level: "error",
code: "rollback_failed",
message: rollbackError
}))
];
return {
op: "scene_apply_ui_spec_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: rollbackErrors.length === 0 ? message : `${message}; rollback: ${rollbackErrors.join("; ")}`,
diagnostics
};
}
}
async function handleGetCurrentSceneMeta(id) {
let raw;
try {
raw = await executeSceneMethod("getCurrentSceneMeta", []);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "scene_get_current_meta_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (!isRecord(raw) || raw.ok !== true) {
return {
op: "scene_get_current_meta_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "getCurrentSceneMeta returned unexpected shape"
};
}
if (raw.hasScene === false) {
const result2 = { ok: true, hasScene: false };
return { op: "scene_get_current_meta_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result: result2 };
}
if (raw.hasScene !== true || typeof raw.sceneName !== "string" || typeof raw.sceneAssetUuid !== "string") {
return {
op: "scene_get_current_meta_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "getCurrentSceneMeta missing sceneName or sceneAssetUuid"
};
}
let assetPath;
try {
const info = await Editor.Message.request(
"asset-db",
"query-asset-info",
raw.sceneAssetUuid
);
if (info && typeof info.file === "string" && info.file.length > 0) {
const rel = path3.relative(Editor.Project.path, info.file).replace(/\\/g, "/");
if (!rel.startsWith("..") && !path3.isAbsolute(rel)) {
assetPath = rel;
}
}
} catch {
}
const result = {
ok: true,
hasScene: true,
sceneName: raw.sceneName,
...assetPath !== void 0 ? { assetPath } : {}
};
return { op: "scene_get_current_meta_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
}
async function forwardToScene(id, method, args) {
try {
const raw = await executeSceneMethod(method, args);
if (method === "getHierarchy") {
return {
op: "scene_hierarchy_ok",
id,
v: SUPPORTED_PROTOCOL_VERSION,
result: typeof raw === "string" ? raw : String(raw)
};
}
if (method === "searchNodes") {
return {
op: "scene_search_ok",
id,
v: SUPPORTED_PROTOCOL_VERSION,
result: typeof raw === "string" ? raw : String(raw)
};
}
return {
op: "scene_inspect_ok",
id,
v: SUPPORTED_PROTOCOL_VERSION,
result: raw
};
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
if (method === "getHierarchy") {
return { op: "scene_hierarchy_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
if (method === "searchNodes") {
return { op: "scene_search_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
return { op: "scene_inspect_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
function safeRelativeProjectPath(raw, fallback) {
const s = typeof raw === "string" && raw.trim() ? raw.trim().replace(/\\/g, "/") : fallback;
if (s.includes("..") || path3.isAbsolute(s)) {
return fallback;
}
return s;
}
function normalizeProjectAssetRelativePath(raw, options = {}) {
if (typeof raw !== "string") {
return { ok: false, message: "assetPath must be a string." };
}
const value = raw.trim().replace(/\\/g, "/").replace(/^db:\/\//, "");
if (!value || value.length > 512 || value.includes("..") || path3.isAbsolute(value)) {
return { ok: false, message: "assetPath must be a project-relative assets/... path without .. segments." };
}
const rel = value.replace(/^\/+/, "");
if (!rel.toLowerCase().startsWith("assets/")) {
return { ok: false, message: "assetPath must start with assets/." };
}
const absolute = path3.resolve(Editor.Project.path, rel);
const assetsRoot = path3.resolve(Editor.Project.path, "assets");
const fromAssets = path3.relative(assetsRoot, absolute);
if (fromAssets.startsWith("..") || path3.isAbsolute(fromAssets)) {
return { ok: false, message: "assetPath must resolve inside the project assets directory." };
}
if (options.mustExist !== false && !fs3.existsSync(absolute)) {
return { ok: false, message: `Asset file does not exist: ${rel}` };
}
return { ok: true, rel, dbUrl: `db://${rel}` };
}
async function resolveAssetUuidFromPath(assetPath, refresh = false) {
const normalized = normalizeProjectAssetRelativePath(assetPath);
if (!normalized.ok) {
return normalized;
}
if (refresh) {
try {
await Editor.Message.request("asset-db", "refresh-asset", normalized.dbUrl);
} catch {
}
}
for (let attempt = 0; attempt < 30; attempt += 1) {
try {
const info = await Editor.Message.request(
"asset-db",
"query-asset-info",
normalized.dbUrl
);
if (info && info.invalid !== true && typeof info.uuid === "string" && info.uuid.length > 0) {
return { ok: true, uuid: info.uuid, rel: normalized.rel };
}
} catch {
}
await sleep(100);
}
return { ok: false, message: `Asset DB did not import ${normalized.rel} within 3 seconds.` };
}
function normalizePrefabAssetRelativePath(raw) {
if (typeof raw !== "string") {
return { ok: false, message: "prefabPath must be a string." };
}
const t = raw.trim().replace(/\\/g, "/");
if (!t || t.length > 512) {
return { ok: false, message: "prefabPath is empty or too long (max 512)." };
}
if (t.includes("..")) {
return { ok: false, message: "prefabPath must not contain .." };
}
if (path3.isAbsolute(t)) {
return { ok: false, message: "prefabPath must be relative to project root (e.g. assets/Foo.prefab)." };
}
const relForward = t.replace(/^\/+/, "");
if (!relForward.toLowerCase().startsWith("assets/")) {
return { ok: false, message: "prefabPath must start with assets/" };
}
if (!relForward.toLowerCase().endsWith(".prefab")) {
return { ok: false, message: "prefabPath must end with .prefab" };
}
const absFile = path3.resolve(Editor.Project.path, relForward);
const assetsRoot = path3.resolve(Editor.Project.path, "assets");
const relFromAssets = path3.relative(assetsRoot, absFile);
if (relFromAssets.startsWith("..") || path3.isAbsolute(relFromAssets)) {
return { ok: false, message: "prefabPath must resolve inside assets/" };
}
if (!fs3.existsSync(absFile)) {
return { ok: false, message: "Prefab file does not exist on disk at the given path." };
}
try {
const st = fs3.statSync(absFile);
if (!st.isFile()) {
return { ok: false, message: "prefabPath must point to a file." };
}
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { ok: false, message };
}
return { ok: true, rel: relForward };
}
function extractSpriteFrameUuidFromSubAssets(subAssets) {
if (!subAssets || typeof subAssets !== "object" || Array.isArray(subAssets)) {
return null;
}
for (const v of Object.values(subAssets)) {
if (!v || typeof v !== "object" || Array.isArray(v)) {
continue;
}
const item = v;
const ty = typeof item.type === "string" ? item.type.toLowerCase() : "";
const im = typeof item.importer === "string" ? item.importer.toLowerCase() : "";
const uuid = typeof item.uuid === "string" ? item.uuid : "";
if (uuid && (ty.includes("spriteframe") || im.includes("sprite-frame") || im.includes("spriteframe"))) {
return uuid;
}
}
return null;
}
function parseAssetMeta(rawMeta) {
if (typeof rawMeta === "string") {
try {
const parsed = JSON.parse(rawMeta);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
}
return rawMeta && typeof rawMeta === "object" && !Array.isArray(rawMeta) ? rawMeta : null;
}
async function resolveSpriteFrameUuidFromAnyAssetUuid(uuid) {
try {
const info = await Editor.Message.request("asset-db", "query-asset-info", uuid);
if (!info || typeof info !== "object") {
return { ok: false, message: "\u8D44\u6E90 UUID \u65E0\u6548\u6216\u672A\u5BFC\u5165\u5B8C\u6210\u3002" };
}
const type = typeof info.type === "string" ? info.type.toLowerCase() : "";
const importer = typeof info.importer === "string" ? info.importer.toLowerCase() : "";
if (type.includes("spriteframe") || importer.includes("sprite-frame") || importer.includes("spriteframe")) {
return { ok: true, uuid };
}
const subUuid = extractSpriteFrameUuidFromSubAssets(info.subAssets);
if (subUuid) {
return { ok: true, uuid: subUuid };
}
const rawMeta = await Editor.Message.request("asset-db", "query-asset-meta", uuid);
const meta = parseAssetMeta(rawMeta);
const metaSubUuid = extractSpriteFrameUuidFromSubAssets(meta?.subMetas);
if (metaSubUuid) {
return { ok: true, uuid: metaSubUuid };
}
return {
ok: false,
message: "\u4F20\u5165\u7684\u8D44\u6E90\u4E0D\u662F SpriteFrame\uFF0C\u4E14\u65E0\u6CD5\u4ECE subAssets \u81EA\u52A8\u89E3\u6790 SpriteFrame \u5B50\u8D44\u6E90\u3002\u8BF7\u4F20\u5165 SpriteFrame UUID\u3002"
};
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { ok: false, message: `asset-db query failed: ${message}` };
}
}
async function resolveFontUuid(uuid) {
try {
const info = await Editor.Message.request("asset-db", "query-asset-info", uuid);
if (!info || typeof info !== "object") {
return { ok: false, message: "\u5B57\u4F53\u8D44\u6E90 UUID \u65E0\u6548\u6216\u672A\u5BFC\u5165\u5B8C\u6210\u3002" };
}
const type = typeof info.type === "string" ? info.type.toLowerCase() : "";
const importer = typeof info.importer === "string" ? info.importer.toLowerCase() : "";
if (type.includes("font") || importer.includes("font") || importer.includes("ttf") || importer.includes("bitmap-font") || importer.includes("bmfont")) {
return { ok: true, uuid };
}
return { ok: false, message: "\u4F20\u5165\u7684\u8D44\u6E90\u4E0D\u662F\u5B57\u4F53\uFF08Font\uFF09\u8D44\u6E90\u3002\u8BF7\u4F20\u5165 Font UUID\u3002" };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { ok: false, message: `asset-db query failed: ${message}` };
}
}
function readProjectLogTail(relPath, maxLines) {
const full = path3.join(Editor.Project.path, relPath);
if (!fs3.existsSync(full)) {
return { ok: false, message: `Log not found: ${relPath}` };
}
try {
const text = fs3.readFileSync(full, "utf-8");
const lines = text.split(/\r?\n/);
const tail = lines.slice(-maxLines).join("\n");
return { ok: true, text: tail };
} catch (e) {
const m = e instanceof Error ? e.message : String(e);
return { ok: false, message: m };
}
}
function handleProjectGetInfo(id) {
try {
const result = {
ok: true,
projectName: Editor.Project.name,
projectPath: Editor.Project.path,
creatorVersion: Editor.App.version
};
return { op: "project_get_info_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "project_get_info_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
function normalizeExtensionEntries(raw) {
if (raw === void 0 || raw === null) {
return { ok: true, set: /* @__PURE__ */ new Set() };
}
if (!Array.isArray(raw)) {
return { ok: false, message: "extensions must be an array of strings" };
}
if (raw.length > 64) {
return { ok: false, message: "extensions array too long (max 64)" };
}
const set = /* @__PURE__ */ new Set();
for (const x of raw) {
if (typeof x !== "string" || !x.trim()) {
return { ok: false, message: "each extension must be a non-empty string" };
}
let s = x.trim().toLowerCase();
if (!s.startsWith(".")) {
s = `.${s}`;
}
if (!/^\.[a-z0-9][a-z0-9._-]*$/i.test(s) || s.length > 32) {
return { ok: false, message: `invalid extension token: ${x}` };
}
set.add(s);
}
return { ok: true, set };
}
function resolveAssetsScanRoot(relRaw) {
const assetsRoot = path3.join(Editor.Project.path, "assets");
if (!fs3.existsSync(assetsRoot)) {
return { ok: false, message: "assets/ directory not found" };
}
const rel = typeof relRaw === "string" && relRaw.trim() ? relRaw.trim().replace(/\\/g, "/") : "";
if (rel.includes("..")) {
return { ok: false, message: "relativeDir must not contain .." };
}
if (path3.isAbsolute(rel)) {
return { ok: false, message: "relativeDir must be relative" };
}
const full = path3.resolve(assetsRoot, rel);
const normRoot = path3.resolve(assetsRoot);
const relFromRoot = path3.relative(normRoot, full);
if (relFromRoot.startsWith("..") || path3.isAbsolute(relFromRoot)) {
return { ok: false, message: "relativeDir must stay under assets/" };
}
if (!fs3.existsSync(full)) {
return { ok: false, message: "relativeDir does not exist under assets/" };
}
const st = fs3.statSync(full);
if (!st.isDirectory()) {
return { ok: false, message: "relativeDir must be a directory under assets/" };
}
return { ok: true, abs: full };
}
function collectListAssetFiles(params) {
const { scanRootAbs, projectRoot, maxResults, extFilter } = params;
const paths = [];
let truncated = false;
const projectRootNorm = path3.resolve(projectRoot);
const walk = (dir) => {
if (paths.length >= maxResults) {
truncated = true;
return;
}
let entries;
try {
entries = fs3.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
entries.sort((a, b) => a.name.localeCompare(b.name));
for (const ent of entries) {
if (paths.length >= maxResults) {
truncated = true;
return;
}
const full = path3.join(dir, ent.name);
if (ent.isDirectory()) {
walk(full);
} else if (ent.isFile()) {
const ext = path3.extname(ent.name).toLowerCase();
if (extFilter && extFilter.size > 0 && !extFilter.has(ext)) {
continue;
}
const rel = path3.relative(projectRootNorm, path3.resolve(full)).replace(/\\/g, "/");
if (!rel.startsWith("assets/")) {
continue;
}
paths.push(rel);
if (paths.length >= maxResults) {
truncated = true;
}
}
}
};
walk(scanRootAbs);
paths.sort((a, b) => a.localeCompare(b));
return { ok: true, paths, truncated };
}
function handleProjectListAssets(id, payload) {
try {
const pl = payload !== void 0 && payload !== null && isRecord(payload) ? payload : {};
const maxResults = Math.min(2e3, Math.max(1, Number(pl.maxResults) || 200));
const rootRes = resolveAssetsScanRoot(pl.relativeDir);
if (!rootRes.ok) {
return { op: "project_list_assets_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: rootRes.message };
}
const extRes = normalizeExtensionEntries(pl.extensions);
if (!extRes.ok) {
return { op: "project_list_assets_err", id, v: SUPPORTED_PROTOCOL_VERSION, message: extRes.message };
}
const extFilter = extRes.set.size > 0 ? extRes.set : null;
const result = collectListAssetFiles({
scanRootAbs: rootRes.abs,
projectRoot: Editor.Project.path,
maxResults,
extFilter
});
return { op: "project_list_assets_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "project_list_assets_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
async function handleProjectGetPrefabInfo(id, payload) {
try {
const pl = payload !== void 0 && payload !== null && isRecord(payload) ? payload : {};
const prefabPathRaw = pl.prefabPath;
if (typeof prefabPathRaw !== "string") {
return {
op: "project_get_prefab_info_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires string prefabPath"
};
}
const pathNorm = normalizePrefabAssetRelativePath(prefabPathRaw);
if (!pathNorm.ok) {
return {
op: "project_get_prefab_info_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: pathNorm.message
};
}
const dbUrl = `db://${pathNorm.rel}`;
const info = await Editor.Message.request("asset-db", "query-asset-info", dbUrl);
if (!info || typeof info !== "object") {
return {
op: "project_get_prefab_info_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "Prefab asset not found in asset-db (check path and import)."
};
}
if (info.isDirectory === true) {
return {
op: "project_get_prefab_info_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "prefabPath must be a file, not a directory."
};
}
const type = typeof info.type === "string" ? info.type : "";
const importer = typeof info.importer === "string" ? info.importer : "";
const ty = type.toLowerCase();
const im = importer.toLowerCase();
if (!ty.includes("prefab") && !im.includes("prefab")) {
return {
op: "project_get_prefab_info_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "Asset is not a prefab (unexpected type/importer)."
};
}
const name = typeof info.name === "string" ? info.name : "";
const displayName = typeof info.displayName === "string" ? info.displayName : name;
const imported = info.imported === true;
const invalid = info.invalid === true;
let subAssetCount;
const sub = info.subAssets;
if (sub && typeof sub === "object" && !Array.isArray(sub)) {
subAssetCount = Object.keys(sub).length;
}
let dependencyCount;
try {
const deps = await Editor.Message.request("asset-db", "query-asset-dependencies", dbUrl);
if (Array.isArray(deps)) {
dependencyCount = deps.length;
}
} catch {
}
let extendsTypes;
const ext = info.extends;
if (Array.isArray(ext)) {
const arr = ext.filter((x) => typeof x === "string");
if (arr.length > 0) {
extendsTypes = arr;
}
}
const instantiation = typeof info.instantiation === "string" ? info.instantiation : void 0;
const result = {
ok: true,
prefabPath: pathNorm.rel,
name,
displayName,
type,
importer,
imported,
invalid
};
if (subAssetCount !== void 0) {
result.subAssetCount = subAssetCount;
}
if (dependencyCount !== void 0) {
result.dependencyCount = dependencyCount;
}
if (extendsTypes) {
result.extendsTypes = extendsTypes;
}
if (instantiation) {
result.instantiation = instantiation;
}
return { op: "project_get_prefab_info_ok", id, v: SUPPORTED_PROTOCOL_VERSION, result };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { op: "project_get_prefab_info_err", id, v: SUPPORTED_PROTOCOL_VERSION, message };
}
}
function isRecord(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function safeSend(ws, obj) {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify(obj));
}
}
function terminateServerClients(server) {
for (const client of server.clients) {
try {
client.close(1001, "server stopping");
client.terminate();
} catch {
}
}
}
function startWebSocketServer() {
if (wss) {
return Promise.resolve();
}
if (startServerPromise) {
return startServerPromise;
}
if (serverStopping || stopServerPromise) {
return Promise.reject(new Error("WebSocket server is still stopping."));
}
const starting = new Promise((resolve2, reject) => {
removeDiscovery();
serverToken = randomToken();
let settled = false;
const failStart = (error) => {
if (settled) {
return;
}
settled = true;
if (pendingWss === server) {
pendingWss = null;
}
if (wss === server) {
wss = null;
}
if (cancelPendingStart === failStart) {
cancelPendingStart = null;
}
terminateServerClients(server);
removeDiscovery();
serverToken = "";
const finish = () => reject(error);
try {
server.close(finish);
} catch {
finish();
}
};
const server = new import_ws.WebSocketServer({ host: "127.0.0.1", port: 0 }, () => {
if (serverStopping) {
failStart(new Error("WebSocket server start was canceled by stop."));
return;
}
try {
const addr = server.address();
if (!addr || typeof addr === "string") {
throw new Error("WebSocketServer: invalid address");
}
writeDiscovery(addr.port, serverToken);
wss = server;
pendingWss = null;
console.info(`[cocos-mcp] WebSocket listening 127.0.0.1:${addr.port}, discovery \u2192 ${getDiscoveryPath()}`);
cancelPendingStart = null;
settled = true;
resolve2();
} catch (error) {
failStart(error instanceof Error ? error : new Error(String(error)));
}
});
pendingWss = server;
cancelPendingStart = failStart;
server.on("error", (err) => {
failStart(err);
});
server.on("connection", (ws, req) => {
const parsed = url.parse(req.url || "", true);
const token = parsed.query.token;
const tokenStr = Array.isArray(token) ? token[0] : token;
if (tokenStr !== serverToken) {
ws.close(4001, "unauthorized");
return;
}
activeWsClients.add(ws);
panelLog("info", `client connected (${activeWsClients.size} online)`);
ws.on("close", () => {
activeWsClients.delete(ws);
panelLog("info", `client disconnected (${activeWsClients.size} online)`);
});
ws.on("message", (raw) => {
if (serverStopping) {
ws.close(1012, "server stopping");
return;
}
wsRequestQueue = wsRequestQueue.then(async () => {
let parsedMsg;
try {
parsedMsg = JSON.parse(raw.toString());
} catch {
return;
}
if (!isRecord(parsedMsg)) {
return;
}
const op = parsedMsg.op;
const id = parsedMsg.id;
if (typeof id !== "string" || typeof op !== "string") {
return;
}
const verRaw = parsedMsg.v;
if (verRaw !== void 0 && verRaw !== null && Number(verRaw) !== SUPPORTED_PROTOCOL_VERSION) {
safeSend(ws, {
op: "scene_protocol_err",
id,
v: SUPPORTED_PROTOCOL_VERSION,
message: `Unsupported protocol version: ${String(verRaw)}; expected ${SUPPORTED_PROTOCOL_VERSION}`
});
return;
}
const env = parsedMsg;
if (env.op === "scene_ping") {
safeSend(ws, await handleScenePing(env.id));
return;
}
if (env.op === "project_log_tail") {
const pl = isRecord(parsedMsg.payload) ? parsedMsg.payload : {};
const maxLines = Math.min(2e3, Math.max(1, Number(pl.maxLines) || 200));
const rel = safeRelativeProjectPath(pl.relativePath, DEFAULT_LOG_RELATIVE);
const out = readProjectLogTail(rel, maxLines);
if (!out.ok) {
safeSend(ws, {
op: "project_log_tail_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: out.message
});
} else {
safeSend(ws, {
op: "project_log_tail_ok",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
result: out.text
});
}
return;
}
if (env.op === "project_get_info") {
safeSend(ws, handleProjectGetInfo(env.id));
return;
}
if (env.op === "project_list_assets") {
safeSend(ws, handleProjectListAssets(env.id, parsedMsg.payload));
return;
}
if (env.op === "project_get_prefab_info") {
safeSend(ws, await handleProjectGetPrefabInfo(env.id, parsedMsg.payload));
return;
}
if (env.op === "scene_get_hierarchy") {
const pl = env.payload;
const hierarchyArg = pl !== void 0 && pl !== null && isRecord(pl) ? pl : {};
let nextSceneUuid = "";
try {
const meta = await executeSceneMethod("getCurrentSceneMeta", []);
nextSceneUuid = inodeRecord(meta) && meta.ok === true && meta.hasScene === true && typeof meta.sceneAssetUuid === "string" ? meta.sceneAssetUuid : "";
} catch {
}
if (mcpUndoSceneUuid && nextSceneUuid && mcpUndoSceneUuid !== nextSceneUuid) {
await clearMcpUndoStack();
}
if (nextSceneUuid) {
mcpUndoSceneUuid = nextSceneUuid;
}
safeSend(ws, await forwardToScene(env.id, "getHierarchy", [hierarchyArg]));
return;
}
if (env.op === "scene_get_current_meta") {
safeSend(ws, await handleGetCurrentSceneMeta(env.id));
return;
}
if (env.op === "scene_search_nodes") {
const pl = env.payload;
const searchArg = pl !== void 0 && pl !== null && isRecord(pl) ? pl : {};
safeSend(ws, await forwardToScene(env.id, "searchNodes", [searchArg]));
return;
}
if (env.op === "scene_inspect_node") {
const pl = env.payload;
const inspectArg = pl !== void 0 && pl !== null && isRecord(pl) ? pl : {};
safeSend(ws, await forwardToScene(env.id, "inspectNode", [inspectArg]));
return;
}
if (env.op === "scene_set_node_active") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_set_node_active_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer nid and boolean active"
});
return;
}
const nid = pl.nid;
const active = pl.active;
if (typeof nid !== "number" || !Number.isInteger(nid) || typeof active !== "boolean") {
safeSend(ws, {
op: "scene_set_node_active_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid and boolean active"
});
return;
}
safeSend(ws, await handleSetNodeActive(env.id, { nid, active }));
return;
}
if (env.op === "scene_set_node_transform") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_set_node_transform_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with nid and at least one of position, rotationEuler, scale"
});
return;
}
const nid = pl.nid;
if (typeof nid !== "number" || !Number.isInteger(nid)) {
safeSend(ws, {
op: "scene_set_node_transform_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid"
});
return;
}
const position = parseFiniteVec3(pl.position);
const rotationEuler = parseFiniteVec3(pl.rotationEuler);
const scale = parseFiniteVec3(pl.scale);
if (!position && !rotationEuler && !scale) {
safeSend(ws, {
op: "scene_set_node_transform_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "provide at least one of position, rotationEuler, scale as { x, y, z } finite numbers"
});
return;
}
const payload = { nid };
if (position) {
payload.position = position;
}
if (rotationEuler) {
payload.rotationEuler = rotationEuler;
}
if (scale) {
payload.scale = scale;
}
safeSend(ws, await handleSetNodeTransform(env.id, payload));
return;
}
if (env.op === "scene_set_node_name") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_set_node_name_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer nid and string name"
});
return;
}
const nid = pl.nid;
const name = pl.name;
if (typeof nid !== "number" || !Number.isInteger(nid) || typeof name !== "string") {
safeSend(ws, {
op: "scene_set_node_name_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid and string name"
});
return;
}
safeSend(ws, await handleSetNodeName(env.id, { nid, name }));
return;
}
if (env.op === "scene_create_node") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_create_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer parentNid and string name"
});
return;
}
const parentNid = pl.parentNid;
const cname = pl.name;
if (typeof parentNid !== "number" || !Number.isInteger(parentNid) || typeof cname !== "string") {
safeSend(ws, {
op: "scene_create_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer parentNid and string name"
});
return;
}
safeSend(ws, await handleCreateNode(env.id, { parentNid, name: cname }));
return;
}
if (env.op === "scene_delete_node") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_delete_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer nid and confirm: true"
});
return;
}
const nid = pl.nid;
const confirm = pl.confirm;
const cascade = pl.cascade === true;
if (typeof nid !== "number" || !Number.isInteger(nid) || confirm !== true) {
safeSend(ws, {
op: "scene_delete_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid and confirm: true (literal)"
});
return;
}
const delPayload = { nid, confirm: true };
if (cascade) {
delPayload.cascade = true;
}
safeSend(ws, await handleDeleteNode(env.id, delPayload));
return;
}
if (env.op === "scene_reparent_node") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_reparent_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer childNid and parentNid"
});
return;
}
const childNid = pl.childNid;
const parentNid = pl.parentNid;
if (typeof childNid !== "number" || !Number.isInteger(childNid)) {
safeSend(ws, {
op: "scene_reparent_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer childNid"
});
return;
}
if (typeof parentNid !== "number" || !Number.isInteger(parentNid)) {
safeSend(ws, {
op: "scene_reparent_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer parentNid"
});
return;
}
const rp = { childNid, parentNid };
if (pl.keepWorldTransform === false) {
rp.keepWorldTransform = false;
}
safeSend(ws, await handleReparentNode(env.id, rp));
return;
}
if (env.op === "scene_set_node_sibling_index") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_set_node_sibling_index_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer nid and non-negative integer index"
});
return;
}
const nid = pl.nid;
const index = pl.index;
if (typeof nid !== "number" || !Number.isInteger(nid) || typeof index !== "number" || !Number.isInteger(index) || index < 0) {
safeSend(ws, {
op: "scene_set_node_sibling_index_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid and non-negative integer index"
});
return;
}
safeSend(ws, await handleSetNodeSiblingIndex(env.id, { nid, index }));
return;
}
if (env.op === "scene_instantiate_prefab") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_instantiate_prefab_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer parentNid and string prefabPath"
});
return;
}
const parentNid = pl.parentNid;
const prefabPath = pl.prefabPath;
if (typeof parentNid !== "number" || !Number.isInteger(parentNid)) {
safeSend(ws, {
op: "scene_instantiate_prefab_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer parentNid"
});
return;
}
if (typeof prefabPath !== "string") {
safeSend(ws, {
op: "scene_instantiate_prefab_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires string prefabPath"
});
return;
}
safeSend(ws, await handleInstantiatePrefab(env.id, { parentNid, prefabPath }));
return;
}
if (env.op === "scene_add_component") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_add_component_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer nid and string componentType"
});
return;
}
const nid = pl.nid;
const componentType = pl.componentType;
if (typeof nid !== "number" || !Number.isInteger(nid) || typeof componentType !== "string") {
safeSend(ws, {
op: "scene_add_component_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid and string componentType"
});
return;
}
safeSend(ws, await handleAddComponent(env.id, { nid, componentType }));
return;
}
if (env.op === "scene_remove_component") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_remove_component_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with integer nid and componentIndex (and optional expectedComponentType)"
});
return;
}
const nid = pl.nid;
const componentIndex = pl.componentIndex;
const expectedComponentType = typeof pl.expectedComponentType === "string" ? pl.expectedComponentType : void 0;
if (typeof nid !== "number" || !Number.isInteger(nid) || typeof componentIndex !== "number" || !Number.isInteger(componentIndex) || componentIndex < 0) {
safeSend(ws, {
op: "scene_remove_component_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid and non-negative integer componentIndex"
});
return;
}
safeSend(ws, await handleRemoveComponent(env.id, { nid, componentIndex, expectedComponentType }));
return;
}
if (env.op === "scene_duplicate_node") {
const pl = parsedMsg.payload;
if (!isRecord(pl) || typeof pl.nid !== "number" || !Number.isInteger(pl.nid)) {
safeSend(ws, {
op: "scene_duplicate_node_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid"
});
return;
}
safeSend(ws, await handleDuplicateNode(env.id, { nid: pl.nid }));
return;
}
if (env.op === "scene_set_component_property") {
const pl = parsedMsg.payload;
if (!isRecord(pl)) {
safeSend(ws, {
op: "scene_set_component_property_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload must be an object with nid, componentIndex, property, value"
});
return;
}
const nid = pl.nid;
const componentIndex = pl.componentIndex;
const property = pl.property;
if (typeof nid !== "number" || !Number.isInteger(nid)) {
safeSend(ws, {
op: "scene_set_component_property_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer nid"
});
return;
}
if (typeof componentIndex !== "number" || !Number.isInteger(componentIndex) || componentIndex < 0) {
safeSend(ws, {
op: "scene_set_component_property_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires non-negative integer componentIndex"
});
return;
}
if (typeof property !== "string" || !property.trim()) {
safeSend(ws, {
op: "scene_set_component_property_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires non-empty string property"
});
return;
}
if (!("value" in pl)) {
safeSend(ws, {
op: "scene_set_component_property_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires value"
});
return;
}
safeSend(
ws,
await handleSetComponentProperty(env.id, {
nid,
componentIndex,
property,
value: pl.value
})
);
return;
}
if (env.op === "scene_apply_ui_spec") {
const pl = parsedMsg.payload;
if (!isRecord(pl) || typeof pl.parentNid !== "number" || !Number.isInteger(pl.parentNid) || !Array.isArray(pl.nodes)) {
safeSend(ws, {
op: "scene_apply_ui_spec_err",
id: env.id,
v: SUPPORTED_PROTOCOL_VERSION,
message: "payload requires integer parentNid and a nodes array"
});
return;
}
safeSend(ws, await handleApplyUiSpec(env.id, pl));
return;
}
if (env.op === "editor_undo") {
safeSend(ws, await handleEditorUndo(env.id));
return;
}
if (env.op === "scene_save") {
safeSend(ws, await handleSaveScene(env.id));
return;
}
if (env.op === "mcp_undo_last") {
safeSend(ws, await handleMcpUndoLast(env.id));
return;
}
}).catch((error) => {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
console.error("[cocos-mcp] queued request failed", message);
});
});
});
});
startServerPromise = starting;
const clearStarting = () => {
if (startServerPromise === starting) {
startServerPromise = null;
cancelPendingStart = null;
}
if (pendingWss && !wss) {
pendingWss = null;
}
};
starting.then(clearStarting, clearStarting);
return starting;
}
function stopWebSocketServer() {
if (stopServerPromise) {
return stopServerPromise;
}
stopServerPromise = (async () => {
serverStopping = true;
const pendingStart = startServerPromise;
if (pendingStart) {
cancelPendingStart?.(new Error("WebSocket server start was canceled by stop."));
await pendingStart.catch(() => {
});
}
const server = wss;
wss = null;
if (server) {
terminateServerClients(server);
}
const serverClosed = server ? new Promise((resolve2) => {
server.close(() => resolve2());
}) : Promise.resolve();
await wsRequestQueue;
await serverClosed;
await clearMcpUndoStack();
activeWsClients.clear();
removeDiscovery();
serverToken = "";
wsRequestQueue = Promise.resolve();
})().finally(() => {
serverStopping = false;
stopServerPromise = null;
});
return stopServerPromise;
}
async function load() {
try {
await startWebSocketServer();
} catch (err) {
console.error("[cocos-mcp] failed to start WebSocket server", err);
throw err;
}
}
async function unload() {
try {
await stopWebSocketServer();
} catch (err) {
console.error("[cocos-mcp] failed to stop WebSocket server", err);
throw err;
}
}
function getRuntimeStatus() {
if (!wss) {
return { running: false, port: null, url: null, connections: 0 };
}
const addr = wss.address();
const port = addr && typeof addr !== "string" ? addr.port : null;
return {
running: true,
port,
url: port ? `http://127.0.0.1:${port}/mcp` : null,
connections: activeWsClients.size
};
}
function clientConfigured(spec, serverName, bridge) {
if (spec.mode === "json") {
return hasServerInJson(spec.configPath, serverName, bridge);
}
return hasServerInCodexToml(spec.configPath, serverName, bridge);
}
function addServerToClient(spec, serverName, bridge) {
if (spec.mode === "json") {
return writeJsonConfig(spec.configPath, serverName, bridge);
}
return writeCodexToml(spec.configPath, serverName, bridge);
}
var latestSelfTest = null;
async function runMcpSelfTest(serverName = "cocos-creator") {
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
const checks = [];
const bridgeCfg = getBridgeLaunchConfig();
if (!bridgeCfg.ok) {
checks.push({ id: "node", ok: false, message: bridgeCfg.message });
checks.push({ id: "bridge", ok: false, message: bridgeCfg.message });
} else {
const bridge = bridgeCfg.value;
const nodeProbe = (0, import_child_process.spawnSync)(bridge.nodeBin, ["--version"], {
encoding: "utf-8",
timeout: 5e3,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"]
});
checks.push({
id: "node",
ok: !nodeProbe.error && nodeProbe.status === 0,
message: nodeProbe.error ? nodeProbe.error.message : `${bridge.nodeBin} ${(nodeProbe.stdout ?? "").trim() || "(version unavailable)"}`
});
const bridgeProbe = (0, import_child_process.spawnSync)(
bridge.nodeBin,
[bridge.bridgeCliPath, "--project", bridge.projectRoot, "--self-test"],
{
encoding: "utf-8",
timeout: 8e3,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"]
}
);
const bridgeOutput = `${bridgeProbe.stdout ?? ""}`.trim();
checks.push({
id: "bridge",
ok: !bridgeProbe.error && bridgeProbe.status === 0,
message: bridgeProbe.error ? bridgeProbe.error.message : bridgeOutput || `${bridgeProbe.stderr ?? ""}`.trim() || "bridge self-test returned no output"
});
}
const server = getRuntimeStatus();
checks.push({
id: "websocket",
ok: server.running && server.port !== null,
message: server.running ? `listening on 127.0.0.1:${server.port} (${server.connections} client connection(s))` : "MCP WebSocket server is stopped"
});
try {
const ping = await executeSceneMethod("ping", []);
checks.push({
id: "scene",
ok: ping?.status === "ok" && ping?.layer === "scene",
message: ping?.status === "ok" ? "Creator Scene process responded" : "unexpected Scene ping response"
});
} catch (error) {
checks.push({
id: "scene",
ok: false,
message: error instanceof Error ? error.message : String(error)
});
}
const configuredCount = bridgeCfg.ok ? getClientSpecs().filter((spec) => clientConfigured(spec, serverName, bridgeCfg.value)).length : 0;
checks.push({
id: "clients",
ok: configuredCount > 0,
message: configuredCount > 0 ? `${configuredCount} MCP client configuration(s) verified` : "No verified MCP client configuration; run Auto Config"
});
latestSelfTest = { ok: checks.every((check) => check.ok), checkedAt, checks };
for (const check of checks) {
panelLog(check.ok ? "info" : "warn", `[self-test:${check.id}] ${check.message}`);
}
return latestSelfTest;
}
var methods = {
openPanel() {
Editor.Panel.open("cocos-mcp");
},
openUserGuide() {
const guidePath = path3.resolve(__dirname, "..", "QUICK_START.md");
const out = fs3.existsSync(guidePath) ? openFileInSystem(guidePath) : { ok: false, message: `Quick Start not found: ${guidePath}` };
panelLog(out.ok ? "info" : "warn", `[quick-start] ${out.message}`);
return out;
},
async startMcpServer() {
if (wss) {
return;
}
await startWebSocketServer();
panelLog("info", "server started from panel");
},
async stopMcpServer() {
await stopWebSocketServer();
panelLog("info", "server stopped from panel");
},
getMcpServerStatus() {
return getRuntimeStatus();
},
async runMcpSelfTest(serverName = "cocos-creator") {
return runMcpSelfTest(serverName);
},
getMcpPanelState(serverName = "cocos-creator") {
const server = getRuntimeStatus();
const bridgeCfg = getBridgeLaunchConfig();
const bridge = bridgeCfg.ok ? bridgeCfg.value : null;
const clients = getClientSpecs().map((spec) => ({
clientType: spec.clientType,
name: spec.name,
configPath: spec.configPath,
configured: bridge ? clientConfigured(spec, serverName, bridge) : false,
exists: fs3.existsSync(spec.configPath),
mode: spec.mode
}));
const launch = bridge ? bridge.args.length > 0 ? `${bridge.command} ${bridge.args.map((a) => `"${a}"`).join(" ")}` : `"${bridge.command}"` : 'node "<extension>/dist/bridge-cli.js" --project "<Cocos project>"';
const cliCommands = {
claude: `claude mcp add --scope user ${serverName} -- ${launch}`,
gemini: `gemini mcp add --scope user ${serverName} -- ${launch}`
};
if (!bridgeCfg.ok) {
panelLog("warn", bridgeCfg.message);
}
return {
server,
serverName,
clients,
cliCommands,
selfTest: latestSelfTest,
logs: [...panelLogs]
};
},
addMcpToClient(clientType, serverName = "cocos-creator") {
const spec = getClientSpecs().find((c) => c.clientType === clientType);
if (!spec) {
return { ok: false, message: `unknown clientType: ${clientType}` };
}
const bridgeCfg = getBridgeLaunchConfig();
if (!bridgeCfg.ok) {
panelLog("error", `[auto-config:${spec.clientType}] ${bridgeCfg.message}`);
return { ok: false, message: bridgeCfg.message };
}
const out = addServerToClient(spec, serverName, bridgeCfg.value);
panelLog(out.ok ? "info" : "error", `[auto-config:${spec.clientType}] ${out.message}`);
return out;
},
addMcpToAllClients(serverName = "cocos-creator") {
const specs = getClientSpecs();
const out = {};
let allOk = true;
const bridgeCfg = getBridgeLaunchConfig();
if (!bridgeCfg.ok) {
panelLog("error", `[auto-config:all] ${bridgeCfg.message}`);
for (const spec of specs) {
out[spec.clientType] = bridgeCfg.message;
}
return { ok: false, results: out };
}
for (const spec of specs) {
const r = addServerToClient(spec, serverName, bridgeCfg.value);
out[spec.clientType] = r.message;
if (!r.ok) {
allOk = false;
}
panelLog(r.ok ? "info" : "error", `[auto-config:${spec.clientType}] ${r.message}`);
}
return { ok: allOk, results: out };
},
openClientConfig(clientType) {
const spec = getClientSpecs().find((c) => c.clientType === clientType);
if (!spec) {
return { ok: false, message: `unknown clientType: ${clientType}` };
}
ensureParentDir(spec.configPath);
if (!fs3.existsSync(spec.configPath)) {
fs3.writeFileSync(spec.configPath, spec.mode === "json" ? '{\n "mcpServers": {}\n}\n' : "", "utf-8");
}
const out = openFileInSystem(spec.configPath);
panelLog(out.ok ? "info" : "warn", `[open-config:${spec.clientType}] ${out.message}`);
return out;
},
clearPanelLogs() {
panelLogs.length = 0;
return { ok: true };
}
};
var cocosCjsExports = typeof exports === "object" && exports ? exports : null;
if (cocosCjsExports) {
cocosCjsExports.load = load;
cocosCjsExports.unload = unload;
cocosCjsExports.methods = methods;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
load,
methods,
unload
});