Skip to main content
Glama

Filesystem MCP Server for WSL

index.ts45.8 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import { exec } from 'child_process'; import { promisify } from 'util'; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { createTwoFilesPatch } from 'diff'; // Définition d'interfaces pour les types utilisés dans le script interface WslDistribution { name: string; state: string; version: string; isDefault: boolean; } interface WslFileStats { size: number; birthtime: Date; mtime: Date; atime: Date; mode: number; isDirectory: () => boolean; isFile: () => boolean; } interface FileEntry { name: string; isDirectory: () => boolean; isFile: () => boolean; } interface TreeEntry { name: string; type: 'file' | 'directory'; children?: TreeEntry[]; } interface FileInfo { size: number; created: Date; modified: Date; accessed: Date; isDirectory: boolean; isFile: boolean; permissions: string; } interface EditOperationType { oldText: string; newText: string; } // Promisify exec pour utiliser async/await const execAsync = promisify(exec); // Command line argument parsing const args = process.argv.slice(2); const distroArg = args.find(arg => arg.startsWith('--distro=')); let allowedDistro: string | null = distroArg ? distroArg.split('=')[1] : null; const pathArgs = args.filter(arg => !arg.startsWith('--')); if (pathArgs.length === 0) { console.error("Usage: mcp-server-wsl-filesystem [--distro=name] <allowed-directory> [additional-directories...]"); process.exit(1); } // Fonctions utilitaires pour manipuler les chemins en respectant les conventions Linux function normalizePath(p: string): string { // Remplacer les chemins Windows par des chemins Linux p = p.replace(/\\/g, '/'); // Gérer les doubles barres obliques p = p.replace(/\/+/g, '/'); // Supprimer les points simples const parts = p.split('/'); const result: string[] = []; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part === '.') continue; if (part === '..') { if (result.length > 0 && result[result.length - 1] !== '..') { result.pop(); } else { result.push('..'); } } else if (part !== '') { result.push(part); } } let normalized = result.join('/'); if (p.startsWith('/')) normalized = '/' + normalized; if (p.endsWith('/') && normalized !== '/') normalized += '/'; return normalized || '.'; } function expandHome(filepath: string): string { if (filepath.startsWith('~/') || filepath === '~') { // Dans WSL, on peut obtenir le home directory avec $HOME return `$HOME${filepath.slice(1)}`; } return filepath; } function isAbsolute(p: string): boolean { return p.startsWith('/'); } function resolve(...paths: string[]): string { let resolvedPath = ''; for (let i = 0; i < paths.length; i++) { const path = paths[i]; if (isAbsolute(path)) { resolvedPath = path; } else { if (!resolvedPath) resolvedPath = process.cwd().replace(/\\/g, '/'); resolvedPath = `${resolvedPath}/${path}`; } } return normalizePath(resolvedPath); } function dirname(p: string): string { const normalized = normalizePath(p); if (normalized === '/') return '/'; if (!normalized.includes('/')) return '.'; const lastSlashIndex = normalized.lastIndexOf('/'); if (lastSlashIndex === 0) return '/'; if (lastSlashIndex === -1) return '.'; return normalized.slice(0, lastSlashIndex); } function join(...paths: string[]): string { return normalizePath(paths.join('/')); } function isUtf16(str:string) { return str.indexOf('\0') !== -1; } function processOutput(output:string) { let lines; if (isUtf16(output)) { // Pour UTF-16 const buffer = Buffer.from(output); const text = buffer.toString('utf16le'); lines = text.trim().split('\n').slice(1); } else { // Pour UTF-8 lines = output.toString().trim().split('\n').slice(1); } // Nettoyer les caractères restants comme les retours chariot return lines.map(line => line.replace(/\r/g, '').trim()); } // Fonctions pour gérer les distributions WSL async function listWslDistributions(): Promise<WslDistribution[]> { try { const { stdout } = await execAsync('wsl --list --verbose'); const lines = processOutput(stdout); return lines.map(line => { // Gestion de l'astérisque pour la distribution par défaut const isDefault = line.trim().startsWith('*'); // Supprimer l'astérisque si présent et diviser par espaces const parts = line.trim().replace(/^\*\s*/, '').split(/\s+/); const name = parts[0]; const state = parts[1]; const version = parts[2]; return { name, state, version, isDefault }; }); } catch (error) { console.error("Erreur lors de la récupération des distributions WSL:", error); return []; } } // Vérifier les distributions disponibles et définir la distribution par défaut si nécessaire async function setupWslDistribution(): Promise<string> { const distributions = await listWslDistributions(); if (distributions.length === 0) { console.error("Aucune distribution WSL n'a été trouvée sur ce système."); process.exit(1); } // Si aucune distribution spécifique n'est demandée, utiliser la distribution par défaut if (!allowedDistro) { const defaultDistro = distributions.find(d => d.isDefault); if (defaultDistro) { allowedDistro = defaultDistro.name; } else { // Si aucune distribution par défaut n'est marquée, utiliser la première allowedDistro = distributions[0].name; } } else { // Vérifier si la distribution demandée existe const exists = distributions.some(d => d.name.toLowerCase() === allowedDistro!.toLowerCase()); if (!exists) { console.error(`La distribution WSL '${allowedDistro}' n'existe pas. Distributions disponibles:`); distributions.forEach(d => console.error(`- ${d.name}`)); process.exit(1); } } console.error(`Utilisation de la distribution WSL: ${allowedDistro}`); return allowedDistro!; } let allowedDirectories: string[] = []; // Initialiser la distribution WSL et attendre qu'elle soit configurée async function initializeWslAndDirectories(): Promise<void> { try { await setupWslDistribution(); allowedDirectories = pathArgs.map(dir => normalizePath(resolve(expandHome(dir)))); await validateDirectories(); } catch (error) { console.error("Erreur lors de l'initialisation:", error); process.exit(1); } } /** * Exécute une commande unique dans WSL */ async function execWslCommand(command: string): Promise<string> { try { const wslCommand = allowedDistro ? `wsl -d ${allowedDistro} ${command}` : `wsl ${command}`; const { stdout } = await execAsync(wslCommand); return stdout.trim(); } catch (error: any) { throw new Error(`WSL command failed: ${error.message}`); } } /** * Exécute une chaîne de commandes avec des pipes dans WSL * Chaque élément du tableau sera préfixé avec wsl [options] avant d'être joint par des pipes */ async function execWslPipeline(commands: string[]): Promise<string> { try { if (commands.length === 0) { throw new Error("No commands provided"); } const joined = commands.join(" | ").replace(/"/g, '\\"'); const fullCommand = allowedDistro ? `wsl -d ${allowedDistro} sh -c "${joined}"` : `wsl sh -c "${joined}"`; const { stdout } = await execAsync(fullCommand); return stdout.trim(); } catch (error: any) { throw new Error(`WSL pipeline failed: ${error.message}`); } } // Convertir un chemin Windows en chemin WSL function toWslPath(windowsPath: string): string { // Nettoyer et normaliser le chemin pour WSL const normalizedPath = normalizePath(windowsPath); // Échapper les caractères spéciaux pour la ligne de commande return normalizedPath.replace(/(["\s'$`\\])/g, '\\$1'); } // Fonctions d'utilitaire pour les opérations de fichier via WSL async function wslStat(filePath: string): Promise<WslFileStats> { const wslPath = toWslPath(filePath); try { const result = await execWslCommand(`stat -c "%s %Y %X %W %a %F" "${wslPath}"`); const [size, mtime, atime, birthtime, permissions, type] = result.split(' '); return { size: parseInt(size), birthtime: new Date(parseInt(birthtime) * 1000), mtime: new Date(parseInt(mtime) * 1000), atime: new Date(parseInt(atime) * 1000), mode: parseInt(permissions, 8), isDirectory: () => type.includes('directory'), isFile: () => type.includes('regular file') }; } catch (error: any) { throw new Error(`Failed to stat ${filePath}: ${error.message}`); } } async function wslReaddir(dirPath: string): Promise<FileEntry[]> { const wslPath = toWslPath(dirPath); try { // Utiliser ls -la, mais filtrer . et .. (d'où le tail -n +3), et utiliser le bon format de sortie const result = await execWslCommand( `sh -c "ls -la \\"${wslPath}\\" | tail -n +3"` ); if (!result) return []; return result.split('\n').map(line => { // Format typique: "drwxr-xr-x 2 user group 4096 Jan 1 12:34 dirname" const parts = line.trim().split(/\s+/); // Le nom peut contenir des espaces, donc nous prenons tout à partir de la 9ème colonne const name = parts.slice(8).join(' '); const isDir = line.startsWith('d'); return { name, isDirectory: () => isDir, isFile: () => !isDir }; }).filter(entry => entry.name !== '.' && entry.name !== '..'); } catch (error: any) { throw new Error(`Failed to read directory ${dirPath}: ${error.message}`); } } async function wslReadFile(filePath: string, encoding: string = 'utf-8'): Promise<string> { const wslPath = toWslPath(filePath); try { return await execWslCommand(`cat "${wslPath}"`); } catch (error: any) { throw new Error(`Failed to read file ${filePath}: ${error.message}`); } } async function wslWriteFile(filePath: string, content: string): Promise<void> { const wslPath = toWslPath(filePath); const tempFile = `/tmp/wsl_write_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; try { // 1. Créer un fichier temporaire vide await execWslCommand(`touch "${tempFile}"`); // 2. Encoder le contenu en base64 pour éviter les problèmes de caractères spéciaux const buffer = Buffer.from(content); const base64Content = buffer.toString('base64'); // 3. Écrire le contenu base64 par morceaux dans le fichier temporaire await writeBase64ContentByChunks(base64Content, tempFile); // 4. Déplacer le fichier temporaire vers la destination finale await execWslCommand(`mv "${tempFile}" "${wslPath}"`); } catch (error: any) { // En cas d'erreur, essayer de nettoyer le fichier temporaire try { await execWslCommand(`rm -f "${tempFile}"`); } catch (e) { // Ignorer les erreurs de nettoyage } throw new Error(`Failed to write to ${filePath}: ${error.message}`); } } // Nouvelle fonction pour écrire le contenu base64 par morceaux async function writeBase64ContentByChunks(base64Content: string, targetPath: string): Promise<void> { // Taille maximale sécurisée pour éviter les erreurs de ligne trop longue const maxChunkSize = 4096; // Si le contenu est petit, utilisez la méthode directe if (base64Content.length <= maxChunkSize) { await execWslCommand(`bash -c "echo '${base64Content}' | base64 -d > '${targetPath}'"`); return; } // Vider le fichier cible await execWslCommand(`truncate -s 0 '${targetPath}'`); // Traiter le contenu par morceaux for (let i = 0; i < base64Content.length; i += maxChunkSize) { const chunk = base64Content.substring(i, i + maxChunkSize); // Pour le premier morceau, utilisez > (écraser) // Pour les morceaux suivants, utilisez >> (ajouter) const redirectOperator = i === 0 ? '>' : '>>'; await execWslCommand(`bash -c "echo '${chunk}' | base64 -d ${redirectOperator} '${targetPath}'"`); } } async function wslMkdir(dirPath: string): Promise<void> { const wslPath = toWslPath(dirPath); try { await execWslCommand(`mkdir -p "${wslPath}"`); } catch (error: any) { throw new Error(`Failed to create directory ${dirPath}: ${error.message}`); } } async function wslRename(oldPath: string, newPath: string): Promise<void> { const wslOldPath = toWslPath(oldPath); const wslNewPath = toWslPath(newPath); try { await execWslCommand(`mv "${wslOldPath}" "${wslNewPath}"`); } catch (error: any) { throw new Error(`Failed to move ${oldPath} to ${newPath}: ${error.message}`); } } async function wslRealpath(filePath: string): Promise<string> { const wslPath = toWslPath(filePath); try { return await execWslCommand(`realpath "${wslPath}"`); } catch (error: any) { throw new Error(`Failed to resolve realpath for ${filePath}: ${error.message}`); } } // Validate that all directories exist and are accessible async function validateDirectories(): Promise<void> { for (const dir of pathArgs) { try { const expandedDir = expandHome(dir); const stats = await wslStat(expandedDir); if (!stats.isDirectory()) { console.error(`Error: ${dir} is not a directory`); process.exit(1); } } catch (error) { console.error(`Error accessing directory ${dir}:`, error); process.exit(1); } } } // Security utilities async function validatePath(requestedPath: string): Promise<string> { const expandedPath = expandHome(requestedPath); const absolute = isAbsolute(expandedPath) ? resolve(expandedPath) : resolve(process.cwd().replace(/\\/g, '/'), expandedPath); const normalizedRequested = normalizePath(absolute); // Check if path is within allowed directories const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir)); if (!isAllowed) { throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`); } // Handle symlinks by checking their real path try { const realPath = await wslRealpath(absolute); const normalizedReal = normalizePath(realPath); const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir)); if (!isRealPathAllowed) { throw new Error("Access denied - symlink target outside allowed directories"); } return realPath; } catch (error) { // For new files that don't exist yet, verify parent directory const parentDir = dirname(absolute); try { const realParentPath = await wslRealpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir)); if (!isParentAllowed) { throw new Error("Access denied - parent directory outside allowed directories"); } return absolute; } catch { throw new Error(`Parent directory does not exist: ${parentDir}`); } } } // Schema definitions const ReadFileArgsSchema = z.object({ path: z.string(), }); const ReadMultipleFilesArgsSchema = z.object({ paths: z.array(z.string()), }); const WriteFileArgsSchema = z.object({ path: z.string(), content: z.string(), }); const EditOperation = z.object({ oldText: z.string().describe('Text to search for - must match exactly'), newText: z.string().describe('Text to replace with') }); const EditFileArgsSchema = z.object({ path: z.string(), edits: z.array(EditOperation), dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format') }); const CreateDirectoryArgsSchema = z.object({ path: z.string(), }); const ListDirectoryArgsSchema = z.object({ path: z.string(), }); const DirectoryTreeArgsSchema = z.object({ path: z.string(), }); const MoveFileArgsSchema = z.object({ source: z.string(), destination: z.string(), }); const SearchFilesArgsSchema = z.object({ path: z.string(), pattern: z.string(), excludePatterns: z.array(z.string()).optional().default([]) }); const GetFileInfoArgsSchema = z.object({ path: z.string(), }); const ListWslDistrosArgsSchema = z.object({}); const ReadFileByPartsArgsSchema = z.object({ path: z.string(), part_number: z.number().int().positive().describe('Part number to read (1, 2, 3, etc.)') }); const SearchInFilesArgsSchema = z.object({ path: z.string(), pattern: z.string(), caseInsensitive: z.boolean().default(false).describe('Case insensitive search'), isRegex: z.boolean().default(false).describe('Treat pattern as regular expression'), includePatterns: z.array(z.string()).optional().default([]).describe('File patterns to include (e.g., *.js, *.ts)'), excludePatterns: z.array(z.string()).optional().default([]).describe('File patterns to exclude'), maxResults: z.number().int().positive().default(1000).describe('Maximum number of results to return'), contextLines: z.number().int().min(0).default(0).describe('Number of context lines before and after match') }); const ToolInputSchema = ToolSchema.shape.inputSchema; type ToolInput = z.infer<typeof ToolInputSchema>; // Server setup const server = new Server({ name: "secure-filesystem-server", version: "1.3.1", }, { capabilities: { tools: {}, }, }); // Tool implementations async function getFileStats(filePath: string): Promise<FileInfo> { const stats = await wslStat(filePath); return { size: stats.size, created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, isDirectory: stats.isDirectory(), isFile: stats.isFile(), permissions: stats.mode.toString(8).slice(-3), }; } async function searchFilesByName( rootPath: string, pattern: string, excludePatterns: string[] = [] ): Promise<string[]> { const wslRootPath = toWslPath(rootPath); const escapedPattern = pattern.replace(/"/g, '\\"'); // Construire une commande find plus robuste const command = [`find "${wslRootPath}" -type f`]; // Ajouter grep si pattern fourni if (pattern) { command.push(`(grep -i "${escapedPattern}" || true)`); } // Ajouter des filtres d'exclusion if (excludePatterns && excludePatterns.length > 0) { for (const ex of excludePatterns) { const excluded = ex.replace(/\*/g, ".*").replace(/"/g, '\\"'); command.push(`(grep -v "${excluded}" || true)`); } } try { const result = await execWslPipeline(command); return result ? result.split("\n").filter(line => line.trim() !== "") : []; } catch (error: any) { throw error; } } async function searchInFiles( rootPath: string, pattern: string, options: { caseInsensitive?: boolean; isRegex?: boolean; includePatterns?: string[]; excludePatterns?: string[]; maxResults?: number; contextLines?: number; } = {} ): Promise<string> { const wslRootPath = toWslPath(rootPath); // Construire la commande grep const grepOptions: string[] = []; // Options de base grepOptions.push('-n'); // Numéros de ligne grepOptions.push('-H'); // Toujours afficher le nom du fichier grepOptions.push('-r'); // Récursif // Options conditionnelles if (options.caseInsensitive) { grepOptions.push('-i'); } if (options.isRegex) { grepOptions.push('-E'); // Extended regex } else { grepOptions.push('-F'); // Fixed string } // Contexte if (options.contextLines && options.contextLines > 0) { grepOptions.push(`-C${options.contextLines}`); } // Limiter les résultats if (options.maxResults) { grepOptions.push(`-m${Math.ceil(options.maxResults / 10)}`); // Approximatif par fichier } // Patterns d'inclusion if (options.includePatterns && options.includePatterns.length > 0) { for (const pattern of options.includePatterns) { grepOptions.push(`--include=${pattern}`); } } // Patterns d'exclusion if (options.excludePatterns && options.excludePatterns.length > 0) { for (const pattern of options.excludePatterns) { grepOptions.push(`--exclude=${pattern}`); } } // Exclure les répertoires communs à ignorer grepOptions.push('--exclude-dir=.git'); grepOptions.push('--exclude-dir=node_modules'); grepOptions.push('--exclude-dir=.svn'); grepOptions.push('--exclude-dir=.hg'); try { // Encoder le pattern en base64 pour éviter tous problèmes d'échappement const base64Pattern = Buffer.from(pattern).toString('base64'); // Construire la commande qui passe le pattern via stdin const grepCommand = `bash -c "echo '${base64Pattern}' | base64 -d | grep ${grepOptions.join(' ')} -f - '${wslRootPath}' 2>&1 || true"`; const result = await execWslCommand(grepCommand); if (!result.trim()) { return "No matches found."; } // Filtrer les messages d'erreur grep courants const lines = result.trim().split('\n').filter(line => { // Ignorer les messages d'erreur courants de grep return !line.includes('grep: ') && !line.includes('Is a directory') && !line.includes('Permission denied') && !line.includes('No such file or directory'); }); if (lines.length === 0) { return "No matches found."; } // Limiter les résultats si nécessaire if (options.maxResults && lines.length > options.maxResults) { const truncated = lines.slice(0, options.maxResults); truncated.push(`\n... (${lines.length - options.maxResults} more results omitted)`); return truncated.join('\n'); } return lines.join('\n'); } catch (error: any) { throw new Error(`Failed to search in files: ${error.message}`); } } async function readFileByParts(filePath: string, partNumber: number): Promise<string> { const wslPath = toWslPath(filePath); const PART_SIZE = 95000; const MAX_BACKTRACK = 300; try { // Obtenir la taille du fichier const fileSizeStr = await execWslCommand(`bash -c "wc -c < '${wslPath}'"`); const fileSize = parseInt(fileSizeStr.trim()); // Calculer la position de début théorique const theoreticalStart = (partNumber - 1) * PART_SIZE; // Vérifier si la partie demandée existe if (theoreticalStart >= fileSize) { throw new Error(`File has only ${fileSize.toLocaleString()} characters. Part ${partNumber} does not exist.`); } let actualStart = theoreticalStart; // Pour la première partie, pas de recul nécessaire if (partNumber === 1) { const content = await execWslCommand(`head -c ${PART_SIZE} "${wslPath}"`); return content; } // Pour les autres parties, trouver le début de ligne précédent if (partNumber > 1) { const searchStart = Math.max(0, theoreticalStart - MAX_BACKTRACK); const searchLength = theoreticalStart - searchStart; if (searchLength > 0) { // Lire la zone de recherche et trouver le dernier \n const searchContent = await execWslCommand( `bash -c "tail -c +${searchStart + 1} '${wslPath}' | head -c ${searchLength}"` ); const lastNewlineIndex = searchContent.lastIndexOf('\n'); if (lastNewlineIndex !== -1) { actualStart = searchStart + lastNewlineIndex + 1; } } } // Lire le contenu depuis actualStart let content = await execWslCommand( `bash -c "tail -c +${actualStart + 1} '${wslPath}' | head -c ${PART_SIZE}"` ); // Pour les parties autres que la première, essayer de finir sur une ligne complète if (partNumber > 1 && content.length === PART_SIZE) { const endSearchStart = actualStart + PART_SIZE; if (endSearchStart < fileSize) { const remainingChars = Math.min(MAX_BACKTRACK, fileSize - endSearchStart); if (remainingChars > 0) { const endSearchContent = await execWslCommand( `bash -c "tail -c +${endSearchStart + 1} '${wslPath}' | head -c ${remainingChars}"` ); const firstNewlineIndex = endSearchContent.indexOf('\n'); if (firstNewlineIndex !== -1) { content += endSearchContent.substring(0, firstNewlineIndex + 1); } } } } return content; } catch (error: any) { if (error.message.includes('File has only')) { throw error; } throw new Error(`Failed to read file part ${partNumber} of ${filePath}: ${error.message}`); } } // file editing and diffing utilities function normalizeLineEndings(text: string): string { return text.replace(/\r\n/g, '\n'); } function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string { // Ensure consistent line endings for diff const normalizedOriginal = normalizeLineEndings(originalContent); const normalizedNew = normalizeLineEndings(newContent); return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified'); } async function applyFileEdits( filePath: string, edits: EditOperationType[], dryRun: boolean = false ): Promise<string> { // Read file content and normalize line endings const content = normalizeLineEndings(await wslReadFile(filePath, 'utf-8')); // Apply edits sequentially let modifiedContent = content; for (const edit of edits) { const normalizedOld = normalizeLineEndings(edit.oldText); const normalizedNew = normalizeLineEndings(edit.newText); // If exact match exists, use it if (modifiedContent.includes(normalizedOld)) { modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); continue; } // Otherwise, try line-by-line matching with flexibility for whitespace const oldLines = normalizedOld.split('\n'); const contentLines = modifiedContent.split('\n'); let matchFound = false; for (let i = 0; i <= contentLines.length - oldLines.length; i++) { const potentialMatch = contentLines.slice(i, i + oldLines.length); // Compare lines with normalized whitespace const isMatch = oldLines.every((oldLine: string, j: number) => { const contentLine = potentialMatch[j]; return oldLine.trim() === contentLine.trim(); }); if (isMatch) { // Preserve original indentation of first line const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; const newLines = normalizedNew.split('\n').map((line: string, j: number) => { if (j === 0) return originalIndent + line.trimStart(); // For subsequent lines, try to preserve relative indentation 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; break; } } if (!matchFound) { throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); } } // Create unified diff const diff = createUnifiedDiff(content, modifiedContent, filePath); // Format diff with appropriate number of backticks let numBackticks = 3; while (diff.includes('`'.repeat(numBackticks))) { numBackticks++; } const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; if (!dryRun) { await wslWriteFile(filePath, modifiedContent); } return formattedDiff; } // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "read_file", description: "Read the complete contents of a file from the file system. " + "Handles various text encodings and provides detailed error messages " + "if the file cannot be read. Use this tool when you need to examine " + "the contents of a single file. Only works within allowed directories.", inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput, }, { name: "read_file_by_parts", description: "Read a file in parts of approximately 95,000 characters. " + "Use this for large files that cannot be read in one go. " + "Part 1 reads the first 95,000 characters. " + "Subsequent parts start at boundaries that respect line breaks when possible. " + "If a requested part number exceeds the file size, an error is returned with the actual file size.", inputSchema: zodToJsonSchema(ReadFileByPartsArgsSchema) as ToolInput, }, { name: "read_multiple_files", description: "Read the contents of multiple files simultaneously. This is more " + "efficient than reading files one by one when you need to analyze " + "or compare multiple files. Each file's content is returned with its " + "path as a reference. Failed reads for individual files won't stop " + "the entire operation. Only works within allowed directories.", inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput, }, { name: "write_file", description: "Create a new file or completely overwrite an existing file with new content. " + "Use with caution as it will overwrite existing files without warning. " + "Handles text content with proper encoding. Only works within allowed directories.", inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput, }, { name: "edit_file", description: "Make line-based edits to a text file. Each edit replaces exact line sequences " + "with new content. Returns a git-style diff showing the changes made. " + "Only works within allowed directories.", inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput, }, { name: "create_directory", description: "Create a new directory or ensure a directory exists. Can create multiple " + "nested directories in one operation. If the directory already exists, " + "this operation will succeed silently. Perfect for setting up directory " + "structures for projects or ensuring required paths exist. Only works within allowed directories.", inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput, }, { name: "list_directory", description: "Get a detailed listing of all files and directories in a specified path. " + "Results clearly distinguish between files and directories with [FILE] and [DIR] " + "prefixes. This tool is essential for understanding directory structure and " + "finding specific files within a directory. Only works within allowed directories.", inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput, }, { name: "directory_tree", description: "Get a recursive tree view of files and directories as a JSON structure. " + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + "Files have no children array, while directories always have a children array (which may be empty). " + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput, }, { name: "move_file", description: "Move or rename files and directories. Can move files between directories " + "and rename them in a single operation. If the destination exists, the " + "operation will fail. Works across different directories and can be used " + "for simple renaming within the same directory. Both source and destination must be within allowed directories.", inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput, }, { name: "search_files_by_name", description: "Recursively search for files and directories matching a pattern. " + "Searches through all subdirectories from the starting path. The search " + "is case-insensitive and matches partial names. Returns full paths to all " + "matching items. Great for finding files when you don't know their exact location. " + "Only searches within allowed directories.", inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput, }, { name: "search_in_files", description: "Search for text patterns within files recursively. " + "Supports plain text and regular expression searches. Can filter by file patterns, " + "exclude certain files/directories, limit results, and show context lines. " + "Returns matching lines with file paths and line numbers. " + "Automatically excludes common directories like .git and node_modules. " + "Only searches within allowed directories.", inputSchema: zodToJsonSchema(SearchInFilesArgsSchema) as ToolInput, }, { name: "get_file_info", description: "Retrieve detailed metadata about a file or directory. Returns comprehensive " + "information including size, creation time, last modified time, permissions, " + "and type. This tool is perfect for understanding file characteristics " + "without reading the actual content. Only works within allowed directories.", inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput, }, { name: "list_allowed_directories", description: "Returns the list of directories that this server is allowed to access. " + "Use this to understand which directories are available before trying to access files.", inputSchema: { type: "object", properties: {}, required: [], } as ToolInput, }, { name: "list_wsl_distributions", description: "Lists all available WSL distributions and shows which one is currently being used.", inputSchema: zodToJsonSchema(ListWslDistrosArgsSchema) as ToolInput, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case "read_file": { const parsed = ReadFileArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for read_file: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const content = await wslReadFile(validPath); return { content: [{ type: "text", text: content }], }; } case "read_file_by_parts": { const parsed = ReadFileByPartsArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for read_file_by_parts: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const content = await readFileByParts(validPath, parsed.data.part_number); return { content: [{ type: "text", text: content }], }; } case "read_multiple_files": { const parsed = ReadMultipleFilesArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`); } const results = await Promise.all(parsed.data.paths.map(async (filePath: string) => { try { const validPath = await validatePath(filePath); const content = await wslReadFile(validPath); return `${filePath}:\n${content}\n`; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return `${filePath}: Error - ${errorMessage}`; } })); return { content: [{ type: "text", text: results.join("\n---\n") }], }; } case "write_file": { const parsed = WriteFileArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for write_file: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); await wslWriteFile(validPath, parsed.data.content); return { content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }], }; } case "edit_file": { const parsed = EditFileArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for edit_file: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun); return { content: [{ type: "text", text: result }], }; } case "create_directory": { const parsed = CreateDirectoryArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for create_directory: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); await wslMkdir(validPath); return { content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }], }; } case "list_directory": { const parsed = ListDirectoryArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for list_directory: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const entries = await wslReaddir(validPath); const formatted = entries .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`) .join("\n"); return { content: [{ type: "text", text: formatted }], }; } case "directory_tree": { const parsed = DirectoryTreeArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const wslPath = toWslPath(validPath); // Utiliser find pour obtenir tous les fichiers et répertoires en une seule commande try { // %y = type (d pour directory, f pour file) // %P = chemin relatif depuis le point de départ const findResult = await execWslCommand( `find "${wslPath}" -printf "%y %P\\n" | sort` ); if (!findResult.trim()) { return { content: [{ type: "text", text: JSON.stringify([], null, 2) }], }; } // Parser la sortie de find et reconstruire l'arbre const lines = findResult.trim().split('\n'); const tree: TreeEntry[] = []; const pathMap = new Map<string, TreeEntry>(); // Première ligne devrait être le répertoire racine lui-même (chemin vide) // On la saute car on veut le contenu du répertoire, pas le répertoire lui-même const startIndex = lines[0].trim() === 'd ' ? 1 : 0; for (let i = startIndex; i < lines.length; i++) { const line = lines[i].trim(); const [type, ...pathParts] = line.split(' '); const relativePath = pathParts.join(' '); if (!relativePath) continue; // Ignorer les lignes vides const parts = relativePath.split('/'); const name = parts[parts.length - 1]; const parentPath = parts.slice(0, -1).join('/'); const entry: TreeEntry = { name, type: type === 'd' ? 'directory' : 'file' }; if (type === 'd') { entry.children = []; } pathMap.set(relativePath, entry); if (parentPath) { // Ajouter à son parent const parent = pathMap.get(parentPath); if (parent && parent.children) { parent.children.push(entry); } } else { // Entrée de premier niveau tree.push(entry); } } return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }], }; } catch (error: any) { throw new Error(`Failed to get directory tree for ${parsed.data.path}: ${error.message}`); } } case "move_file": { const parsed = MoveFileArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for move_file: ${parsed.error}`); } const validSourcePath = await validatePath(parsed.data.source); const validDestPath = await validatePath(parsed.data.destination); await wslRename(validSourcePath, validDestPath); return { content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }], }; } case "search_files_by_name": { const parsed = SearchFilesArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for search_files_by_name: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const results = await searchFilesByName(validPath, parsed.data.pattern, parsed.data.excludePatterns || []); return { content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }], }; } case "search_in_files": { const parsed = SearchInFilesArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for search_in_files: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const result = await searchInFiles(validPath, parsed.data.pattern, { caseInsensitive: parsed.data.caseInsensitive, isRegex: parsed.data.isRegex, includePatterns: parsed.data.includePatterns, excludePatterns: parsed.data.excludePatterns, maxResults: parsed.data.maxResults, contextLines: parsed.data.contextLines }); return { content: [{ type: "text", text: result }], }; } case "get_file_info": { const parsed = GetFileInfoArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const info = await getFileStats(validPath); return { content: [{ type: "text", text: Object.entries(info) .map(([key, value]) => `${key}: ${value}`) .join("\n") }], }; } case "list_allowed_directories": { return { content: [{ type: "text", text: `Allowed directories:\n${allowedDirectories.join('\n')}` }], }; } case "list_wsl_distributions": { const distributions = await listWslDistributions(); const formattedList = distributions.map(d => { const isActive = allowedDistro && d.name.toLowerCase() === allowedDistro.toLowerCase() ? " (ACTIVE)" : d.name.includes("(Default)") ? " (DEFAULT)" : ""; return `${d.name}${isActive} - State: ${d.state}, Version: ${d.version}`; }).join('\n'); return { content: [{ type: "text", text: `Available WSL Distributions:\n${formattedList}\n\nCurrently using: ${allowedDistro}` }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } }); // Start server async function runServer() { await initializeWslAndDirectories(); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Secure MCP WSL Filesystem Server running on stdio"); console.error(`Using WSL distribution: ${allowedDistro}`); console.error("Allowed directories:", allowedDirectories); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });

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/webconsulting/mcp-server-wsl-filesystem'

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