#
tokens: 18369/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .npmignore
├── index.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
1 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | dist
3 | .npm
4 | *.tgz
5 | Notes*
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | > ⚠️ **IMPORTANT INFORMATION:**  
  2 | > The original [Filesystem MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) can already access WSL files by simply using the network path `\\wsl.localhost\DistributionName` as a parameter in the configuration.  
  3 | > Example:
  4 | > 
  5 | > ```json
  6 | > {
  7 | >   "mcpServers": {
  8 | >     "filesystem": {
  9 | >       "command": "npx",
 10 | >       "args": [
 11 | >         "-y",
 12 | >         "@modelcontextprotocol/server-filesystem",
 13 | >         "\\\\wsl.localhost\\Debian",
 14 | >         "C:\\path\\to\\other\\allowed\\dir"
 15 | >       ]
 16 | >     }
 17 | >   }
 18 | > }
 19 | > ```
 20 | >
 21 | > However, this project offers an **alternative implementation specifically optimized for WSL Linux distributions**.
 22 | >
 23 | > While the official server works by recursively walking directories using Node.js’s `fs` module, this implementation leverages **native Linux commands inside WSL** (such as `find`, `grep`, etc.), making **file listing and content search operations significantly faster**.
 24 | >
 25 | > This can be especially useful when dealing with large directory trees or when search performance is critical.
 26 | >
 27 | > So while the native network path may be simpler for many use cases, this project remains **a valuable solution** for WSL users looking for **better performance** or more **custom control** over the indexing and searching logic.
 28 | 
 29 | ---
 30 | 
 31 | # Filesystem MCP Server for WSL
 32 | 
 33 | [![npm version](https://img.shields.io/npm/v/mcp-server-wsl-filesystem.svg)](https://www.npmjs.com/package/mcp-server-wsl-filesystem)
 34 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 35 | 
 36 | Node.js server implementing the Model Context Protocol (MCP), specifically designed for filesystem operations in Windows Subsystem for Linux (WSL).  
 37 | This project is a fork of the original [Filesystem MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) but completely reimagined for WSL environments.  
 38 | Unlike the original project, which handles generic file operations, this version focuses exclusively on seamless interaction between Windows and Linux distributions under WSL.  
 39 | Both projects are compatible and can run in parallel on the same system.
 40 | 
 41 | ## Features
 42 | 
 43 | - Access any WSL distribution from Windows
 44 | - Read/write files in WSL from Windows host
 45 | - Create/list/delete directories in WSL
 46 | - Move files/directories across WSL filesystem
 47 | - Search files within WSL 
 48 | - Get file metadata from the WSL filesystem
 49 | - Support for multiple WSL distributions
 50 | 
 51 | **Note**: The server only allows operations within directories specified via `args`.
 52 | 
 53 | ---
 54 | 
 55 | ## API
 56 | 
 57 | ### Resources
 58 | 
 59 | - `wsl -d <distrib>`: Command for operations on WSL distributions
 60 | 
 61 | ### Tools
 62 | 
 63 | - **read_file**
 64 |   - Read complete contents of a file from WSL
 65 |   - Input: `path` (string)
 66 |   - Reads content as UTF-8 text
 67 | 
 68 | - **read_file_by_parts**
 69 |   - Read large files in parts of approximately 95,000 characters
 70 |   - Inputs:
 71 |     - `path` (string)
 72 |     - `part_number` (positive integer: 1, 2, 3, etc.)
 73 |   - Features:
 74 |     - Part 1 starts from the beginning of the file
 75 |     - Subsequent parts align to line boundaries (max 300 character adjustment)
 76 |     - Returns error with actual file size if requested part doesn't exist
 77 |     - Useful for files too large to read in one operation
 78 | 
 79 | - **read_multiple_files**
 80 |   - Read multiple files simultaneously from WSL
 81 |   - Input: `paths` (string[])
 82 |   - Failed reads won't stop the entire operation
 83 | 
 84 | - **write_file**
 85 |   - Create or overwrite a file in WSL (use with caution)
 86 |   - Inputs:
 87 |     - `path` (string)
 88 |     - `content` (string)
 89 | 
 90 | - **edit_file**
 91 |   - Selective edits with advanced pattern matching and formatting
 92 |   - Inputs:
 93 |     - `path` (string)
 94 |     - `edits` (array of `{ oldText, newText }`)
 95 |     - `dryRun` (boolean, optional)
 96 |   - Features:
 97 |     - Multi-line matching
 98 |     - Indentation preservation
 99 |     - Git-style diff preview
100 |     - Non-destructive dry run mode
101 | 
102 | - **create_directory**
103 |   - Create or ensure the existence of a directory in WSL
104 |   - Input: `path` (string)
105 | 
106 | - **list_directory**
107 |   - List directory contents with `[FILE]` or `[DIR]` prefixes
108 |   - Input: `path` (string)
109 | 
110 | - **directory_tree**
111 |   - Recursive JSON tree view of contents
112 |   - Input: `path` (string)
113 | 
114 | - **move_file**
115 |   - Move or rename files/directories
116 |   - Inputs:
117 |     - `source` (string)
118 |     - `destination` (string)
119 | 
120 | - **search_files**
121 |   - Recursively search by name
122 |   - Inputs:
123 |     - `path` (string)
124 |     - `pattern` (string)
125 |     - `excludePatterns` (string[], optional)
126 | 
127 | - **search_in_files**
128 |   - Search for text patterns within files recursively
129 |   - Inputs:
130 |     - `path` (string) - root directory to search
131 |     - `pattern` (string) - text or regex pattern to find
132 |     - `caseInsensitive` (boolean, optional) - case-insensitive search
133 |     - `isRegex` (boolean, optional) - treat pattern as regex
134 |     - `includePatterns` (string[], optional) - file patterns to include (e.g., *.js)
135 |     - `excludePatterns` (string[], optional) - file patterns to exclude
136 |     - `maxResults` (number, optional, default: 1000) - maximum results to return
137 |     - `contextLines` (number, optional, default: 0) - lines of context before/after
138 |   - Features:
139 |     - Handles all special characters (apostrophes, quotes, $, backslashes)
140 |     - Supports plain text and regular expression searches
141 |     - Shows matching lines with file paths and line numbers
142 |     - Automatically excludes .git, node_modules, .svn, .hg directories
143 |     - Can show context lines around matches
144 | 
145 | - **get_file_info**
146 |   - Detailed metadata
147 |   - Input: `path` (string)
148 |   - Returns: size, timestamps, type, permissions
149 | 
150 | - **list_allowed_directories**
151 |   - Lists all directories accessible to the server
152 | 
153 | - **list_wsl_distributions**
154 |   - Lists available distributions and shows the active one
155 | 
156 | ---
157 | 
158 | ## Requirements
159 | 
160 | - [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install) properly configured
161 | - At least one Linux distribution installed in WSL
162 | 
163 | **For Claude Desktop users:**  
164 | No additional installation required — just configure your `claude_desktop_config.json`.
165 | 
166 | **NPM Package:**  
167 | The package is available on npm: [mcp-server-wsl-filesystem](https://www.npmjs.com/package/mcp-server-wsl-filesystem)
168 | 
169 | **For development:**
170 | 
171 | - [Node.js](https://nodejs.org/en/download/) (v18.0.0 or higher)
172 | - TypeScript (included as a dev dependency)
173 | 
174 | ### Installing Node.js on Windows
175 | 
176 | 1. Download the installer from [nodejs.org](https://nodejs.org/en/download/)
177 | 2. Run it and follow the instructions
178 | 3. Check versions:
179 | 
180 | ```bash
181 | node --version
182 | npm --version
183 | ```
184 | 
185 | ## Usage
186 | 
187 | Before running the server, you need to build the TypeScript project:
188 | ```bash
189 | npm install
190 | npm run build
191 | ```
192 | 
193 | Run the server by specifying which WSL distribution to use (optional) and which directories to expose:
194 | 
195 | ```bash
196 | node dist/index.js [--distro=distribution_name] <allowed_directory> [additional_directories...]
197 | ```
198 | 
199 | If no distribution is specified, the default WSL distribution will be used.
200 | 
201 | ### Examples
202 | 
203 | Access Ubuntu-20.04 distribution:
204 | ```bash
205 | node dist/index.js --distro=Ubuntu-20.04 /home/user/documents
206 | ```
207 | 
208 | Use default distribution:
209 | ```bash
210 | node dist/index.js /home/user/documents
211 | ```
212 | 
213 | ## Usage with Claude Desktop
214 | 
215 | Add this to your `claude_desktop_config.json`:
216 | 
217 | ### Option 1: Using a specific WSL distribution
218 | 
219 | ```json
220 | {
221 |   "mcpServers": {
222 |     "wsl-filesystem": {
223 |       "command": "npx",
224 |       "args": [
225 |         "-y",
226 |         "mcp-server-wsl-filesystem",
227 |         "--distro=Ubuntu-20.04",
228 |         "/home/user/documents"
229 |       ]
230 |     }
231 |   }
232 | }
233 | ```
234 | 
235 | ### Option 2: Using the default WSL distribution
236 | 
237 | ```json
238 | {
239 |   "mcpServers": {
240 |     "wsl-filesystem": {
241 |       "command": "npx",
242 |       "args": [
243 |         "-y",
244 |         "mcp-server-wsl-filesystem",
245 |         "/home/user/documents"
246 |       ]
247 |     }
248 |   }
249 | }
250 | ```
251 | 
252 | In the second example, the system will use your default WSL distribution without you needing to specify it.
253 | 
254 | ## Differences from original project
255 | 
256 | This fork adapts the original Filesystem MCP Server to work with WSL by:
257 | 
258 | 1. Replacing direct Node.js filesystem calls with WSL command executions
259 | 2. Adding support for selecting specific WSL distributions
260 | 3. Implementing path translation between Windows and Linux formats
261 | 4. Enhancing file content handling for cross-platform compatibility
262 | 5. Adding specialized tools for WSL management
263 | 
264 | ## License
265 | 
266 | This project is a fork of the original [Filesystem MCP Server](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem) created by the Model Context Protocol team.
267 | 
268 | This MCP server for WSL is licensed under the MIT License, following the original project's license. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the original project repository.
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "compilerOptions": {
 3 |       "target": "ES2022",
 4 |       "strict": true,
 5 |       "esModuleInterop": true,
 6 |       "skipLibCheck": true,
 7 |       "forceConsistentCasingInFileNames": true,
 8 |       "resolveJsonModule": true,
 9 |       "outDir": "./dist",
10 |       "rootDir": ".",
11 |       "moduleResolution": "NodeNext",
12 |       "module": "NodeNext"
13 |     },
14 |     "include": ["./**/*.ts"],
15 |     "exclude": ["node_modules"]
16 |   }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "mcp-server-wsl-filesystem",
 3 |   "version": "1.3.1",
 4 |   "description": "MCP server for wsl filesystem access",
 5 |   "license": "MIT",
 6 |   "author": "Web-C",
 7 |   "type": "module",
 8 |   "bin": {
 9 |     "wsl-filesystem": "dist/index.js"
10 |   },
11 |   "files": [
12 |     "dist"
13 |   ],
14 |   "scripts": {
15 |     "build": "tsc",
16 |     "prepare": "npm run build",
17 |     "watch": "tsc --watch"
18 |   },
19 |   "dependencies": {
20 |     "@modelcontextprotocol/sdk": "^1.13.2",
21 |     "diff": "^8.0.2",
22 |     "zod-to-json-schema": "^3.24.6"
23 |   },
24 |   "devDependencies": {
25 |     "@types/diff": "^5.0.9",
26 |     "@types/node": "^22",
27 |     "typescript": "^5.3.3"
28 |   },
29 |   "main": "index.js",
30 |   "repository": {
31 |     "type": "git",
32 |     "url": "git+https://github.com/webconsulting/mcp-server-wsl-filesystem.git"
33 |   },
34 |   "keywords": [
35 |     "mcp", "wsl", "filesystem"
36 |   ],
37 |   "bugs": {
38 |     "url": "https://github.com/webconsulting/mcp-server-wsl-filesystem/issues"
39 |   },
40 |   "homepage": "https://github.com/webconsulting/mcp-server-wsl-filesystem#readme",
41 |   "engines": {
42 |     "node": ">=18"
43 |   }
44 | }
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | #!/usr/bin/env node
   2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
   3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   4 | import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js";
   5 | import { exec } from 'child_process';
   6 | import { promisify } from 'util';
   7 | import { z } from "zod";
   8 | import { zodToJsonSchema } from "zod-to-json-schema";
   9 | import { createTwoFilesPatch } from 'diff';
  10 | 
  11 | // Définition d'interfaces pour les types utilisés dans le script
  12 | interface WslDistribution {
  13 |   name: string;
  14 |   state: string;
  15 |   version: string;
  16 |   isDefault: boolean;
  17 | }
  18 | 
  19 | interface WslFileStats {
  20 |   size: number;
  21 |   birthtime: Date;
  22 |   mtime: Date;
  23 |   atime: Date;
  24 |   mode: number;
  25 |   isDirectory: () => boolean;
  26 |   isFile: () => boolean;
  27 | }
  28 | 
  29 | interface FileEntry {
  30 |   name: string;
  31 |   isDirectory: () => boolean;
  32 |   isFile: () => boolean;
  33 | }
  34 | 
  35 | interface TreeEntry {
  36 |   name: string;
  37 |   type: 'file' | 'directory';
  38 |   children?: TreeEntry[];
  39 | }
  40 | 
  41 | interface FileInfo {
  42 |   size: number;
  43 |   created: Date;
  44 |   modified: Date;
  45 |   accessed: Date;
  46 |   isDirectory: boolean;
  47 |   isFile: boolean;
  48 |   permissions: string;
  49 | }
  50 | 
  51 | interface EditOperationType {
  52 |   oldText: string;
  53 |   newText: string;
  54 | }
  55 | 
  56 | // Promisify exec pour utiliser async/await
  57 | const execAsync = promisify(exec);
  58 | 
  59 | // Command line argument parsing
  60 | const args = process.argv.slice(2);
  61 | const distroArg = args.find(arg => arg.startsWith('--distro='));
  62 | let allowedDistro: string | null = distroArg ? distroArg.split('=')[1] : null;
  63 | const pathArgs = args.filter(arg => !arg.startsWith('--'));
  64 | 
  65 | if (pathArgs.length === 0) {
  66 |   console.error("Usage: mcp-server-wsl-filesystem [--distro=name] <allowed-directory> [additional-directories...]");
  67 |   process.exit(1);
  68 | }
  69 | 
  70 | // Fonctions utilitaires pour manipuler les chemins en respectant les conventions Linux
  71 | function normalizePath(p: string): string {
  72 |   // Remplacer les chemins Windows par des chemins Linux
  73 |   p = p.replace(/\\/g, '/');
  74 |   // Gérer les doubles barres obliques
  75 |   p = p.replace(/\/+/g, '/');
  76 |   // Supprimer les points simples
  77 |   const parts = p.split('/');
  78 |   const result: string[] = [];
  79 |   for (let i = 0; i < parts.length; i++) {
  80 |     const part = parts[i];
  81 |     if (part === '.')
  82 |       continue;
  83 |     if (part === '..') {
  84 |       if (result.length > 0 && result[result.length - 1] !== '..') {
  85 |         result.pop();
  86 |       }
  87 |       else {
  88 |         result.push('..');
  89 |       }
  90 |     }
  91 |     else if (part !== '') {
  92 |       result.push(part);
  93 |     }
  94 |   }
  95 |   let normalized = result.join('/');
  96 |   if (p.startsWith('/'))
  97 |     normalized = '/' + normalized;
  98 |   if (p.endsWith('/') && normalized !== '/')
  99 |     normalized += '/';
 100 |   return normalized || '.';
 101 | }
 102 | 
 103 | function expandHome(filepath: string): string {
 104 |   if (filepath.startsWith('~/') || filepath === '~') {
 105 |     // Dans WSL, on peut obtenir le home directory avec $HOME
 106 |     return `$HOME${filepath.slice(1)}`;
 107 |   }
 108 |   return filepath;
 109 | }
 110 | 
 111 | function isAbsolute(p: string): boolean {
 112 |   return p.startsWith('/');
 113 | }
 114 | 
 115 | function resolve(...paths: string[]): string {
 116 |   let resolvedPath = '';
 117 |   for (let i = 0; i < paths.length; i++) {
 118 |     const path = paths[i];
 119 |     if (isAbsolute(path)) {
 120 |       resolvedPath = path;
 121 |     }
 122 |     else {
 123 |       if (!resolvedPath)
 124 |         resolvedPath = process.cwd().replace(/\\/g, '/');
 125 |       resolvedPath = `${resolvedPath}/${path}`;
 126 |     }
 127 |   }
 128 |   return normalizePath(resolvedPath);
 129 | }
 130 | 
 131 | function dirname(p: string): string {
 132 |   const normalized = normalizePath(p);
 133 |   if (normalized === '/')
 134 |     return '/';
 135 |   if (!normalized.includes('/'))
 136 |     return '.';
 137 |   const lastSlashIndex = normalized.lastIndexOf('/');
 138 |   if (lastSlashIndex === 0)
 139 |     return '/';
 140 |   if (lastSlashIndex === -1)
 141 |     return '.';
 142 |   return normalized.slice(0, lastSlashIndex);
 143 | }
 144 | 
 145 | function join(...paths: string[]): string {
 146 |   return normalizePath(paths.join('/'));
 147 | }
 148 | 
 149 | function isUtf16(str:string) {
 150 |   return str.indexOf('\0') !== -1;
 151 | }
 152 | 
 153 | function processOutput(output:string) {
 154 |   let lines;
 155 |   
 156 |   if (isUtf16(output)) {
 157 |     // Pour UTF-16
 158 |     const buffer = Buffer.from(output);
 159 |     const text = buffer.toString('utf16le');
 160 |     lines = text.trim().split('\n').slice(1);
 161 |   } else {
 162 |     // Pour UTF-8
 163 |     lines = output.toString().trim().split('\n').slice(1);
 164 |   }
 165 |   
 166 |   // Nettoyer les caractères restants comme les retours chariot
 167 |   return lines.map(line => line.replace(/\r/g, '').trim());
 168 | }
 169 | 
 170 | // Fonctions pour gérer les distributions WSL
 171 | async function listWslDistributions(): Promise<WslDistribution[]> {
 172 |   try {
 173 |       const { stdout } = await execAsync('wsl --list --verbose');
 174 |       const lines = processOutput(stdout);
 175 | 
 176 |       return lines.map(line => {
 177 |           // Gestion de l'astérisque pour la distribution par défaut
 178 |           const isDefault = line.trim().startsWith('*');
 179 |           // Supprimer l'astérisque si présent et diviser par espaces
 180 |           const parts = line.trim().replace(/^\*\s*/, '').split(/\s+/);
 181 |           
 182 |           const name = parts[0];
 183 |           const state = parts[1];
 184 |           const version = parts[2];
 185 |           
 186 |           return { 
 187 |               name, 
 188 |               state, 
 189 |               version,
 190 |               isDefault
 191 |           };
 192 |       });
 193 |   } catch (error) {
 194 |       console.error("Erreur lors de la récupération des distributions WSL:", error);
 195 |       return [];
 196 |   }
 197 | }
 198 | 
 199 | // Vérifier les distributions disponibles et définir la distribution par défaut si nécessaire
 200 | async function setupWslDistribution(): Promise<string> {
 201 |   const distributions = await listWslDistributions();
 202 | 
 203 |   if (distributions.length === 0) {
 204 |     console.error("Aucune distribution WSL n'a été trouvée sur ce système.");
 205 |     process.exit(1);
 206 |   }
 207 | 
 208 |   // Si aucune distribution spécifique n'est demandée, utiliser la distribution par défaut
 209 |   if (!allowedDistro) {
 210 |     const defaultDistro = distributions.find(d => d.isDefault);
 211 |     if (defaultDistro) {
 212 |         allowedDistro = defaultDistro.name;
 213 |     } else {
 214 |         // Si aucune distribution par défaut n'est marquée, utiliser la première
 215 |         allowedDistro = distributions[0].name;
 216 |     }
 217 |   } else {
 218 |     // Vérifier si la distribution demandée existe
 219 |     const exists = distributions.some(d => d.name.toLowerCase() === allowedDistro!.toLowerCase());
 220 |     if (!exists) {
 221 |       console.error(`La distribution WSL '${allowedDistro}' n'existe pas. Distributions disponibles:`);
 222 |       distributions.forEach(d => console.error(`- ${d.name}`));
 223 |       process.exit(1);
 224 |     }
 225 |   }
 226 | 
 227 |   console.error(`Utilisation de la distribution WSL: ${allowedDistro}`);
 228 |   return allowedDistro!;
 229 | }
 230 | 
 231 | let allowedDirectories: string[] = [];
 232 | 
 233 | // Initialiser la distribution WSL et attendre qu'elle soit configurée
 234 | async function initializeWslAndDirectories(): Promise<void> {
 235 |   try {
 236 |     await setupWslDistribution();
 237 | 
 238 |     allowedDirectories = pathArgs.map(dir => normalizePath(resolve(expandHome(dir))));
 239 | 
 240 |     await validateDirectories();
 241 |   } catch (error) {
 242 |     console.error("Erreur lors de l'initialisation:", error);
 243 |     process.exit(1);
 244 |   }
 245 | }
 246 | 
 247 | /**
 248 |  * Exécute une commande unique dans WSL
 249 |  */
 250 | async function execWslCommand(command: string): Promise<string> {
 251 |   try {
 252 |     const wslCommand = allowedDistro ? `wsl -d ${allowedDistro} ${command}` : `wsl ${command}`;
 253 |     const { stdout } = await execAsync(wslCommand);
 254 |     return stdout.trim();
 255 |   }
 256 |   catch (error: any) {
 257 |     throw new Error(`WSL command failed: ${error.message}`);
 258 |   }
 259 | }
 260 | 
 261 | /**
 262 |  * Exécute une chaîne de commandes avec des pipes dans WSL
 263 |  * Chaque élément du tableau sera préfixé avec wsl [options] avant d'être joint par des pipes
 264 |  */
 265 | async function execWslPipeline(commands: string[]): Promise<string> {
 266 |   try {
 267 |     if (commands.length === 0) {
 268 |       throw new Error("No commands provided");
 269 |     }
 270 |     
 271 |     const joined = commands.join(" | ").replace(/"/g, '\\"');
 272 |     const fullCommand = allowedDistro
 273 |       ? `wsl -d ${allowedDistro} sh -c "${joined}"`
 274 |       : `wsl sh -c "${joined}"`;
 275 | 
 276 |     const { stdout } = await execAsync(fullCommand);
 277 |     return stdout.trim();
 278 |   }
 279 |   catch (error: any) {
 280 |     throw new Error(`WSL pipeline failed: ${error.message}`);
 281 |   }
 282 | }
 283 | 
 284 | // Convertir un chemin Windows en chemin WSL
 285 | function toWslPath(windowsPath: string): string {
 286 |   // Nettoyer et normaliser le chemin pour WSL
 287 |   const normalizedPath = normalizePath(windowsPath);
 288 |   // Échapper les caractères spéciaux pour la ligne de commande
 289 |   return normalizedPath.replace(/(["\s'$`\\])/g, '\\$1');
 290 | }
 291 | 
 292 | // Fonctions d'utilitaire pour les opérations de fichier via WSL
 293 | async function wslStat(filePath: string): Promise<WslFileStats> {
 294 |   const wslPath = toWslPath(filePath);
 295 |   try {
 296 |     const result = await execWslCommand(`stat -c "%s %Y %X %W %a %F" "${wslPath}"`);
 297 |     const [size, mtime, atime, birthtime, permissions, type] = result.split(' ');
 298 |     return {
 299 |       size: parseInt(size),
 300 |       birthtime: new Date(parseInt(birthtime) * 1000),
 301 |       mtime: new Date(parseInt(mtime) * 1000),
 302 |       atime: new Date(parseInt(atime) * 1000),
 303 |       mode: parseInt(permissions, 8),
 304 |       isDirectory: () => type.includes('directory'),
 305 |       isFile: () => type.includes('regular file')
 306 |     };
 307 |   } catch (error: any) {
 308 |     throw new Error(`Failed to stat ${filePath}: ${error.message}`);
 309 |   }
 310 | }
 311 | 
 312 | async function wslReaddir(dirPath: string): Promise<FileEntry[]> {
 313 |   const wslPath = toWslPath(dirPath);
 314 |   try {
 315 |     // Utiliser ls -la, mais filtrer . et .. (d'où le tail -n +3), et utiliser le bon format de sortie
 316 |     const result = await execWslCommand(
 317 |       `sh -c "ls -la \\"${wslPath}\\" | tail -n +3"`
 318 |     );
 319 |     if (!result)
 320 |       return [];
 321 | 
 322 |     return result.split('\n').map(line => {
 323 |       // Format typique: "drwxr-xr-x 2 user group 4096 Jan 1 12:34 dirname"
 324 |       const parts = line.trim().split(/\s+/);
 325 |       // Le nom peut contenir des espaces, donc nous prenons tout à partir de la 9ème colonne
 326 |       const name = parts.slice(8).join(' ');
 327 |       const isDir = line.startsWith('d');
 328 | 
 329 |       return {
 330 |         name,
 331 |         isDirectory: () => isDir,
 332 |         isFile: () => !isDir
 333 |       };
 334 |     }).filter(entry => entry.name !== '.' && entry.name !== '..');
 335 |   } catch (error: any) {
 336 |     throw new Error(`Failed to read directory ${dirPath}: ${error.message}`);
 337 |   }
 338 | }
 339 | 
 340 | async function wslReadFile(filePath: string, encoding: string = 'utf-8'): Promise<string> {
 341 |   const wslPath = toWslPath(filePath);
 342 |   try {
 343 |     return await execWslCommand(`cat "${wslPath}"`);
 344 |   } catch (error: any) {
 345 |     throw new Error(`Failed to read file ${filePath}: ${error.message}`);
 346 |   }
 347 | }
 348 | 
 349 | async function wslWriteFile(filePath: string, content: string): Promise<void> {
 350 |   const wslPath = toWslPath(filePath);
 351 |   const tempFile = `/tmp/wsl_write_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
 352 | 
 353 |   try {
 354 |     // 1. Créer un fichier temporaire vide
 355 |     await execWslCommand(`touch "${tempFile}"`);
 356 | 
 357 |     // 2. Encoder le contenu en base64 pour éviter les problèmes de caractères spéciaux
 358 |     const buffer = Buffer.from(content);
 359 |     const base64Content = buffer.toString('base64');
 360 | 
 361 |     // 3. Écrire le contenu base64 par morceaux dans le fichier temporaire
 362 |     await writeBase64ContentByChunks(base64Content, tempFile);
 363 | 
 364 |     // 4. Déplacer le fichier temporaire vers la destination finale
 365 |     await execWslCommand(`mv "${tempFile}" "${wslPath}"`);
 366 |   } catch (error: any) {
 367 |     // En cas d'erreur, essayer de nettoyer le fichier temporaire
 368 |     try {
 369 |       await execWslCommand(`rm -f "${tempFile}"`);
 370 |     } catch (e) {
 371 |       // Ignorer les erreurs de nettoyage
 372 |     }
 373 |     throw new Error(`Failed to write to ${filePath}: ${error.message}`);
 374 |   }
 375 | }
 376 | 
 377 | // Nouvelle fonction pour écrire le contenu base64 par morceaux
 378 | async function writeBase64ContentByChunks(base64Content: string, targetPath: string): Promise<void> {
 379 |   // Taille maximale sécurisée pour éviter les erreurs de ligne trop longue
 380 |   const maxChunkSize = 4096;
 381 |   
 382 |   // Si le contenu est petit, utilisez la méthode directe
 383 |   if (base64Content.length <= maxChunkSize) {
 384 |     await execWslCommand(`bash -c "echo '${base64Content}' | base64 -d > '${targetPath}'"`);
 385 |     return;
 386 |   }
 387 |   
 388 |   // Vider le fichier cible
 389 |   await execWslCommand(`truncate -s 0 '${targetPath}'`);
 390 |   
 391 |   // Traiter le contenu par morceaux
 392 |   for (let i = 0; i < base64Content.length; i += maxChunkSize) {
 393 |     const chunk = base64Content.substring(i, i + maxChunkSize);
 394 |     // Pour le premier morceau, utilisez > (écraser)
 395 |     // Pour les morceaux suivants, utilisez >> (ajouter)
 396 |     const redirectOperator = i === 0 ? '>' : '>>';
 397 |     await execWslCommand(`bash -c "echo '${chunk}' | base64 -d ${redirectOperator} '${targetPath}'"`);
 398 |   }
 399 | }
 400 | 
 401 | async function wslMkdir(dirPath: string): Promise<void> {
 402 |   const wslPath = toWslPath(dirPath);
 403 |   try {
 404 |     await execWslCommand(`mkdir -p "${wslPath}"`);
 405 |   } catch (error: any) {
 406 |     throw new Error(`Failed to create directory ${dirPath}: ${error.message}`);
 407 |   }
 408 | }
 409 | 
 410 | async function wslRename(oldPath: string, newPath: string): Promise<void> {
 411 |   const wslOldPath = toWslPath(oldPath);
 412 |   const wslNewPath = toWslPath(newPath);
 413 |   try {
 414 |     await execWslCommand(`mv "${wslOldPath}" "${wslNewPath}"`);
 415 |   } catch (error: any) {
 416 |     throw new Error(`Failed to move ${oldPath} to ${newPath}: ${error.message}`);
 417 |   }
 418 | }
 419 | 
 420 | async function wslRealpath(filePath: string): Promise<string> {
 421 |   const wslPath = toWslPath(filePath);
 422 |   try {
 423 |     return await execWslCommand(`realpath "${wslPath}"`);
 424 |   } catch (error: any) {
 425 |     throw new Error(`Failed to resolve realpath for ${filePath}: ${error.message}`);
 426 |   }
 427 | }
 428 | 
 429 | // Validate that all directories exist and are accessible
 430 | async function validateDirectories(): Promise<void> {
 431 |   for (const dir of pathArgs) {
 432 |     try {
 433 |       const expandedDir = expandHome(dir);
 434 |       const stats = await wslStat(expandedDir);
 435 |       if (!stats.isDirectory()) {
 436 |         console.error(`Error: ${dir} is not a directory`);
 437 |         process.exit(1);
 438 |       }
 439 |     }
 440 |     catch (error) {
 441 |       console.error(`Error accessing directory ${dir}:`, error);
 442 |       process.exit(1);
 443 |     }
 444 |   }
 445 | }
 446 | 
 447 | 
 448 | // Security utilities
 449 | async function validatePath(requestedPath: string): Promise<string> {
 450 |   const expandedPath = expandHome(requestedPath);
 451 |   const absolute = isAbsolute(expandedPath)
 452 |     ? resolve(expandedPath)
 453 |     : resolve(process.cwd().replace(/\\/g, '/'), expandedPath);
 454 | 
 455 |   const normalizedRequested = normalizePath(absolute);
 456 | 
 457 |   // Check if path is within allowed directories
 458 |   const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir));
 459 |   if (!isAllowed) {
 460 |     throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
 461 |   }
 462 | 
 463 |   // Handle symlinks by checking their real path
 464 |   try {
 465 |     const realPath = await wslRealpath(absolute);
 466 |     const normalizedReal = normalizePath(realPath);
 467 |     const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir));
 468 |     if (!isRealPathAllowed) {
 469 |       throw new Error("Access denied - symlink target outside allowed directories");
 470 |     }
 471 |     return realPath;
 472 |   }
 473 |   catch (error) {
 474 |     // For new files that don't exist yet, verify parent directory
 475 |     const parentDir = dirname(absolute);
 476 |     try {
 477 |       const realParentPath = await wslRealpath(parentDir);
 478 |       const normalizedParent = normalizePath(realParentPath);
 479 |       const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir));
 480 |       if (!isParentAllowed) {
 481 |         throw new Error("Access denied - parent directory outside allowed directories");
 482 |       }
 483 |       return absolute;
 484 |     }
 485 |     catch {
 486 |       throw new Error(`Parent directory does not exist: ${parentDir}`);
 487 |     }
 488 |   }
 489 | }
 490 | 
 491 | // Schema definitions
 492 | const ReadFileArgsSchema = z.object({
 493 |   path: z.string(),
 494 | });
 495 | 
 496 | const ReadMultipleFilesArgsSchema = z.object({
 497 |   paths: z.array(z.string()),
 498 | });
 499 | 
 500 | const WriteFileArgsSchema = z.object({
 501 |   path: z.string(),
 502 |   content: z.string(),
 503 | });
 504 | 
 505 | const EditOperation = z.object({
 506 |   oldText: z.string().describe('Text to search for - must match exactly'),
 507 |   newText: z.string().describe('Text to replace with')
 508 | });
 509 | 
 510 | const EditFileArgsSchema = z.object({
 511 |   path: z.string(),
 512 |   edits: z.array(EditOperation),
 513 |   dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
 514 | });
 515 | 
 516 | const CreateDirectoryArgsSchema = z.object({
 517 |   path: z.string(),
 518 | });
 519 | 
 520 | const ListDirectoryArgsSchema = z.object({
 521 |   path: z.string(),
 522 | });
 523 | 
 524 | const DirectoryTreeArgsSchema = z.object({
 525 |   path: z.string(),
 526 | });
 527 | 
 528 | const MoveFileArgsSchema = z.object({
 529 |   source: z.string(),
 530 |   destination: z.string(),
 531 | });
 532 | 
 533 | const SearchFilesArgsSchema = z.object({
 534 |   path: z.string(),
 535 |   pattern: z.string(),
 536 |   excludePatterns: z.array(z.string()).optional().default([])
 537 | });
 538 | 
 539 | const GetFileInfoArgsSchema = z.object({
 540 |   path: z.string(),
 541 | });
 542 | 
 543 | const ListWslDistrosArgsSchema = z.object({});
 544 | 
 545 | const ReadFileByPartsArgsSchema = z.object({
 546 |   path: z.string(),
 547 |   part_number: z.number().int().positive().describe('Part number to read (1, 2, 3, etc.)')
 548 | });
 549 | 
 550 | const SearchInFilesArgsSchema = z.object({
 551 |   path: z.string(),
 552 |   pattern: z.string(),
 553 |   caseInsensitive: z.boolean().default(false).describe('Case insensitive search'),
 554 |   isRegex: z.boolean().default(false).describe('Treat pattern as regular expression'),
 555 |   includePatterns: z.array(z.string()).optional().default([]).describe('File patterns to include (e.g., *.js, *.ts)'),
 556 |   excludePatterns: z.array(z.string()).optional().default([]).describe('File patterns to exclude'),
 557 |   maxResults: z.number().int().positive().default(1000).describe('Maximum number of results to return'),
 558 |   contextLines: z.number().int().min(0).default(0).describe('Number of context lines before and after match')
 559 | });
 560 | 
 561 | const ToolInputSchema = ToolSchema.shape.inputSchema;
 562 | type ToolInput = z.infer<typeof ToolInputSchema>;
 563 | 
 564 | // Server setup
 565 | const server = new Server({
 566 |   name: "secure-filesystem-server",
 567 |   version: "1.3.1",
 568 | }, {
 569 |   capabilities: {
 570 |     tools: {},
 571 |   },
 572 | });
 573 | 
 574 | // Tool implementations
 575 | async function getFileStats(filePath: string): Promise<FileInfo> {
 576 |   const stats = await wslStat(filePath);
 577 |   return {
 578 |     size: stats.size,
 579 |     created: stats.birthtime,
 580 |     modified: stats.mtime,
 581 |     accessed: stats.atime,
 582 |     isDirectory: stats.isDirectory(),
 583 |     isFile: stats.isFile(),
 584 |     permissions: stats.mode.toString(8).slice(-3),
 585 |   };
 586 | }
 587 | 
 588 | async function searchFilesByName(
 589 |   rootPath: string,
 590 |   pattern: string,
 591 |   excludePatterns: string[] = []
 592 | ): Promise<string[]> {
 593 |   const wslRootPath = toWslPath(rootPath);
 594 |   const escapedPattern = pattern.replace(/"/g, '\\"');
 595 |   // Construire une commande find plus robuste
 596 |   const command = [`find "${wslRootPath}" -type f`];
 597 | 
 598 |   // Ajouter grep si pattern fourni
 599 |   if (pattern) {
 600 |     command.push(`(grep -i "${escapedPattern}" || true)`);
 601 |   }
 602 | 
 603 |   // Ajouter des filtres d'exclusion
 604 |   if (excludePatterns && excludePatterns.length > 0) {
 605 |      for (const ex of excludePatterns) {
 606 |       const excluded = ex.replace(/\*/g, ".*").replace(/"/g, '\\"');
 607 |       command.push(`(grep -v "${excluded}" || true)`);
 608 |     }
 609 |   }
 610 | 
 611 |   try {
 612 |     const result = await execWslPipeline(command);
 613 |     return result ? result.split("\n").filter(line => line.trim() !== "") : [];
 614 |   }
 615 |   catch (error: any) {
 616 |     throw error;
 617 |   }
 618 | }
 619 | 
 620 | async function searchInFiles(
 621 |   rootPath: string,
 622 |   pattern: string,
 623 |   options: {
 624 |     caseInsensitive?: boolean;
 625 |     isRegex?: boolean;
 626 |     includePatterns?: string[];
 627 |     excludePatterns?: string[];
 628 |     maxResults?: number;
 629 |     contextLines?: number;
 630 |   } = {}
 631 | ): Promise<string> {
 632 |   const wslRootPath = toWslPath(rootPath);
 633 |   
 634 |   // Construire la commande grep
 635 |   const grepOptions: string[] = [];
 636 |   
 637 |   // Options de base
 638 |   grepOptions.push('-n'); // Numéros de ligne
 639 |   grepOptions.push('-H'); // Toujours afficher le nom du fichier
 640 |   grepOptions.push('-r'); // Récursif
 641 |   
 642 |   // Options conditionnelles
 643 |   if (options.caseInsensitive) {
 644 |     grepOptions.push('-i');
 645 |   }
 646 |   
 647 |   if (options.isRegex) {
 648 |     grepOptions.push('-E'); // Extended regex
 649 |   } else {
 650 |     grepOptions.push('-F'); // Fixed string
 651 |   }
 652 |   
 653 |   // Contexte
 654 |   if (options.contextLines && options.contextLines > 0) {
 655 |     grepOptions.push(`-C${options.contextLines}`);
 656 |   }
 657 |   
 658 |   // Limiter les résultats
 659 |   if (options.maxResults) {
 660 |     grepOptions.push(`-m${Math.ceil(options.maxResults / 10)}`); // Approximatif par fichier
 661 |   }
 662 |   
 663 |   // Patterns d'inclusion
 664 |   if (options.includePatterns && options.includePatterns.length > 0) {
 665 |     for (const pattern of options.includePatterns) {
 666 |       grepOptions.push(`--include=${pattern}`);
 667 |     }
 668 |   }
 669 |   
 670 |   // Patterns d'exclusion
 671 |   if (options.excludePatterns && options.excludePatterns.length > 0) {
 672 |     for (const pattern of options.excludePatterns) {
 673 |       grepOptions.push(`--exclude=${pattern}`);
 674 |     }
 675 |   }
 676 |   
 677 |   // Exclure les répertoires communs à ignorer
 678 |   grepOptions.push('--exclude-dir=.git');
 679 |   grepOptions.push('--exclude-dir=node_modules');
 680 |   grepOptions.push('--exclude-dir=.svn');
 681 |   grepOptions.push('--exclude-dir=.hg');
 682 |   
 683 |   try {
 684 |     // Encoder le pattern en base64 pour éviter tous problèmes d'échappement
 685 |     const base64Pattern = Buffer.from(pattern).toString('base64');
 686 |     
 687 |     // Construire la commande qui passe le pattern via stdin
 688 |     const grepCommand = `bash -c "echo '${base64Pattern}' | base64 -d | grep ${grepOptions.join(' ')} -f - '${wslRootPath}' 2>&1 || true"`;
 689 |     const result = await execWslCommand(grepCommand);
 690 |     
 691 |     if (!result.trim()) {
 692 |       return "No matches found.";
 693 |     }
 694 |     
 695 |     // Filtrer les messages d'erreur grep courants
 696 |     const lines = result.trim().split('\n').filter(line => {
 697 |       // Ignorer les messages d'erreur courants de grep
 698 |       return !line.includes('grep: ') && 
 699 |              !line.includes('Is a directory') &&
 700 |              !line.includes('Permission denied') &&
 701 |              !line.includes('No such file or directory');
 702 |     });
 703 |     
 704 |     if (lines.length === 0) {
 705 |       return "No matches found.";
 706 |     }
 707 |     
 708 |     // Limiter les résultats si nécessaire
 709 |     if (options.maxResults && lines.length > options.maxResults) {
 710 |       const truncated = lines.slice(0, options.maxResults);
 711 |       truncated.push(`\n... (${lines.length - options.maxResults} more results omitted)`);
 712 |       return truncated.join('\n');
 713 |     }
 714 |     
 715 |     return lines.join('\n');
 716 |   } catch (error: any) {
 717 |     throw new Error(`Failed to search in files: ${error.message}`);
 718 |   }
 719 | }
 720 | 
 721 | async function readFileByParts(filePath: string, partNumber: number): Promise<string> {
 722 |   const wslPath = toWslPath(filePath);
 723 |   const PART_SIZE = 95000;
 724 |   const MAX_BACKTRACK = 300;
 725 |   
 726 |   try {
 727 |     // Obtenir la taille du fichier
 728 |     const fileSizeStr = await execWslCommand(`bash -c "wc -c < '${wslPath}'"`);
 729 |     const fileSize = parseInt(fileSizeStr.trim());
 730 |     
 731 |     // Calculer la position de début théorique
 732 |     const theoreticalStart = (partNumber - 1) * PART_SIZE;
 733 |     
 734 |     // Vérifier si la partie demandée existe
 735 |     if (theoreticalStart >= fileSize) {
 736 |       throw new Error(`File has only ${fileSize.toLocaleString()} characters. Part ${partNumber} does not exist.`);
 737 |     }
 738 |     
 739 |     let actualStart = theoreticalStart;
 740 |     
 741 |     // Pour la première partie, pas de recul nécessaire
 742 |     if (partNumber === 1) {
 743 |       const content = await execWslCommand(`head -c ${PART_SIZE} "${wslPath}"`);
 744 |       return content;
 745 |     }
 746 |     
 747 |     // Pour les autres parties, trouver le début de ligne précédent
 748 |     if (partNumber > 1) {
 749 |       const searchStart = Math.max(0, theoreticalStart - MAX_BACKTRACK);
 750 |       const searchLength = theoreticalStart - searchStart;
 751 |       
 752 |       if (searchLength > 0) {
 753 |         // Lire la zone de recherche et trouver le dernier \n
 754 |         const searchContent = await execWslCommand(
 755 |           `bash -c "tail -c +${searchStart + 1} '${wslPath}' | head -c ${searchLength}"`
 756 |         );
 757 |         
 758 |         const lastNewlineIndex = searchContent.lastIndexOf('\n');
 759 |         
 760 |         if (lastNewlineIndex !== -1) {
 761 |           actualStart = searchStart + lastNewlineIndex + 1;
 762 |         }
 763 |       }
 764 |     }
 765 |     
 766 |     // Lire le contenu depuis actualStart
 767 |     let content = await execWslCommand(
 768 |       `bash -c "tail -c +${actualStart + 1} '${wslPath}' | head -c ${PART_SIZE}"`
 769 |     );
 770 |     
 771 |     // Pour les parties autres que la première, essayer de finir sur une ligne complète
 772 |     if (partNumber > 1 && content.length === PART_SIZE) {
 773 |       const endSearchStart = actualStart + PART_SIZE;
 774 |       
 775 |       if (endSearchStart < fileSize) {
 776 |         const remainingChars = Math.min(MAX_BACKTRACK, fileSize - endSearchStart);
 777 |         
 778 |         if (remainingChars > 0) {
 779 |           const endSearchContent = await execWslCommand(
 780 |             `bash -c "tail -c +${endSearchStart + 1} '${wslPath}' | head -c ${remainingChars}"`
 781 |           );
 782 |           
 783 |           const firstNewlineIndex = endSearchContent.indexOf('\n');
 784 |           
 785 |           if (firstNewlineIndex !== -1) {
 786 |             content += endSearchContent.substring(0, firstNewlineIndex + 1);
 787 |           }
 788 |         }
 789 |       }
 790 |     }
 791 |     
 792 |     return content;
 793 |   } catch (error: any) {
 794 |     if (error.message.includes('File has only')) {
 795 |       throw error;
 796 |     }
 797 |     throw new Error(`Failed to read file part ${partNumber} of ${filePath}: ${error.message}`);
 798 |   }
 799 | }
 800 | 
 801 | // file editing and diffing utilities
 802 | function normalizeLineEndings(text: string): string {
 803 |   return text.replace(/\r\n/g, '\n');
 804 | }
 805 | 
 806 | function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
 807 |   // Ensure consistent line endings for diff
 808 |   const normalizedOriginal = normalizeLineEndings(originalContent);
 809 |   const normalizedNew = normalizeLineEndings(newContent);
 810 | 
 811 |   return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified');
 812 | }
 813 | 
 814 | async function applyFileEdits(
 815 |   filePath: string,
 816 |   edits: EditOperationType[],
 817 |   dryRun: boolean = false
 818 | ): Promise<string> {
 819 |   // Read file content and normalize line endings
 820 |   const content = normalizeLineEndings(await wslReadFile(filePath, 'utf-8'));
 821 | 
 822 |   // Apply edits sequentially
 823 |   let modifiedContent = content;
 824 |   for (const edit of edits) {
 825 |     const normalizedOld = normalizeLineEndings(edit.oldText);
 826 |     const normalizedNew = normalizeLineEndings(edit.newText);
 827 | 
 828 |     // If exact match exists, use it
 829 |     if (modifiedContent.includes(normalizedOld)) {
 830 |       modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
 831 |       continue;
 832 |     }
 833 | 
 834 |     // Otherwise, try line-by-line matching with flexibility for whitespace
 835 |     const oldLines = normalizedOld.split('\n');
 836 |     const contentLines = modifiedContent.split('\n');
 837 |     let matchFound = false;
 838 | 
 839 |     for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
 840 |       const potentialMatch = contentLines.slice(i, i + oldLines.length);
 841 | 
 842 |       // Compare lines with normalized whitespace
 843 |       const isMatch = oldLines.every((oldLine: string, j: number) => {
 844 |         const contentLine = potentialMatch[j];
 845 |         return oldLine.trim() === contentLine.trim();
 846 |       });
 847 | 
 848 |       if (isMatch) {
 849 |         // Preserve original indentation of first line
 850 |         const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
 851 |         const newLines = normalizedNew.split('\n').map((line: string, j: number) => {
 852 |           if (j === 0)
 853 |             return originalIndent + line.trimStart();
 854 |           // For subsequent lines, try to preserve relative indentation
 855 |           const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
 856 |           const newIndent = line.match(/^\s*/)?.[0] || '';
 857 |           if (oldIndent && newIndent) {
 858 |             const relativeIndent = newIndent.length - oldIndent.length;
 859 |             return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
 860 |           }
 861 |           return line;
 862 |         });
 863 | 
 864 |         contentLines.splice(i, oldLines.length, ...newLines);
 865 |         modifiedContent = contentLines.join('\n');
 866 |         matchFound = true;
 867 |         break;
 868 |       }
 869 |     }
 870 | 
 871 |     if (!matchFound) {
 872 |       throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
 873 |     }
 874 |   }
 875 | 
 876 |   // Create unified diff
 877 |   const diff = createUnifiedDiff(content, modifiedContent, filePath);
 878 | 
 879 |   // Format diff with appropriate number of backticks
 880 |   let numBackticks = 3;
 881 |   while (diff.includes('`'.repeat(numBackticks))) {
 882 |     numBackticks++;
 883 |   }
 884 |   const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
 885 | 
 886 |   if (!dryRun) {
 887 |     await wslWriteFile(filePath, modifiedContent);
 888 |   }
 889 | 
 890 |   return formattedDiff;
 891 | }
 892 | 
 893 | // Tool handlers
 894 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 895 |   return {
 896 |     tools: [
 897 |       {
 898 |         name: "read_file",
 899 |         description: "Read the complete contents of a file from the file system. " +
 900 |           "Handles various text encodings and provides detailed error messages " +
 901 |           "if the file cannot be read. Use this tool when you need to examine " +
 902 |           "the contents of a single file. Only works within allowed directories.",
 903 |         inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput,
 904 |       },
 905 |       {
 906 |         name: "read_file_by_parts",
 907 |         description: "Read a file in parts of approximately 95,000 characters. " +
 908 |           "Use this for large files that cannot be read in one go. " +
 909 |           "Part 1 reads the first 95,000 characters. " +
 910 |           "Subsequent parts start at boundaries that respect line breaks when possible. " +
 911 |           "If a requested part number exceeds the file size, an error is returned with the actual file size.",
 912 |         inputSchema: zodToJsonSchema(ReadFileByPartsArgsSchema) as ToolInput,
 913 |       },
 914 |       {
 915 |         name: "read_multiple_files",
 916 |         description: "Read the contents of multiple files simultaneously. This is more " +
 917 |           "efficient than reading files one by one when you need to analyze " +
 918 |           "or compare multiple files. Each file's content is returned with its " +
 919 |           "path as a reference. Failed reads for individual files won't stop " +
 920 |           "the entire operation. Only works within allowed directories.",
 921 |         inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput,
 922 |       },
 923 |       {
 924 |         name: "write_file",
 925 |         description: "Create a new file or completely overwrite an existing file with new content. " +
 926 |           "Use with caution as it will overwrite existing files without warning. " +
 927 |           "Handles text content with proper encoding. Only works within allowed directories.",
 928 |         inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput,
 929 |       },
 930 |       {
 931 |         name: "edit_file",
 932 |         description: "Make line-based edits to a text file. Each edit replaces exact line sequences " +
 933 |           "with new content. Returns a git-style diff showing the changes made. " +
 934 |           "Only works within allowed directories.",
 935 |         inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput,
 936 |       },
 937 |       {
 938 |         name: "create_directory",
 939 |         description: "Create a new directory or ensure a directory exists. Can create multiple " +
 940 |           "nested directories in one operation. If the directory already exists, " +
 941 |           "this operation will succeed silently. Perfect for setting up directory " +
 942 |           "structures for projects or ensuring required paths exist. Only works within allowed directories.",
 943 |         inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput,
 944 |       },
 945 |       {
 946 |         name: "list_directory",
 947 |         description: "Get a detailed listing of all files and directories in a specified path. " +
 948 |           "Results clearly distinguish between files and directories with [FILE] and [DIR] " +
 949 |           "prefixes. This tool is essential for understanding directory structure and " +
 950 |           "finding specific files within a directory. Only works within allowed directories.",
 951 |         inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
 952 |       },
 953 |       {
 954 |         name: "directory_tree",
 955 |         description: "Get a recursive tree view of files and directories as a JSON structure. " +
 956 |           "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
 957 |           "Files have no children array, while directories always have a children array (which may be empty). " +
 958 |           "The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
 959 |         inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
 960 |       },
 961 |       {
 962 |         name: "move_file",
 963 |         description: "Move or rename files and directories. Can move files between directories " +
 964 |           "and rename them in a single operation. If the destination exists, the " +
 965 |           "operation will fail. Works across different directories and can be used " +
 966 |           "for simple renaming within the same directory. Both source and destination must be within allowed directories.",
 967 |         inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput,
 968 |       },
 969 |       {
 970 |         name: "search_files_by_name",
 971 |         description: "Recursively search for files and directories matching a pattern. " +
 972 |           "Searches through all subdirectories from the starting path. The search " +
 973 |           "is case-insensitive and matches partial names. Returns full paths to all " +
 974 |           "matching items. Great for finding files when you don't know their exact location. " +
 975 |           "Only searches within allowed directories.",
 976 |         inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput,
 977 |       },
 978 |       {
 979 |         name: "search_in_files",
 980 |         description: "Search for text patterns within files recursively. " +
 981 |           "Supports plain text and regular expression searches. Can filter by file patterns, " +
 982 |           "exclude certain files/directories, limit results, and show context lines. " +
 983 |           "Returns matching lines with file paths and line numbers. " +
 984 |           "Automatically excludes common directories like .git and node_modules. " +
 985 |           "Only searches within allowed directories.",
 986 |         inputSchema: zodToJsonSchema(SearchInFilesArgsSchema) as ToolInput,
 987 |       },
 988 |       {
 989 |         name: "get_file_info",
 990 |         description: "Retrieve detailed metadata about a file or directory. Returns comprehensive " +
 991 |           "information including size, creation time, last modified time, permissions, " +
 992 |           "and type. This tool is perfect for understanding file characteristics " +
 993 |           "without reading the actual content. Only works within allowed directories.",
 994 |         inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput,
 995 |       },
 996 |       {
 997 |         name: "list_allowed_directories",
 998 |         description: "Returns the list of directories that this server is allowed to access. " +
 999 |           "Use this to understand which directories are available before trying to access files.",
1000 |         inputSchema: {
1001 |           type: "object",
1002 |           properties: {},
1003 |           required: [],
1004 |         } as ToolInput,
1005 |       },
1006 |       {
1007 |         name: "list_wsl_distributions",
1008 |         description: "Lists all available WSL distributions and shows which one is currently being used.",
1009 |         inputSchema: zodToJsonSchema(ListWslDistrosArgsSchema) as ToolInput,
1010 |       },
1011 |     ],
1012 |   };
1013 | });
1014 | 
1015 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
1016 |   try {
1017 |     const { name, arguments: args } = request.params;
1018 |     switch (name) {
1019 |       case "read_file": {
1020 |         const parsed = ReadFileArgsSchema.safeParse(args);
1021 |         if (!parsed.success) {
1022 |           throw new Error(`Invalid arguments for read_file: ${parsed.error}`);
1023 |         }
1024 |         const validPath = await validatePath(parsed.data.path);
1025 |         const content = await wslReadFile(validPath);
1026 |         return {
1027 |           content: [{ type: "text", text: content }],
1028 |         };
1029 |       }
1030 |       case "read_file_by_parts": {
1031 |         const parsed = ReadFileByPartsArgsSchema.safeParse(args);
1032 |         if (!parsed.success) {
1033 |           throw new Error(`Invalid arguments for read_file_by_parts: ${parsed.error}`);
1034 |         }
1035 |         const validPath = await validatePath(parsed.data.path);
1036 |         const content = await readFileByParts(validPath, parsed.data.part_number);
1037 |         
1038 |         return {
1039 |           content: [{ 
1040 |             type: "text", 
1041 |             text: content
1042 |           }],
1043 |         };
1044 |       }
1045 |       case "read_multiple_files": {
1046 |         const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
1047 |         if (!parsed.success) {
1048 |           throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`);
1049 |         }
1050 |         const results = await Promise.all(parsed.data.paths.map(async (filePath: string) => {
1051 |           try {
1052 |             const validPath = await validatePath(filePath);
1053 |             const content = await wslReadFile(validPath);
1054 |             return `${filePath}:\n${content}\n`;
1055 |           }
1056 |           catch (error) {
1057 |             const errorMessage = error instanceof Error ? error.message : String(error);
1058 |             return `${filePath}: Error - ${errorMessage}`;
1059 |           }
1060 |         }));
1061 |         return {
1062 |           content: [{ type: "text", text: results.join("\n---\n") }],
1063 |         };
1064 |       }
1065 |       case "write_file": {
1066 |         const parsed = WriteFileArgsSchema.safeParse(args);
1067 |         if (!parsed.success) {
1068 |           throw new Error(`Invalid arguments for write_file: ${parsed.error}`);
1069 |         }
1070 |         const validPath = await validatePath(parsed.data.path);
1071 |         await wslWriteFile(validPath, parsed.data.content);
1072 |         return {
1073 |           content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }],
1074 |         };
1075 |       }
1076 |       case "edit_file": {
1077 |         const parsed = EditFileArgsSchema.safeParse(args);
1078 |         if (!parsed.success) {
1079 |           throw new Error(`Invalid arguments for edit_file: ${parsed.error}`);
1080 |         }
1081 |         const validPath = await validatePath(parsed.data.path);
1082 |         const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
1083 |         return {
1084 |           content: [{ type: "text", text: result }],
1085 |         };
1086 |       }
1087 |       case "create_directory": {
1088 |         const parsed = CreateDirectoryArgsSchema.safeParse(args);
1089 |         if (!parsed.success) {
1090 |           throw new Error(`Invalid arguments for create_directory: ${parsed.error}`);
1091 |         }
1092 |         const validPath = await validatePath(parsed.data.path);
1093 |         await wslMkdir(validPath);
1094 |         return {
1095 |           content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }],
1096 |         };
1097 |       }
1098 |       case "list_directory": {
1099 |         const parsed = ListDirectoryArgsSchema.safeParse(args);
1100 |         if (!parsed.success) {
1101 |           throw new Error(`Invalid arguments for list_directory: ${parsed.error}`);
1102 |         }
1103 |         const validPath = await validatePath(parsed.data.path);
1104 |         const entries = await wslReaddir(validPath);
1105 |         const formatted = entries
1106 |           .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
1107 |           .join("\n");
1108 |         return {
1109 |           content: [{ type: "text", text: formatted }],
1110 |         };
1111 |       }
1112 |       case "directory_tree": {
1113 |         const parsed = DirectoryTreeArgsSchema.safeParse(args);
1114 |         if (!parsed.success) {
1115 |           throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
1116 |         }
1117 |         
1118 |         const validPath = await validatePath(parsed.data.path);
1119 |         const wslPath = toWslPath(validPath);
1120 |         
1121 |         // Utiliser find pour obtenir tous les fichiers et répertoires en une seule commande
1122 |         try {
1123 |           // %y = type (d pour directory, f pour file)
1124 |           // %P = chemin relatif depuis le point de départ
1125 |           const findResult = await execWslCommand(
1126 |             `find "${wslPath}" -printf "%y %P\\n" | sort`
1127 |           );
1128 |           
1129 |           if (!findResult.trim()) {
1130 |             return {
1131 |               content: [{
1132 |                 type: "text",
1133 |                 text: JSON.stringify([], null, 2)
1134 |               }],
1135 |             };
1136 |           }
1137 |           
1138 |           // Parser la sortie de find et reconstruire l'arbre
1139 |           const lines = findResult.trim().split('\n');
1140 |           const tree: TreeEntry[] = [];
1141 |           const pathMap = new Map<string, TreeEntry>();
1142 |           
1143 |           // Première ligne devrait être le répertoire racine lui-même (chemin vide)
1144 |           // On la saute car on veut le contenu du répertoire, pas le répertoire lui-même
1145 |           const startIndex = lines[0].trim() === 'd ' ? 1 : 0;
1146 |           
1147 |           for (let i = startIndex; i < lines.length; i++) {
1148 |             const line = lines[i].trim();
1149 |             const [type, ...pathParts] = line.split(' ');
1150 |             const relativePath = pathParts.join(' ');
1151 |             
1152 |             if (!relativePath) continue; // Ignorer les lignes vides
1153 |             
1154 |             const parts = relativePath.split('/');
1155 |             const name = parts[parts.length - 1];
1156 |             const parentPath = parts.slice(0, -1).join('/');
1157 |             
1158 |             const entry: TreeEntry = {
1159 |               name,
1160 |               type: type === 'd' ? 'directory' : 'file'
1161 |             };
1162 |             
1163 |             if (type === 'd') {
1164 |               entry.children = [];
1165 |             }
1166 |             
1167 |             pathMap.set(relativePath, entry);
1168 |             
1169 |             if (parentPath) {
1170 |               // Ajouter à son parent
1171 |               const parent = pathMap.get(parentPath);
1172 |               if (parent && parent.children) {
1173 |                 parent.children.push(entry);
1174 |               }
1175 |             } else {
1176 |               // Entrée de premier niveau
1177 |               tree.push(entry);
1178 |             }
1179 |           }
1180 |           
1181 |           return {
1182 |             content: [{
1183 |               type: "text",
1184 |               text: JSON.stringify(tree, null, 2)
1185 |             }],
1186 |           };
1187 |         } catch (error: any) {
1188 |           throw new Error(`Failed to get directory tree for ${parsed.data.path}: ${error.message}`);
1189 |         }
1190 |       }
1191 |       case "move_file": {
1192 |         const parsed = MoveFileArgsSchema.safeParse(args);
1193 |         if (!parsed.success) {
1194 |           throw new Error(`Invalid arguments for move_file: ${parsed.error}`);
1195 |         }
1196 |         const validSourcePath = await validatePath(parsed.data.source);
1197 |         const validDestPath = await validatePath(parsed.data.destination);
1198 |         await wslRename(validSourcePath, validDestPath);
1199 |         return {
1200 |           content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }],
1201 |         };
1202 |       }
1203 |       case "search_files_by_name": {
1204 |         const parsed = SearchFilesArgsSchema.safeParse(args);
1205 |         if (!parsed.success) {
1206 |           throw new Error(`Invalid arguments for search_files_by_name: ${parsed.error}`);
1207 |         }
1208 |         const validPath = await validatePath(parsed.data.path);
1209 |         const results = await searchFilesByName(validPath, parsed.data.pattern, parsed.data.excludePatterns || []);
1210 |         return {
1211 |           content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }],
1212 |         };
1213 |       }
1214 |       case "search_in_files": {
1215 |         const parsed = SearchInFilesArgsSchema.safeParse(args);
1216 |         if (!parsed.success) {
1217 |           throw new Error(`Invalid arguments for search_in_files: ${parsed.error}`);
1218 |         }
1219 |         const validPath = await validatePath(parsed.data.path);
1220 |         const result = await searchInFiles(validPath, parsed.data.pattern, {
1221 |           caseInsensitive: parsed.data.caseInsensitive,
1222 |           isRegex: parsed.data.isRegex,
1223 |           includePatterns: parsed.data.includePatterns,
1224 |           excludePatterns: parsed.data.excludePatterns,
1225 |           maxResults: parsed.data.maxResults,
1226 |           contextLines: parsed.data.contextLines
1227 |         });
1228 |         return {
1229 |           content: [{ type: "text", text: result }],
1230 |         };
1231 |       }
1232 |       case "get_file_info": {
1233 |         const parsed = GetFileInfoArgsSchema.safeParse(args);
1234 |         if (!parsed.success) {
1235 |           throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`);
1236 |         }
1237 |         const validPath = await validatePath(parsed.data.path);
1238 |         const info = await getFileStats(validPath);
1239 |         return {
1240 |           content: [{
1241 |             type: "text", text: Object.entries(info)
1242 |               .map(([key, value]) => `${key}: ${value}`)
1243 |               .join("\n")
1244 |           }],
1245 |         };
1246 |       }
1247 |       case "list_allowed_directories": {
1248 |         return {
1249 |           content: [{
1250 |             type: "text",
1251 |             text: `Allowed directories:\n${allowedDirectories.join('\n')}`
1252 |           }],
1253 |         };
1254 |       }
1255 |       case "list_wsl_distributions": {
1256 |         const distributions = await listWslDistributions();
1257 |         const formattedList = distributions.map(d => {
1258 |           const isActive = allowedDistro && d.name.toLowerCase() === allowedDistro.toLowerCase()
1259 |             ? " (ACTIVE)"
1260 |             : d.name.includes("(Default)") ? " (DEFAULT)" : "";
1261 |           return `${d.name}${isActive} - State: ${d.state}, Version: ${d.version}`;
1262 |         }).join('\n');
1263 | 
1264 |         return {
1265 |           content: [{
1266 |             type: "text",
1267 |             text: `Available WSL Distributions:\n${formattedList}\n\nCurrently using: ${allowedDistro}`
1268 |           }],
1269 |         };
1270 |       }
1271 |       default:
1272 |         throw new Error(`Unknown tool: ${name}`);
1273 |     }
1274 |   }
1275 |   catch (error) {
1276 |     const errorMessage = error instanceof Error ? error.message : String(error);
1277 |     return {
1278 |       content: [{ type: "text", text: `Error: ${errorMessage}` }],
1279 |       isError: true,
1280 |     };
1281 |   }
1282 | });
1283 | 
1284 | // Start server
1285 | async function runServer() {
1286 |   await initializeWslAndDirectories();
1287 | 
1288 |   const transport = new StdioServerTransport();
1289 |   await server.connect(transport);
1290 |   console.error("Secure MCP WSL Filesystem Server running on stdio");
1291 |   console.error(`Using WSL distribution: ${allowedDistro}`);
1292 |   console.error("Allowed directories:", allowedDirectories);
1293 | }
1294 | 
1295 | runServer().catch((error) => {
1296 |   console.error("Fatal error running server:", error);
1297 |   process.exit(1);
1298 | });
```