【迁移】迁移Codex Unity MCP工具到UnityAI
This commit is contained in:
165
Tools/CodexUnityMcp/src/server.mjs
Normal file
165
Tools/CodexUnityMcp/src/server.mjs
Normal 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);
|
||||
Reference in New Issue
Block a user