# 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 | [](https://www.npmjs.com/package/mcp-server-wsl-filesystem)
34 | [](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 | });
```