【迁移】迁移Codex Unity MCP工具到UnityAI

This commit is contained in:
Shiliang
2026-07-03 15:04:38 +08:00
parent 5e5b9f8c80
commit b213e39b28
7 changed files with 1266 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const DEFAULT_BRIDGE_URL = "http://127.0.0.1:17777";
const DEFAULT_TIMEOUT_MS = 30000;
const LONG_TIMEOUT_MS = 120000;
const bridgeUrl = (process.env.UNITY_MCP_BRIDGE_URL || DEFAULT_BRIDGE_URL).replace(/\/+$/, "");
const server = new McpServer({
name: "crystalbattle-unity",
version: "0.1.0"
});
server.registerTool(
"unity_health",
{
title: "Unity health",
description: "Check whether the CrystalBattle Unity Editor bridge is online.",
inputSchema: {}
},
async () => textResult(await getHealth())
);
server.registerTool(
"unity_refresh_assets",
{
title: "Refresh Unity assets",
description: "Run AssetDatabase.Refresh in the Unity Editor.",
inputSchema: {}
},
async () => textResult(await callUnity("refresh_assets", {}, DEFAULT_TIMEOUT_MS))
);
server.registerTool(
"unity_execute_menu_item",
{
title: "Execute Unity menu item",
description: "Execute a whitelisted Unity Editor menu item.",
inputSchema: {
menuItem: z.string().min(1).describe("Whitelisted Unity menu item path, for example Assets/Refresh.")
}
},
async ({ menuItem }) => textResult(await callUnity("execute_menu_item", { menuItem }, DEFAULT_TIMEOUT_MS))
);
server.registerTool(
"unity_get_console_logs",
{
title: "Get Unity console logs",
description: "Return recent logs captured by the Unity Editor bridge.",
inputSchema: {
limit: z.number().int().min(1).max(200).optional().describe("Maximum number of log entries to return.")
}
},
async ({ limit = 50 }) => textResult(await callUnity("get_console_logs", { limit }, DEFAULT_TIMEOUT_MS))
);
server.registerTool(
"unity_generate_ui_prefab",
{
title: "Generate UI prefab",
description: "Generate a Unity UI prefab from a JSON layout file.",
inputSchema: {
jsonPath: z.string().min(1).describe("JSON layout path. Absolute paths and Unity-project-relative paths are supported."),
prefabPath: z.string().min(1).optional().describe("Optional Unity asset path for the generated prefab.")
}
},
async ({ jsonPath, prefabPath }) =>
textResult(await callUnity("generate_ui_prefab", { jsonPath, prefabPath }, LONG_TIMEOUT_MS))
);
server.registerTool(
"unity_export_prefab_to_json",
{
title: "Export prefab to JSON",
description: "Export a Unity UI prefab to a JSON layout file.",
inputSchema: {
prefabPath: z.string().min(1).describe("Unity asset path of the prefab to export."),
jsonPath: z.string().min(1).optional().describe("Optional output JSON path.")
}
},
async ({ prefabPath, jsonPath }) =>
textResult(await callUnity("export_prefab_to_json", { prefabPath, jsonPath }, LONG_TIMEOUT_MS))
);
async function getHealth() {
const response = await request(`${bridgeUrl}/health`, {
method: "GET",
timeoutMs: DEFAULT_TIMEOUT_MS
});
return response;
}
async function callUnity(command, args, timeoutMs) {
return request(`${bridgeUrl}/command`, {
method: "POST",
timeoutMs,
body: {
id: `${command}-${Date.now()}`,
command,
args: stripUndefined(args)
}
});
}
async function request(url, options) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
try {
const response = await fetch(url, {
method: options.method,
signal: controller.signal,
headers: options.body ? { "content-type": "application/json" } : undefined,
body: options.body ? JSON.stringify(options.body) : undefined
});
const text = await response.text();
let payload;
try {
payload = text ? JSON.parse(text) : {};
} catch {
payload = { ok: false, message: text || "Unity bridge returned a non-JSON response." };
}
if (!response.ok && payload.ok !== false) {
payload.ok = false;
payload.message = payload.message || `Unity bridge returned HTTP ${response.status}.`;
}
return payload;
} catch (error) {
const reason = error.name === "AbortError"
? `Timed out after ${options.timeoutMs} ms.`
: error.message;
return {
ok: false,
message: `Unity Editor bridge is not reachable at ${bridgeUrl}. ${reason}`,
data: {},
logs: []
};
} finally {
clearTimeout(timeout);
}
}
function stripUndefined(value) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}
function textResult(payload) {
return {
content: [
{
type: "text",
text: JSON.stringify(payload, null, 2)
}
]
};
}
const transport = new StdioServerTransport();
await server.connect(transport);