Skip to main content
Glama
git-files.ts23.6 kB
/** * Git Files Tool * * File management tool providing CRUD operations for repository files. * Supports both local Git operations and remote provider operations. * * Operations: read, create, update, delete, search, backup */ import { GitCommandExecutor } from '../utils/git-command-executor.js'; import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js'; import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js'; import { ProviderOperationHandler } from '../providers/provider-operation-handler.js'; import { ProviderConfig, ProviderOperation } from '../providers/types.js'; import { configManager } from '../config.js'; import { TerminalController } from '../utils/terminal-controller.js'; import path from 'path'; import fs from 'fs/promises'; export interface GitFilesParams extends ToolParams { action: 'read' | 'search' | 'backup' | 'list'; // File path parameters filePath?: string; // Relative path to file within repository // Content parameters (for read operations only) encoding?: 'utf8' | 'base64' | 'binary'; // Content encoding for read operations // Remote operation parameters branch?: string; // Target branch for remote read operations // Search parameters query?: string; // Search query filePattern?: string; // File pattern for search (e.g., "*.js") caseSensitive?: boolean; // Case sensitive search // Backup parameters backupPath?: string; // Path for backup includePattern?: string; // Pattern for files to backup excludePattern?: string; // Pattern for files to exclude from backup // Remote provider parameters repo?: string; // Repository name } export class GitFilesTool { private gitExecutor: GitCommandExecutor; private terminalController: TerminalController; private providerHandler?: ProviderOperationHandler; constructor(providerConfig?: ProviderConfig) { this.gitExecutor = new GitCommandExecutor(); this.terminalController = new TerminalController(); if (providerConfig) { this.providerHandler = new ProviderOperationHandler(providerConfig); } } /** * Execute git-files operation */ async execute(params: GitFilesParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-files', params); if (!validation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Parameter validation failed: ${validation.errors.join(', ')}`, params.action, { validationErrors: validation.errors }, validation.suggestions ); } // Validate operation-specific parameters const operationValidation = ParameterValidator.validateOperationParams('git-files', params.action, params); if (!operationValidation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Operation validation failed: ${operationValidation.errors.join(', ')}`, params.action, { validationErrors: operationValidation.errors }, operationValidation.suggestions ); } // Check if operation is restricted (file content modification) if (this.isRestrictedOperation(params.action)) { return OperationErrorHandler.createToolError( 'OPERATION_RESTRICTED', `File modification operations (create, update, delete) are not allowed. This tool only supports read-only operations for security reasons.`, params.action, { restrictedOperation: params.action }, [ 'Use read, list, or search operations to view file content', 'Use backup operations to create local backups', 'File modifications should be done through your local development environment' ] ); } // Route to appropriate handler // File operations are local by default, remote only when provider is explicitly specified const isRemoteOperation = params.provider && this.isRemoteOperation(params.action); if (isRemoteOperation) { return await this.executeRemoteOperation(params, startTime); } else { return await this.executeLocalOperation(params, startTime); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'EXECUTION_ERROR', `Failed to execute ${params.action}: ${errorMessage}`, params.action, { error: errorMessage }, ['Check the error details and try again'] ); } } /** * Execute local file operations */ private async executeLocalOperation(params: GitFilesParams, startTime: number): Promise<ToolResult> { switch (params.action) { case 'list': return await this.handleLocalList(params, startTime); case 'read': return await this.handleLocalRead(params, startTime); case 'search': return await this.handleLocalSearch(params, startTime); case 'backup': return await this.handleLocalBackup(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_OPERATION', `Local operation '${params.action}' is not supported`, params.action, {}, ['Use one of: read, list, search, backup'] ); } } /** * Execute remote provider operations */ private async executeRemoteOperation(params: GitFilesParams, startTime: number): Promise<ToolResult> { if (!this.providerHandler) { return OperationErrorHandler.createToolError( 'PROVIDER_NOT_CONFIGURED', 'Provider handler is not configured for remote operations', params.action, {}, ['Configure GitHub or Gitea provider to use remote operations'] ); } if (!params.provider) { if (configManager.isUniversalMode()) { params.provider = 'both'; console.error(`[Universal Mode] Auto-applying both providers for ${params.action}`); } else { return OperationErrorHandler.createToolError( 'PROVIDER_REQUIRED', 'Provider parameter is required for remote operations', params.action, {}, ['Specify provider as: github, gitea, or both'] ); } } const operation: ProviderOperation = { provider: params.provider, operation: this.mapActionToProviderOperation(params.action), parameters: this.extractRemoteParameters(params), requiresAuth: true, isRemoteOperation: true }; try { const result = await this.providerHandler.executeOperation(operation); return { success: result.success, data: result.partialFailure ? result : result.results[0]?.data, error: result.success ? undefined : { code: result.errors[0]?.error?.code || 'REMOTE_OPERATION_ERROR', message: result.errors[0]?.error?.message || 'Remote operation failed', details: result.errors, suggestions: ['Check provider configuration and credentials'] }, metadata: { provider: params.provider, operation: params.action, timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_OPERATION_ERROR', `Remote operation failed: ${errorMessage}`, params.action, { error: errorMessage }, ['Check provider configuration and network connectivity'] ); } } /** * Handle local file read operation */ private async handleLocalRead(params: GitFilesParams, startTime: number): Promise<ToolResult> { try { if (!params.filePath) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'File path is required for read operation', 'read', {}, ['Provide a filePath parameter with the relative path to the file'] ); } const fullPath = path.join(params.projectPath, params.filePath); // Check if file exists try { await fs.access(fullPath); } catch { return OperationErrorHandler.createToolError( 'FILE_NOT_FOUND', `File not found: ${params.filePath}`, 'read', { filePath: params.filePath, fullPath }, ['Check that the file path is correct and the file exists'] ); } // Read file content const encoding = params.encoding || 'utf8'; let content: string | Buffer; if (encoding === 'binary') { content = await fs.readFile(fullPath); } else { content = await fs.readFile(fullPath, encoding as BufferEncoding); } // Get file stats const stats = await fs.stat(fullPath); return { success: true, data: { filePath: params.filePath, content: encoding === 'binary' ? content.toString('base64') : content, encoding, size: stats.size, modified: stats.mtime.toISOString(), created: stats.birthtime.toISOString(), isDirectory: stats.isDirectory(), permissions: stats.mode.toString(8) }, metadata: { operation: 'read', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'READ_ERROR', `Failed to read file: ${errorMessage}`, 'read', { error: errorMessage, filePath: params.filePath } ); } } /** * Check if operation is restricted (file content modification) */ private isRestrictedOperation(action: string): boolean { return ['create', 'update', 'delete'].includes(action); } /** * Handle local file search operation */ private async handleLocalSearch(params: GitFilesParams, startTime: number): Promise<ToolResult> { try { if (!params.query) { return OperationErrorHandler.createToolError( 'MISSING_PARAMETER', 'Query is required for search operation', 'search', {}, ['Provide a query parameter with the text to search for'] ); } const searchOptions = { caseSensitive: params.caseSensitive || false, filePattern: params.filePattern || '*', maxResults: 100 }; // Use terminal controller to search for content const searchResult = await this.terminalController.searchInFiles( params.projectPath, params.query, searchOptions ); // Normalize no-match behavior (grep/findstr return code 1) if (!searchResult.success && searchResult.exitCode === 1 && !searchResult.stdout.trim()) { // No matches found — return empty result set return { success: true, data: { query: params.query, matches: [], totalMatches: 0, searchOptions }, metadata: { operation: 'search', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } if (!searchResult.success) { return OperationErrorHandler.createToolError( 'SEARCH_ERROR', `Search failed: ${searchResult.stderr}`, 'search', { error: searchResult.stderr }, ['Check the search query and try again'] ); } // Parse search results const matches = this.parseSearchResults(searchResult.stdout); return { success: true, data: { query: params.query, matches, totalMatches: matches.length, searchOptions }, metadata: { operation: 'search', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'SEARCH_ERROR', `Failed to search files: ${errorMessage}`, 'search', { error: errorMessage, query: params.query } ); } } /** * Handle local directory listing */ private async handleLocalList(params: GitFilesParams, startTime: number): Promise<ToolResult> { try { const listPath = params.filePath ? path.join(params.projectPath, params.filePath) : params.projectPath; // Verify path exists try { await fs.access(listPath); } catch { return OperationErrorHandler.createToolError( 'PATH_NOT_FOUND', `Path not found: ${listPath}`, 'list', { path: listPath }, ['Provide a valid projectPath/filePath'] ); } const entries = await fs.readdir(listPath, { withFileTypes: true }); const items = await Promise.all(entries.map(async (entry) => { const entryPath = path.join(listPath, entry.name); const stats = await fs.stat(entryPath); return { name: entry.name, path: path.relative(params.projectPath, entryPath).replace(/\\/g, '/'), isDirectory: entry.isDirectory(), size: stats.size, modified: stats.mtime.toISOString() }; })); return { success: true, data: { items, total: items.length }, metadata: { operation: 'list', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'LIST_ERROR', `Failed to list path: ${errorMessage}`, 'list', { error: errorMessage } ); } } /** * Handle local file backup operation */ private async handleLocalBackup(params: GitFilesParams, startTime: number): Promise<ToolResult> { try { if (!params.backupPath) { // Generate default backup path const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); params.backupPath = path.join(params.projectPath, '..', `files-backup-${timestamp}`); } // Create backup using git archive if in a git repository const isGitRepo = await this.gitExecutor.isGitRepository(params.projectPath); if (isGitRepo) { const result = await this.gitExecutor.createBackup(params.projectPath, params.backupPath, 'tar'); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'backup', params.projectPath); } return { success: true, data: { message: 'Files backup created successfully using Git archive', backupPath: `${params.backupPath}.tar.gz`, method: 'git-archive', output: result.stdout }, metadata: { operation: 'backup', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } else { // Fallback to manual file copy const backupResult = await this.createManualBackup( params.projectPath, params.backupPath, { includePattern: params.includePattern, excludePattern: params.excludePattern } ); return { success: true, data: { message: 'Files backup created successfully using file copy', backupPath: params.backupPath, method: 'file-copy', filesBackedUp: backupResult.fileCount, totalSize: backupResult.totalSize }, metadata: { operation: 'backup', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'BACKUP_ERROR', `Failed to create backup: ${errorMessage}`, 'backup', { error: errorMessage, backupPath: params.backupPath } ); } } /** * Create manual backup by copying files */ private async createManualBackup( sourcePath: string, backupPath: string, options: { includePattern?: string; excludePattern?: string; } ): Promise<{ fileCount: number; totalSize: number }> { await fs.mkdir(backupPath, { recursive: true }); let fileCount = 0; let totalSize = 0; const copyRecursive = async (src: string, dest: string) => { const stats = await fs.stat(src); if (stats.isDirectory()) { await fs.mkdir(dest, { recursive: true }); const entries = await fs.readdir(src); for (const entry of entries) { const srcPath = path.join(src, entry); const destPath = path.join(dest, entry); await copyRecursive(srcPath, destPath); } } else { // Apply include/exclude patterns const relativePath = path.relative(sourcePath, src); if (options.excludePattern && this.matchesPattern(relativePath, options.excludePattern)) { return; } if (options.includePattern && !this.matchesPattern(relativePath, options.includePattern)) { return; } await fs.copyFile(src, dest); fileCount++; totalSize += stats.size; } }; await copyRecursive(sourcePath, backupPath); return { fileCount, totalSize }; } /** * Check if operation is a remote operation */ private isRemoteOperation(action: string): boolean { // Only read-only operations can be remote operations when provider is specified return ['read', 'search', 'list'].includes(action); } /** * Extract parameters for remote operations */ private extractRemoteParameters(params: GitFilesParams): Record<string, any> { const remoteParams: Record<string, any> = { projectPath: params.projectPath }; // Common parameters if (params.repo) remoteParams.repo = params.repo; if (params.filePath) remoteParams.path = params.filePath; // Operation-specific parameters (only read-only operations) switch (params.action) { case 'read': if (params.branch) remoteParams.ref = params.branch; break; case 'search': if (params.query) remoteParams.query = params.query; break; case 'list': // No additional parameters needed for list break; } return remoteParams; } /** * Map git-files actions to provider operations */ private mapActionToProviderOperation(action: string): string { const actionMap: Record<string, string> = { 'read': 'file-read', 'list': 'listFiles', 'search': 'file-search' }; return actionMap[action] || action; } /** * Parse search results from grep-like output */ private parseSearchResults(output: string): Array<{ file: string; line: number; content: string; match: string; }> { const matches: Array<{ file: string; line: number; content: string; match: string; }> = []; const lines = output.split('\n').filter(line => line.trim()); for (const line of lines) { // Parse grep output format: file:line:content const match = line.match(/^([^:]+):(\d+):(.*)$/); if (match) { const [, file, lineNum, content] = match; matches.push({ file, line: parseInt(lineNum), content: content.trim(), match: content.trim() }); } } return matches; } /** * Check if path matches pattern (simple glob matching) */ private matchesPattern(filePath: string, pattern: string): boolean { // Convert glob pattern to regex const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); const regex = new RegExp(`^${regexPattern}$`); return regex.test(filePath); } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-files', description: 'Read-only file management tool for repository files. Supports reading, searching, listing, and backup operations for both local and remote repositories. File content modification operations are not allowed for security reasons. In universal mode (GIT_MCP_MODE=universal), automatically executes on both GitHub and Gitea providers.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['read', 'search', 'backup', 'list'], description: 'The file operation to perform. Only read-only operations are supported.' }, projectPath: { type: 'string', description: 'Absolute path to the project directory (required for all operations)' }, provider: { type: 'string', enum: ['github', 'gitea', 'both'], description: 'Provider for remote operations (optional for local operations)' }, filePath: { type: 'string', description: 'Relative path to the file within the repository (required for read operation)' }, encoding: { type: 'string', enum: ['utf8', 'base64', 'binary'], description: 'Content encoding for read operations (default: utf8)' }, branch: { type: 'string', description: 'Target branch for remote read operations' }, query: { type: 'string', description: 'Search query (required for search operation)' }, filePattern: { type: 'string', description: 'File pattern for search (e.g., "*.js")' }, caseSensitive: { type: 'boolean', description: 'Case sensitive search (default: false)' }, backupPath: { type: 'string', description: 'Path for backup (optional for backup operation - will auto-generate if not provided)' }, includePattern: { type: 'string', description: 'Pattern for files to include in backup' }, excludePattern: { type: 'string', description: 'Pattern for files to exclude from backup' }, repo: { type: 'string', description: 'Repository name (for remote operations)' } }, required: ['action', 'projectPath'] } }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Andre-Buzeli/git-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server