UNPKG

@akiojin/unity-mcp-server

Version:

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

97 lines (88 loc) 4.19 kB
import fs from 'fs/promises'; import path from 'path'; import { BaseToolHandler } from '../base/BaseToolHandler.js'; import { ProjectInfoProvider } from '../../core/projectInfo.js'; export class ScriptSymbolsGetToolHandler extends BaseToolHandler { constructor(unityConnection) { super( 'script_symbols_get', 'FIRST STEP: Identify symbols (classes, methods, fields, properties) with spans before any edit. Path must start with Assets/ or Packages/. Use this to scope changes to a single symbol and avoid line-based edits. Returns line/column positions and container names (helpful to build container namePath like Outer/Nested/Member).', { type: 'object', properties: { path: { type: 'string', description: 'Project-relative .cs path under Assets/ or Packages/ (e.g., Packages/unity-mcp-server/Editor/Foo.cs). Do NOT prefix repository folders (e.g., UnityMCPServer/…).' } }, required: ['path'] } ); this.unityConnection = unityConnection; this.projectInfo = new ProjectInfoProvider(unityConnection); } validate(params) { super.validate(params); const { path } = params; if (!path || path.trim() === '') { throw new Error('path cannot be empty'); } // Check for .cs extension if (!path.endsWith('.cs')) { throw new Error('Only .cs files are supported'); } } async execute(params) { // Normalize to project-relative path (strip repo-root prefixes if provided) const rawPath = String(params.path || '').replace(/\\/g, '/'); const ai = rawPath.indexOf('Assets/'); const pi = rawPath.indexOf('Packages/'); const cut = (ai >= 0 && pi >= 0) ? Math.min(ai, pi) : (ai >= 0 ? ai : pi); const relPath = cut >= 0 ? rawPath.substring(cut) : rawPath; try { const info = await this.projectInfo.get(); if (!relPath.startsWith('Assets/') && !relPath.startsWith('Packages/')) { return { error: 'Path must be under Assets/ or Packages/' }; } const abs = path.join(info.projectRoot, relPath); const st = await fs.stat(abs).catch(() => null); if (!st || !st.isFile()) return { error: 'File not found', path: relPath }; const { LspRpcClient } = await import('../../lsp/LspRpcClient.js'); const lsp = new LspRpcClient(info.projectRoot); const uri = 'file://' + abs.replace(/\\\\/g, '/'); const res = await lsp.request('textDocument/documentSymbol', { textDocument: { uri } }); const docSymbols = res?.result ?? res ?? []; const list = []; const visit = (s, container) => { const start = s.range?.start || s.selectionRange?.start || {}; list.push({ name: s.name || '', kind: this.mapKind(s.kind), container: container || null, namespace: null, startLine: (start.line ?? 0) + 1, startColumn: (start.character ?? 0) + 1, endLine: (start.line ?? 0) + 1, endColumn: (start.character ?? 0) + 1 }); if (Array.isArray(s.children)) for (const c of s.children) visit(c, s.name || container); }; if (Array.isArray(docSymbols)) for (const s of docSymbols) visit(s, null); return { success: true, path: relPath, symbols: list }; } catch (e) { return { error: e.message || 'Failed to get symbols' }; } } mapKind(k) { switch (k) { case 5: return 'class'; case 23: return 'struct'; case 11: return 'interface'; case 10: return 'enum'; case 6: return 'method'; case 7: return 'property'; case 8: return 'field'; default: return undefined; } } }