UNPKG

@akiojin/unity-mcp-server

Version:

MCP server and Unity Editor bridge — enables AI assistants to control Unity for AI-assisted workflows

134 lines (126 loc) 4.73 kB
import { LspProcessManager } from './LspProcessManager.js'; import { config, logger } from '../core/config.js'; export class LspRpcClient { constructor(projectRoot = null) { this.mgr = new LspProcessManager(); this.proc = null; this.seq = 1; this.pending = new Map(); this.buf = Buffer.alloc(0); this.initialized = false; this.projectRoot = projectRoot; this.boundOnData = null; } async ensure() { if (this.proc && !this.proc.killed) return this.proc; this.proc = await this.mgr.ensureStarted(); // Attach data handler once per process if (this.boundOnData) { try { this.proc.stdout.off('data', this.boundOnData); } catch {} } this.boundOnData = (chunk) => this.onData(chunk); this.proc.stdout.on('data', this.boundOnData); // On process close: reject all pending and reset state this.proc.on('close', () => { for (const [id, p] of Array.from(this.pending.entries())) { try { p.reject(new Error('LSP process exited')); } catch {} this.pending.delete(id); } this.initialized = false; this.proc = null; }); if (!this.initialized) await this.initialize(); return this.proc; } onData(chunk) { this.buf = Buffer.concat([this.buf, Buffer.from(chunk)]); while (true) { const headerEnd = this.buf.indexOf('\r\n\r\n'); if (headerEnd < 0) break; const header = this.buf.slice(0, headerEnd).toString('utf8'); const m = header.match(/Content-Length:\s*(\d+)/i); const len = m ? parseInt(m[1], 10) : 0; const total = headerEnd + 4 + len; if (this.buf.length < total) break; const jsonBuf = this.buf.slice(headerEnd + 4, total); this.buf = this.buf.slice(total); try { const msg = JSON.parse(jsonBuf.toString('utf8')); if (msg.id && this.pending.has(msg.id)) { this.pending.get(msg.id).resolve(msg); this.pending.delete(msg.id); } } catch { /* ignore */ } } } writeMessage(obj) { const json = JSON.stringify(obj); const payload = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`; this.proc.stdin.write(payload, 'utf8'); } async initialize() { await this.ensure(); const id = this.seq++; const req = { jsonrpc: '2.0', id, method: 'initialize', params: { processId: process.pid, rootUri: this.projectRoot ? ('file://' + String(this.projectRoot).replace(/\\/g, '/')) : null, capabilities: {}, workspaceFolders: null, } }; const timeoutMs = Math.max(5000, Math.min(60000, config.lsp?.requestTimeoutMs || 60000)); const p = new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`initialize timed out after ${timeoutMs} ms`)); } }, timeoutMs); }); this.writeMessage(req); const resp = await p; // ignore result contents for stub // send initialized notification this.writeMessage({ jsonrpc: '2.0', method: 'initialized', params: {} }); this.initialized = true; return resp; } async request(method, params) { return await this.#requestWithRetry(method, params, 1); } async #requestWithRetry(method, params, attempt) { await this.ensure(); const id = this.seq++; const timeoutMs = Math.max(1000, Math.min(300000, config.lsp?.requestTimeoutMs || 60000)); const p = new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`${method} timed out after ${timeoutMs} ms`)); } }, timeoutMs); }); try { this.writeMessage({ jsonrpc: '2.0', id, method, params }); return await p; } catch (e) { const msg = String(e && e.message || e); const recoverable = /timed out|LSP process exited/i.test(msg); if (recoverable && attempt === 1) { // Auto-reinit and retry once try { await this.mgr.stop(0); } catch {} this.proc = null; this.initialized = false; this.buf = Buffer.alloc(0); logger.warn(`[csharp-lsp] recoverable error on ${method}: ${msg}. Retrying once...`); return await this.#requestWithRetry(method, params, attempt + 1); } // Standardize error message const hint = recoverable ? 'The server was restarted. Try again if the issue persists.' : 'Check request parameters or increase lsp.requestTimeoutMs.'; throw new Error(`[${method}] failed: ${msg}. ${hint}`); } } }