"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 "/dist/bridge-cli.js" --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 });