UNPKG

@akiojin/unity-mcp-server

Version:

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

141 lines (124 loc) 4.85 kB
import fs from 'fs'; import path from 'path'; import { logger } from '../core/config.js'; import { WORKSPACE_ROOT } from '../core/config.js'; export class CSharpLspUtils { constructor() {} detectRid() { if (process.platform === 'win32') return process.arch === 'arm64' ? 'win-arm64' : 'win-x64'; if (process.platform === 'darwin') return process.arch === 'arm64' ? 'osx-arm64' : 'osx-x64'; return process.arch === 'arm64' ? 'linux-arm64' : 'linux-x64'; } getDesiredVersion() { try { const pkg = JSON.parse(fs.readFileSync(path.resolve('mcp-server/package.json'), 'utf8')); return pkg.version; } catch { return null; } } getLocalPath(rid) { const root = WORKSPACE_ROOT || process.cwd(); const exe = process.platform === 'win32' ? 'server.exe' : 'server'; return path.resolve(root, '.unity', 'tools', 'csharp-lsp', rid, exe); } getVersionMarkerPath(rid) { const bin = this.getLocalPath(rid); return path.resolve(path.dirname(bin), 'VERSION'); } readLocalVersion(rid) { try { const m = this.getVersionMarkerPath(rid); if (fs.existsSync(m)) return fs.readFileSync(m, 'utf8').trim(); } catch {} return null; } writeLocalVersion(rid, version) { try { const m = this.getVersionMarkerPath(rid); fs.writeFileSync(m, String(version || '').trim() + '\n', 'utf8'); } catch {} } async ensureLocal(rid) { const p = this.getLocalPath(rid); const desired = this.getDesiredVersion(); if (!desired) throw new Error('mcp-server version not found; cannot resolve LSP tag'); const current = this.readLocalVersion(rid); if (fs.existsSync(p) && current === desired) return p; await this.autoDownload(rid, desired); if (!fs.existsSync(p)) throw new Error('csharp-lsp binary not found after download'); this.writeLocalVersion(rid, desired); return p; } async autoDownload(rid, version) { const repo = process.env.GITHUB_REPOSITORY || 'akiojin/unity-mcp-server'; const tag = `v${version}`; const manifestUrl = `https://github.com/${repo}/releases/download/${tag}/csharp-lsp-manifest.json`; const manifest = await this.fetchJson(manifestUrl); const entry = manifest?.assets?.[rid]; if (!entry?.url || !entry?.sha256) throw new Error(`manifest missing entry for ${rid}`); const dest = this.getLocalPath(rid); fs.mkdirSync(path.dirname(dest), { recursive: true }); const tmp = dest + '.download'; await this.downloadTo(entry.url, tmp); const actual = await this.sha256File(tmp); if (String(actual).toLowerCase() !== String(entry.sha256).toLowerCase()) { try { fs.unlinkSync(tmp); } catch {} throw new Error('checksum mismatch for csharp-lsp asset'); } // atomic replace try { fs.renameSync(tmp, dest); } catch (e) { // Windows may need removal before rename try { fs.unlinkSync(dest); } catch {} fs.renameSync(tmp, dest); } try { if (process.platform !== 'win32') fs.chmodSync(dest, 0o755); } catch {} logger.info(`[csharp-lsp] downloaded: ${path.basename(dest)} @ ${path.dirname(dest)}`); } async fetchJson(url) { const res = await fetch(url, { headers: { 'User-Agent': 'unity-mcp-server' } }); if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`); return await res.json(); } async downloadTo(url, dest) { const headers = { 'User-Agent': 'unity-mcp-server' }; const fetchOnce = async () => { const r = await fetch(url, { headers }); if (!r.ok) throw new Error(`HTTP ${r.status} for ${url}`); return r; }; const res = await fetchOnce(); const body = res.body; // Prefer WebStream -> Node stream piping if (body) { try { const file = fs.createWriteStream(dest); const { Readable } = await import('node:stream'); const nodeStream = Readable.fromWeb(body); await new Promise((resolve, reject) => { nodeStream.pipe(file); nodeStream.on('error', reject); file.on('finish', resolve); file.on('error', reject); }); return; } catch (e) { // If streaming failed or body was already consumed, re-fetch and fall back to arrayBuffer } } // Fallback: re-fetch fresh response and write full buffer const res2 = await fetchOnce(); const ab = await res2.arrayBuffer(); await fs.promises.writeFile(dest, Buffer.from(ab)); } async sha256File(file) { const { createHash } = await import('crypto'); return new Promise((resolve, reject) => { const hash = createHash('sha256'); const stream = fs.createReadStream(file); stream.on('data', d => hash.update(d)); stream.on('error', reject); stream.on('end', () => resolve(hash.digest('hex'))); }); } }