UNPKG

@gabrielmaialva33/mcp-filesystem

Version:
176 lines 7.35 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import { createTwoFilesPatch } from 'diff'; import { z } from 'zod'; import { logger } from '../logger/index.js'; import { metrics } from '../metrics/index.js'; import { validateFileSize, validatePath } from './path.js'; import { FileSizeError, InvalidArgumentsError, PathNotFoundError } from '../errors/index.js'; export const ReadFileArgsSchema = z.object({ path: z.string().describe('Path to the file to read'), encoding: z .enum(['utf-8', 'utf8', 'base64']) .optional() .default('utf-8') .describe('File encoding'), }); export async function readFile(args, config) { const endMetric = metrics.startOperation('read_file'); try { const validPath = await validatePath(args.path, config); if (config.security.maxFileSize > 0) { await validateFileSize(validPath, config.security.maxFileSize); } const content = await fs.readFile(validPath, args.encoding); await logger.debug(`Successfully read file: ${validPath}`); endMetric(); return content; } catch (error) { metrics.recordError('read_file'); if (error.code === 'ENOENT') { throw new PathNotFoundError(args.path); } throw error; } } export const ReadMultipleFilesArgsSchema = z.object({ paths: z.array(z.string()).describe('List of file paths to read'), encoding: z .enum(['utf-8', 'utf8', 'base64']) .optional() .default('utf-8') .describe('File encoding'), }); export async function readMultipleFiles(args, config) { const endMetric = metrics.startOperation('read_multiple_files'); const results = {}; await Promise.all(args.paths.map(async (filePath) => { try { const validPath = await validatePath(filePath, config); if (config.security.maxFileSize > 0) { await validateFileSize(validPath, config.security.maxFileSize); } const content = await fs.readFile(validPath, args.encoding); results[filePath] = content; } catch (error) { if (error instanceof Error) { results[filePath] = error; } else { results[filePath] = new Error(String(error)); } } })); endMetric(); return results; } export const WriteFileArgsSchema = z.object({ path: z.string().describe('Path where to write the file'), content: z.string().describe('Content to write to the file'), encoding: z .enum(['utf-8', 'utf8', 'base64']) .optional() .default('utf-8') .describe('File encoding'), }); export async function writeFile(args, config) { const endMetric = metrics.startOperation('write_file'); try { const validPath = await validatePath(args.path, config); if (config.security.maxFileSize > 0) { const contentSize = Buffer.byteLength(args.content, args.encoding); if (contentSize > config.security.maxFileSize) { metrics.recordError('write_file'); throw new FileSizeError(args.path, contentSize, config.security.maxFileSize); } } const parentDir = path.dirname(validPath); await fs.mkdir(parentDir, { recursive: true }); await fs.writeFile(validPath, args.content, args.encoding); await logger.debug(`Successfully wrote to file: ${validPath}`); endMetric(); return `Successfully wrote to ${args.path}`; } catch (error) { metrics.recordError('write_file'); throw error; } } export const EditOperation = z.object({ oldText: z.string().describe('Text to search for - must match exactly'), newText: z.string().describe('Text to replace with'), }); export const EditFileArgsSchema = z.object({ path: z.string().describe('Path to the file to edit'), edits: z.array(EditOperation).describe('List of edit operations to perform'), dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format'), }); export async function editFile(args, config) { const endMetric = metrics.startOperation('edit_file'); try { const validPath = await validatePath(args.path, config); const content = await fs.readFile(validPath, 'utf-8'); let modifiedContent = content; let appliedAnyEdit = false; for (const edit of args.edits) { const contentLines = modifiedContent.split('\n'); let matchFound = false; const normalizedOld = edit.oldText.replace(/\r\n/g, '\n'); const normalizedNew = edit.newText.replace(/\r\n/g, '\n'); const oldLines = normalizedOld.split('\n'); if (oldLines.length === 0) { throw new InvalidArgumentsError('edit_file', 'Edit operation contains empty oldText'); } for (let i = 0; i <= contentLines.length - oldLines.length; i++) { const potentialMatch = contentLines.slice(i, i + oldLines.length).join('\n'); if (potentialMatch === normalizedOld) { const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; const newLines = normalizedNew.split('\n').map((line, j) => { if (j === 0) return originalIndent + line.trimStart(); const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''; const newIndent = line.match(/^\s*/)?.[0] || ''; if (oldIndent && newIndent) { const relativeIndent = newIndent.length - oldIndent.length; return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart(); } return line; }); contentLines.splice(i, oldLines.length, ...newLines); modifiedContent = contentLines.join('\n'); matchFound = true; appliedAnyEdit = true; break; } } if (!matchFound) { throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); } } if (!appliedAnyEdit) { return 'No changes made - all edit patterns were empty or not found'; } const diff = createUnifiedDiff(content, modifiedContent, validPath); if (!args.dryRun) { await fs.writeFile(validPath, modifiedContent, 'utf-8'); await logger.debug(`Successfully edited file: ${validPath}`); } endMetric(); return diff; } catch (error) { metrics.recordError('edit_file'); throw error; } } function createUnifiedDiff(originalContent, modifiedContent, filePath) { const diff = createTwoFilesPatch(filePath, filePath, originalContent, modifiedContent, 'Original', 'Modified'); let numBackticks = 3; while (diff.includes('`'.repeat(numBackticks))) { numBackticks++; } return `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; } //# sourceMappingURL=tools.js.map