UNPKG

@akiojin/unity-editor-mcp

Version:

MCP server for Unity Editor integration - enables AI assistants to control Unity Editor

144 lines (132 loc) 5.74 kB
import { BaseToolHandler } from '../base/BaseToolHandler.js'; import { RoslynCliUtils } from '../roslyn/RoslynCliUtils.js'; import { CodeIndex } from '../../core/codeIndex.js'; export class ScriptRefsFindToolHandler extends BaseToolHandler { constructor(unityConnection) { super( 'script_refs_find', 'Find code references/usages using external Roslyn CLI (no Unity comms). LLM-friendly paging/summary: respects pageSize and maxBytes, caps matches per file (maxMatchesPerFile), and trims snippet text to ~400 chars. Use scope/name/kind/path to narrow results.', { type: 'object', properties: { name: { type: 'string', description: 'Symbol name to search usages for.' }, scope: { type: 'string', enum: ['assets', 'packages', 'embedded', 'all'], default: 'all', description: 'Search scope: assets (Assets/), packages (Packages/), embedded, or all.' }, snippetContext: { type: 'number', description: 'Number of context lines to include around each match.' }, maxMatchesPerFile: { type: 'number', description: 'Cap reference matches returned per file.' }, pageSize: { type: 'number', description: 'Maximum results to return per page.' }, maxBytes: { type: 'number', description: 'Maximum response size (bytes) to keep outputs LLM‑friendly.' }, container: { type: 'string', description: 'Optional: container (class) of the symbol.' }, namespace: { type: 'string', description: 'Optional: namespace of the symbol.' }, path: { type: 'string', description: 'Optional: constrain to file path containing the symbol.' }, kind: { type: 'string', description: 'Optional: symbol kind (class, method, field, property).' } }, required: ['name'] } ); this.unityConnection = unityConnection; this.roslyn = new RoslynCliUtils(unityConnection); this.index = new CodeIndex(unityConnection); } validate(params) { super.validate(params); const { name } = params; if (!name || name.trim() === '') { throw new Error('name cannot be empty'); } } async execute(params) { const { name, path, kind, namespace, container, scope = 'all', pageSize = 50, maxBytes = 1024 * 64, snippetContext = 2, // 現状CLIは±1行固定。ハンドラ側では文字数トリムのみ行う。 maxMatchesPerFile = 5 } = params; // Roslyn CLIへ委譲 const args = ['find-references']; args.push(...(await this.roslyn.getSolutionOrProjectArgs())); args.push('--name', String(name)); if (path) args.push('--relative', String(path).replace(/\\\\/g, '/')); const res = await this.roslyn.runCli(args); let raw = Array.isArray(res?.results) ? res.results : []; // スコープ絞り込み if (scope && scope !== 'all') { raw = raw.filter(r => { const p = (r.path || '').replace(/\\\\/g, '/'); switch (scope) { case 'assets': return p.startsWith('Assets/'); case 'packages': return p.startsWith('Packages/') || p.startsWith('Library/PackageCache/'); case 'embedded': return p.startsWith('Packages/'); default: return true; } }); } // ファイル毎の件数制限 + スニペット文字数トリム const MAX_SNIPPET = 400; const perFile = new Map(); for (const item of raw) { const key = (item.path || '').replace(/\\\\/g, '/'); const list = perFile.get(key) || []; if (list.length < maxMatchesPerFile) { if (typeof item.snippet === 'string' && item.snippet.length > MAX_SNIPPET) { item.snippet = item.snippet.slice(0, MAX_SNIPPET) + '…'; item.snippetTruncated = true; } list.push(item); perFile.set(key, list); } } // ページング/サイズ上限 const results = []; let bytes = 0; for (const [_, arr] of perFile) { for (const it of arr) { const json = JSON.stringify(it); const size = Buffer.byteLength(json, 'utf8'); if (results.length >= pageSize || (bytes + size) > maxBytes) { return { success: true, results, total: results.length, truncated: true }; } results.push(it); bytes += size; } } return { success: true, results, total: results.length, truncated: false }; } }