UNPKG

@akiojin/unity-mcp-server

Version:

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

107 lines (93 loc) 4.42 kB
import fs from 'fs/promises'; import path from 'path'; import { BaseToolHandler } from '../base/BaseToolHandler.js'; import { ProjectInfoProvider } from '../../core/projectInfo.js'; import { logger } from '../../core/config.js'; export class ScriptReadToolHandler extends BaseToolHandler { constructor(unityConnection) { super( 'script_read', 'Read a C# file with optional line range and payload limits. Files must be under Assets/ or Packages/ and have .cs extension. PRIORITY: Read minimally — locate the target with script_symbols_get and read only the signature area (~30–40 lines). For large files, always pass startLine/endLine and (optionally) maxBytes.', { type: 'object', properties: { path: { type: 'string', description: 'Project-relative C# path under Assets/ or Packages/ (e.g., Packages/unity-mcp-server/Editor/Example.cs). Do NOT prefix repository folders (e.g., UnityMCPServer/…).' }, startLine: { type: 'number', description: 'Starting line (1-based, inclusive). Defaults to 1.' }, endLine: { type: 'number', description: 'Ending line (inclusive). Defaults to startLine + 199 when omitted.' }, maxBytes: { type: 'number', description: 'Maximum bytes to return to cap payload size for LLMs.' } }, required: ['path'] } ); this.unityConnection = unityConnection; this.projectInfo = new ProjectInfoProvider(unityConnection); } validate(params) { super.validate(params); const { path, startLine, endLine } = params; // Validate path if (!path || path.trim() === '') { throw new Error('path cannot be empty'); } // Validate line numbers if provided if (startLine !== undefined && startLine < 1) { throw new Error('startLine must be at least 1'); } if (endLine !== undefined && startLine !== undefined && endLine < startLine) { throw new Error('endLine cannot be less than startLine'); } } async execute(params) { const { path, startLine = 1, endLine, maxBytes } = params; try { // Resolve project paths (Unity未接続でも推定可) const info = await this.projectInfo.get(); // Normalize and validate // Normalize common mistakes like UnityMCPServer/Packages/… → Packages/… const raw = (path || '').replace(/\\/g, '/'); const ai = raw.indexOf('Assets/'); const pi = raw.indexOf('Packages/'); const idx = (ai >= 0 && pi >= 0) ? Math.min(ai, pi) : (ai >= 0 ? ai : pi); const norm = idx >= 0 ? raw.substring(idx) : raw; if (!norm.startsWith('Assets/') && !norm.startsWith('Packages/')) { return { error: 'Path must be under Assets/ or Packages/' }; } if (!norm.toLowerCase().endsWith('.cs')) { return { error: 'Only .cs files are supported' }; } const abs = info.projectRoot + '/' + norm; const stat = await fs.stat(abs).catch(() => null); if (!stat || !stat.isFile()) return { error: 'File not found', path: norm }; const data = await fs.readFile(abs, 'utf8'); const lines = data.split('\n'); const s = Math.max(1, startLine); const e = Math.min(lines.length, endLine || (s + 199)); let content = lines.slice(s - 1, e).join('\n'); if (typeof maxBytes === 'number' && maxBytes > 0) { const buf = Buffer.from(content, 'utf8'); if (buf.length > maxBytes) content = buf.subarray(0, maxBytes).toString('utf8'); } return { success: true, path: norm, startLine: s, endLine: e, content }; } catch (e) { logger.error(`[script_read] failed: ${e.message}`); return { error: e.message }; } } }