@akiojin/unity-editor-mcp
Version:
MCP server for Unity Editor integration - enables AI assistants to control Unity Editor
117 lines (112 loc) • 4.04 kB
JavaScript
import { spawn } from 'child_process';
import { logger } from '../../core/config.js';
import { RoslynCliUtils } from './RoslynCliUtils.js';
let serverProc = null;
let starting = null; // Promise guard to avoid duplicate spawns
let nextId = 1;
const pending = new Map();
// FIFO queue of last payloads emitted by roslyn-cli (serve writes result first, then an id-envelope)
const resultQueue = [];
const roslynUtils = new RoslynCliUtils(null);
async function startServer() {
// Use built self-contained binary (auto-build once if missing)
const cli = await roslynUtils.getCliPath();
// Strict: require solution at serve start
let solArgs = [];
try {
solArgs = await roslynUtils.getSolutionOrProjectArgs(); // e.g., ['--solution', '/abs/path/UnityProject.sln']
} catch (e) {
logger.error(`[roslyn-cli serve] solution resolve failed: ${e.message}`);
throw e;
}
const proc = spawn(cli, ['serve', ...solArgs], { stdio: ['pipe', 'pipe', 'pipe'] });
proc.on('error', (e) => logger.error(`[roslyn-cli serve] error: ${e.message}`));
proc.stderr.on('data', d => logger.debug(`[roslyn-cli serve] ${d.toString().trim()}`));
let buffer = '';
proc.stdout.on('data', chunk => {
buffer += chunk.toString();
let idx;
while ((idx = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 1);
if (!line) continue;
try {
const json = JSON.parse(line);
const id = json.id;
if (id && pending.has(id)) {
// Pair the latest result payload (if any) with this id envelope
const payload = resultQueue.length ? resultQueue.shift() : null;
const resolved = payload ? Object.assign({ id }, payload) : json;
pending.get(id).resolve(resolved);
pending.delete(id);
} else if (!id) {
// This is a raw payload from the command; enqueue for pairing
resultQueue.push(json);
}
} catch { /* ignore stray output */ }
}
});
proc.on('close', (code) => {
logger.warn(`[roslyn-cli serve] exited with code ${code}`);
serverProc = null;
// Reject all pending requests to unblock callers
for (const [id, p] of Array.from(pending.entries())) {
p.reject(new Error('roslyn-cli serve exited'));
pending.delete(id);
}
// Clear queued payloads
resultQueue.length = 0;
});
return proc;
}
async function ensureServer() {
if (serverProc && !serverProc.killed) return serverProc;
if (starting) return starting;
starting = (async () => {
try {
serverProc = await startServer();
return serverProc;
} catch (e) {
logger.error(`[roslyn-cli serve] failed to start: ${e.message}`);
serverProc = null;
throw e; // 明示的に失敗を伝播(フォールバックなし)
} finally {
starting = null;
}
})();
return starting;
}
export async function sendServe(cmd, args = []) {
await ensureServer();
if (!serverProc) {
throw new Error('roslyn-cli serve unavailable (server process not running)');
}
const attempt = async (retry) => {
const id = String(nextId++);
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
try {
serverProc.stdin.write(JSON.stringify({ id, cmd, args }) + '\n');
} catch (e) {
pending.delete(id);
return reject(e);
}
// Heavy Roslyn ops on large projects may take >60s. Use generous timeout.
const to = setTimeout(() => {
if (pending.has(id)) {
pending.get(id).reject(new Error('roslyn-cli serve timeout'));
pending.delete(id);
}
}, 300000);
// bridge resolution to clear timeout
const original = pending.get(id);
if (original) {
pending.set(id, {
resolve: (v) => { clearTimeout(to); original.resolve(v); },
reject: (err) => { clearTimeout(to); original.reject(err); }
});
}
});
};
return await attempt(false);
}