Unity MCP Server
- unity-smithery-mcp
- src
- api
- tool-handlers
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
}
};
}
}
});
}