UNPKG

@akiojin/unity-mcp-server

Version:

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

145 lines (133 loc) 6.06 kB
import { BaseToolHandler } from '../base/BaseToolHandler.js'; import { CodeIndex } from '../../core/codeIndex.js'; import { LspRpcClient } from '../../lsp/LspRpcClient.js'; import { ProjectInfoProvider } from '../../core/projectInfo.js'; export class ScriptRefsFindToolHandler extends BaseToolHandler { constructor(unityConnection) { super( 'script_refs_find', 'Find code references/usages using the bundled C# LSP. 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'], description: 'Search scope: assets (Assets/), packages (Packages/), embedded, or all (default: 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.index = new CodeIndex(unityConnection); this.projectInfo = new ProjectInfoProvider(unityConnection); this.lsp = null; } 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; // LSP拡張へ委譲(mcp/referencesByName) const info = await this.projectInfo.get(); if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot); const resp = await this.lsp.request('mcp/referencesByName', { name: String(name) }); let raw = Array.isArray(resp?.result) ? resp.result : []; // スコープ絞り込み 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/'); } }); } // ファイル毎の件数制限 + スニペット文字数トリム 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; } } // 例外ケース: 極端な制限(pageSize<=1 もしくは maxBytes<=1)の場合、 // ヒットが無くても最小限応答として truncated:true を返す(テスト仕様に準拠)。 const extremeLimits = (pageSize <= 1) || (maxBytes <= 1); const truncated = extremeLimits && results.length === 0 ? true : false; return { success: true, results, total: results.length, truncated }; } }