Unity MCP Server

import path from 'path'; import fs from 'fs-extra'; import { glob } from 'glob'; import { ToolRegistry } from '../tool-registry'; import logger from '../../logging/logger'; import { Tool } from '../types'; /** * Register file manipulation tools with the registry */ export function registerFileTools(): void { const registry = ToolRegistry.getInstance(); // Tool: open_file registry.registerTool({ name: 'open_file', description: 'Reads the contents of a file in the Unity project', parameterSchema: { type: 'object', required: ['path'], properties: { path: { type: 'string', description: 'Project-relative path of the file to read', }, }, }, execute: async (parameters: any) => { const filePath = parameters.path; try { logger.debug(`Reading file: ${filePath}`); // Validate file exists if (!fs.existsSync(filePath)) { return { success: false, error: { message: `File not found: ${filePath}`, code: 'FILE_NOT_FOUND' } }; } // Read file contents const content = await fs.readFile(filePath, 'utf8'); return { success: true, data: { content, path: filePath } }; } catch (error: any) { logger.error(`Error reading file: ${filePath}`, error); return { success: false, error: { message: `Error reading file: ${error.message}`, code: 'FILE_READ_ERROR', details: error } }; } } }); // Tool: search_files registry.registerTool({ name: 'search_files', description: 'Searches for text patterns in project files', parameterSchema: { type: 'object', required: ['query'], properties: { query: { type: 'string', description: 'Text to search for in project files', }, fileTypes: { type: 'array', items: { type: 'string' }, description: 'Specific file extensions to search', }, maxResults: { type: 'number', description: 'Maximum number of results to return', }, }, }, execute: async (parameters: any) => { try { const { query, fileTypes = [], maxResults = 100 } = parameters; // Build glob pattern based on file types const patterns = fileTypes.length > 0 ? fileTypes.map((ext: string) => `**/*.${ext}`) : ['**/*.cs', '**/*.unity', '**/*.prefab', '**/*.shader', '**/*.txt', '**/*.json', '**/*.asset']; logger.debug(`Searching for "${query}" in patterns: ${patterns.join(', ')}`); // Find matching files const files = await glob(patterns); // Search in each file const results = []; for (const file of files) { // Skip files in node_modules, Library, etc. if (file.includes('node_modules/') || file.includes('Library/') || file.includes('.git/')) { continue; } try { const content = await fs.readFile(file, 'utf8'); if (content.includes(query)) { const lines = content.split('\n'); const matches = []; // Find line numbers of matches lines.forEach((line, index) => { if (line.includes(query)) { matches.push({ line: index + 1, content: line.trim(), }); } }); if (matches.length > 0) { results.push({ file, matches }); // If we have enough results, stop searching if (results.length >= maxResults) { break; } } } } catch (err) { logger.warn(`Error reading file ${file} for search: ${err}`); // Continue with other files } } return { success: true, data: { query, results, totalResults: results.length } }; } catch (error: any) { logger.error('Error searching files', error); return { success: false, error: { message: `Error searching files: ${error.message}`, code: 'SEARCH_ERROR', details: error } }; } } }); // Tool: list_assets registry.registerTool({ name: 'list_assets', description: 'Lists project assets of a certain type or in a folder', parameterSchema: { type: 'object', properties: { type: { type: 'string', description: 'Filter by asset type', }, folder: { type: 'string', description: 'Directory to list assets from', }, }, }, execute: async (parameters: any) => { try { const { type, folder = 'Assets' } = parameters; // Validate folder exists if (!fs.existsSync(folder)) { return { success: false, error: { message: `Folder not found: ${folder}`, code: 'FOLDER_NOT_FOUND' } }; } // Determine pattern based on asset type let pattern: string; switch (type?.toLowerCase()) { case 'scene': pattern = '**/*.unity'; break; case 'prefab': pattern = '**/*.prefab'; break; case 'script': pattern = '**/*.cs'; break; case 'material': pattern = '**/*.mat'; break; case 'shader': pattern = '**/*.shader'; break; case 'texture': pattern = '**/*.{png,jpg,jpeg,tga,bmp,psd,tif,tiff}'; break; case 'model': pattern = '**/*.{fbx,obj,blend,max,ma,mb}'; break; case 'audio': pattern = '**/*.{mp3,wav,ogg,aiff}'; break; case 'animation': pattern = '**/*.anim'; break; case 'scriptableobject': pattern = '**/*.asset'; break; default: pattern = '**/*.*'; break; } logger.debug(`Listing assets in ${folder} with pattern: ${pattern}`); // Find files matching the pattern const files = await glob(`${folder}/${pattern}`); // Group files by type const filesByType: Record<string, string[]> = {}; files.forEach(file => { const extension = path.extname(file).toLowerCase().substring(1); if (!filesByType[extension]) { filesByType[extension] = []; } filesByType[extension].push(file); }); return { success: true, data: { folder, type: type || 'all', totalFiles: files.length, files, filesByType } }; } catch (error: any) { logger.error('Error listing assets', error); return { success: false, error: { message: `Error listing assets: ${error.message}`, code: 'LIST_ASSETS_ERROR', details: error } }; } } }); }