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

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── renovate.json
├── src
│   ├── command-executor.ts
│   ├── constants.ts
│   ├── errors.ts
│   ├── index.ts
│   └── types.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
1 | # Package Managers
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
1 | {
2 | 	"useTabs": true,
3 | 	"singleQuote": true,
4 | 	"trailingComma": "all",
5 | 	"printWidth": 70,
6 | 	"proseWrap": "always"
7 | }
```

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

```
 1 | # Dependencies
 2 | node_modules/
 3 | .pnpm-store/
 4 | 
 5 | # Build output
 6 | dist/
 7 | build/
 8 | 
 9 | # Environment variables
10 | .env
11 | .env.local
12 | .env.*.local
13 | 
14 | # IDE
15 | .vscode/
16 | .idea/
17 | *.swp
18 | *.swo
19 | 
20 | # Logs
21 | *.log
22 | npm-debug.log*
23 | pnpm-debug.log*
24 | 
25 | # Testing
26 | coverage/
27 | 
28 | # Database files
29 | *.db
30 | *.db-journal
31 | 
32 | # OS
33 | .DS_Store
34 | Thumbs.db
```

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

```markdown
1 | # Changesets
2 | 
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 | 
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 | 
```

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

```markdown
  1 | # mcp-wsl-exec
  2 | 
  3 | A Model Context Protocol (MCP) server for **Windows + Claude Desktop users** to interact with Windows Subsystem for Linux (WSL). Provides both read-only information gathering and secure command execution capabilities.
  4 | 
  5 | <a href="https://glama.ai/mcp/servers/wv6df94kb8">
  6 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/wv6df94kb8/badge" />
  7 | </a>
  8 | 
  9 | ## ⚠️ Important: Who Should Use This?
 10 | 
 11 | **✅ You SHOULD use this if:**
 12 | - You're using **Claude Desktop on Windows**
 13 | - You need to interact with your WSL environment
 14 | - You want to provide WSL context to Claude (system info, processes, files, etc.)
 15 | 
 16 | **❌ You DON'T need this if:**
 17 | - You're using **Claude Code** (it has native bash access)
 18 | - You're on Linux/macOS (use native tools instead)
 19 | - You only need Windows PowerShell/CMD (use a different MCP server)
 20 | 
 21 | ## Features
 22 | 
 23 | ### 📊 Information Gathering (Read-Only)
 24 | - 🖥️ Get system information (OS, kernel, hostname)
 25 | - 📁 Browse directory contents
 26 | - 💾 Check disk usage
 27 | - ⚙️ List environment variables
 28 | - 🔄 Monitor running processes
 29 | 
 30 | ### 🔧 Command Execution (With Safety)
 31 | - 🔒 Secure command execution in WSL environments
 32 | - ⚡ Built-in safety features:
 33 |   - Dangerous command detection
 34 |   - Command confirmation system
 35 |   - Path traversal prevention
 36 |   - Command sanitization
 37 | - 📁 Working directory support
 38 | - ⏱️ Command timeout functionality
 39 | - 🛡️ Protection against shell injection
 40 | 
 41 | ## Configuration
 42 | 
 43 | This server requires configuration through your MCP client. Here are
 44 | examples for different environments:
 45 | 
 46 | ### Cline Configuration
 47 | 
 48 | Add this to your Cline MCP settings:
 49 | 
 50 | ```json
 51 | {
 52 | 	"mcpServers": {
 53 | 		"mcp-wsl-exec": {
 54 | 			"command": "npx",
 55 | 			"args": ["-y", "mcp-wsl-exec"]
 56 | 		}
 57 | 	}
 58 | }
 59 | ```
 60 | 
 61 | ### Claude Desktop Configuration
 62 | 
 63 | Add this to your Claude Desktop configuration:
 64 | 
 65 | ```json
 66 | {
 67 | 	"mcpServers": {
 68 | 		"mcp-wsl-exec": {
 69 | 			"command": "npx",
 70 | 			"args": ["-y", "mcp-wsl-exec"]
 71 | 		}
 72 | 	}
 73 | }
 74 | ```
 75 | 
 76 | ## API
 77 | 
 78 | The server provides 7 MCP tools:
 79 | 
 80 | ### Information Gathering (Read-Only) 📊
 81 | 
 82 | These tools provide context about your WSL environment without making changes:
 83 | 
 84 | #### get_system_info
 85 | 
 86 | Get system information (OS version, kernel, hostname).
 87 | 
 88 | **Parameters:** None
 89 | 
 90 | #### get_directory_info
 91 | 
 92 | Get directory contents and file information.
 93 | 
 94 | **Parameters:**
 95 | - `path` (string, optional): Directory path (defaults to current directory)
 96 | - `details` (boolean, optional): Show detailed information (permissions, sizes, etc.)
 97 | 
 98 | #### get_disk_usage
 99 | 
100 | Get disk space information.
101 | 
102 | **Parameters:**
103 | - `path` (string, optional): Specific path to check (defaults to all filesystems)
104 | 
105 | #### get_environment
106 | 
107 | Get environment variables.
108 | 
109 | **Parameters:**
110 | - `filter` (string, optional): Filter pattern to search for specific variables
111 | 
112 | #### list_processes
113 | 
114 | List running processes.
115 | 
116 | **Parameters:**
117 | - `filter` (string, optional): Filter by process name
118 | 
119 | ### Command Execution (Potentially Destructive) 🔧
120 | 
121 | Use these tools when you need to make changes or run custom commands:
122 | 
123 | #### execute_command
124 | 
125 | Execute a command in WSL with safety checks and validation.
126 | 
127 | **Parameters:**
128 | - `command` (string, required): Command to execute
129 | - `working_dir` (string, optional): Working directory for command execution
130 | - `timeout` (number, optional): Timeout in milliseconds
131 | 
132 | **Note:** Dangerous commands will require confirmation via `confirm_command`.
133 | 
134 | #### confirm_command
135 | 
136 | Confirm execution of a dangerous command that was flagged by safety checks.
137 | 
138 | **Parameters:**
139 | - `confirmation_id` (string, required): Confirmation ID received from execute_command
140 | - `confirm` (boolean, required): Whether to proceed with the command execution
141 | 
142 | ## Safety Features
143 | 
144 | ### Dangerous Command Detection
145 | 
146 | The server maintains a list of potentially dangerous commands that
147 | require explicit confirmation before execution, including:
148 | 
149 | - File system operations (rm, rmdir, mv)
150 | - System commands (shutdown, reboot)
151 | - Package management (apt, yum, dnf)
152 | - File redirections (>, >>)
153 | - Permission changes (chmod, chown)
154 | - And more...
155 | 
156 | ### Command Sanitization
157 | 
158 | All commands are sanitized to prevent:
159 | 
160 | - Shell metacharacter injection
161 | - Path traversal attempts
162 | - Home directory references
163 | - Dangerous command chaining
164 | 
165 | ## Development
166 | 
167 | ### Setup
168 | 
169 | 1. Clone the repository
170 | 2. Install dependencies:
171 | 
172 | ```bash
173 | pnpm install
174 | ```
175 | 
176 | 3. Build the project:
177 | 
178 | ```bash
179 | pnpm build
180 | ```
181 | 
182 | 4. Run in development mode:
183 | 
184 | ```bash
185 | pnpm dev
186 | ```
187 | 
188 | ### Publishing
189 | 
190 | The project uses changesets for version management. To publish:
191 | 
192 | 1. Create a changeset:
193 | 
194 | ```bash
195 | pnpm changeset
196 | ```
197 | 
198 | 2. Version the package:
199 | 
200 | ```bash
201 | pnpm changeset version
202 | ```
203 | 
204 | 3. Publish to npm:
205 | 
206 | ```bash
207 | pnpm release
208 | ```
209 | 
210 | ## Contributing
211 | 
212 | Contributions are welcome! Please feel free to submit a Pull Request.
213 | 
214 | ## License
215 | 
216 | MIT License - see the [LICENSE](LICENSE) file for details.
217 | 
218 | ## Acknowledgments
219 | 
220 | - Built on the
221 |   [Model Context Protocol](https://github.com/modelcontextprotocol)
222 | - Designed for secure WSL command execution
223 | 
```

--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 |   "extends": [
4 |     "config:recommended"
5 |   ]
6 | }
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # mcp-wsl-exec
 2 | 
 3 | ## 0.0.3
 4 | 
 5 | ### Patch Changes
 6 | 
 7 | - dd1155f: Migrate to tmcp, add read-only info tools, clarify
 8 |   Windows+Claude Desktop focus
 9 | 
10 | ## 0.0.2
11 | 
12 | ### Patch Changes
13 | 
14 | - glama badge
15 | 
16 | ## 0.0.1
17 | 
18 | ### Patch Changes
19 | 
20 | - init
21 | 
```

--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
 3 |   "changelog": "@changesets/cli/changelog",
 4 |   "commit": false,
 5 |   "fixed": [],
 6 |   "linked": [],
 7 |   "access": "public",
 8 |   "baseBranch": "main",
 9 |   "updateInternalDependencies": "patch",
10 |   "ignore": []
11 | }
```

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

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

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export interface CommandResponse {
 2 | 	stdout: string;
 3 | 	stderr: string;
 4 | 	exit_code: number | null;
 5 | 	command: string;
 6 | 	requires_confirmation?: boolean;
 7 | 	error?: string;
 8 | 	working_dir?: string;
 9 | }
10 | 
11 | export interface PendingConfirmation {
12 | 	command: string;
13 | 	working_dir?: string;
14 | 	timeout?: number;
15 | 	resolve: (value: CommandResponse) => void;
16 | 	reject: (reason?: any) => void;
17 | }
18 | 
```

--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // Define dangerous commands that require confirmation
 2 | export const dangerous_commands = [
 3 | 	'rm',
 4 | 	'rmdir',
 5 | 	'dd',
 6 | 	'mkfs',
 7 | 	'mkswap',
 8 | 	'fdisk',
 9 | 	'shutdown',
10 | 	'reboot',
11 | 	'>', // redirect that could overwrite
12 | 	'>>', // append redirect that could modify files
13 | 	'format',
14 | 	'chmod',
15 | 	'chown',
16 | 	'sudo',
17 | 	'su',
18 | 	'passwd',
19 | 	'mv', // moving files can be dangerous
20 | 	'find -delete',
21 | 	'truncate',
22 | 	'shred',
23 | 	'kill',
24 | 	'pkill',
25 | 	'service',
26 | 	'systemctl',
27 | 	'mount',
28 | 	'umount',
29 | 	'apt',
30 | 	'apt-get',
31 | 	'dpkg',
32 | 	'yum',
33 | 	'dnf',
34 | 	'pacman',
35 | ] as const;
36 | 
37 | // WSL process configuration
38 | export const wsl_config = {
39 | 	executable: 'wsl.exe',
40 | 	shell: 'bash',
41 | 	default_timeout: 30000, // 30 seconds
42 | } as const;
43 | 
```

--------------------------------------------------------------------------------
/src/errors.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export class WslExecutionError extends Error {
 2 | 	constructor(
 3 | 		message: string,
 4 | 		public readonly details?: any,
 5 | 	) {
 6 | 		super(message);
 7 | 		this.name = 'WslExecutionError';
 8 | 	}
 9 | }
10 | 
11 | export class CommandValidationError extends WslExecutionError {
12 | 	constructor(message: string, details?: any) {
13 | 		super(message, details);
14 | 		this.name = 'CommandValidationError';
15 | 	}
16 | }
17 | 
18 | export class CommandTimeoutError extends WslExecutionError {
19 | 	constructor(timeout: number) {
20 | 		super(`Command timed out after ${timeout}ms`, { timeout });
21 | 		this.name = 'CommandTimeoutError';
22 | 	}
23 | }
24 | 
25 | export class InvalidConfirmationError extends WslExecutionError {
26 | 	constructor(confirmation_id: string) {
27 | 		super('Invalid or expired confirmation ID', { confirmation_id });
28 | 		this.name = 'InvalidConfirmationError';
29 | 	}
30 | }
31 | 
```

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

```json
 1 | {
 2 | 	"name": "mcp-wsl-exec",
 3 | 	"version": "0.0.3",
 4 | 	"description": "A secure Model Context Protocol (MCP) server for executing commands in Windows Subsystem for Linux (WSL) with built-in safety features and validation",
 5 | 	"type": "module",
 6 | 	"main": "dist/index.js",
 7 | 	"types": "dist/index.d.ts",
 8 | 	"bin": {
 9 | 		"mcp-wsl-exec": "./dist/index.js"
10 | 	},
11 | 	"files": [
12 | 		"dist",
13 | 		"README.md",
14 | 		"LICENSE"
15 | 	],
16 | 	"scripts": {
17 | 		"build": "tsc && chmod +x dist/index.js",
18 | 		"start": "node dist/index.js",
19 | 		"dev": "npx @modelcontextprotocol/inspector dist/index.js",
20 | 		"changeset": "changeset",
21 | 		"version": "changeset version",
22 | 		"release": "pnpm run build && changeset publish"
23 | 	},
24 | 	"keywords": [
25 | 		"mcp",
26 | 		"model-context-protocol",
27 | 		"wsl",
28 | 		"exec",
29 | 		"command-execution",
30 | 		"windows-subsystem-linux",
31 | 		"security",
32 | 		"command-line",
33 | 		"cli",
34 | 		"shell",
35 | 		"bash",
36 | 		"linux",
37 | 		"windows",
38 | 		"safe-execution",
39 | 		"command-validation",
40 | 		"path-validation",
41 | 		"timeout",
42 | 		"error-handling"
43 | 	],
44 | 	"author": "Scott Spence",
45 | 	"license": "MIT",
46 | 	"repository": {
47 | 		"type": "git",
48 | 		"url": "https://github.com/spences10/mcp-wsl-exec.git"
49 | 	},
50 | 	"bugs": {
51 | 		"url": "https://github.com/spences10/mcp-wsl-exec/issues"
52 | 	},
53 | 	"homepage": "https://github.com/spences10/mcp-wsl-exec#readme",
54 | 	"devDependencies": {
55 | 		"@changesets/cli": "^2.29.7",
56 | 		"@types/node": "^24.6.1",
57 | 		"typescript": "^5.9.3"
58 | 	},
59 | 	"dependencies": {
60 | 		"@tmcp/adapter-valibot": "^0.1.4",
61 | 		"@tmcp/transport-stdio": "^0.3.1",
62 | 		"tmcp": "^1.14.0",
63 | 		"valibot": "^1.1.0"
64 | 	}
65 | }
```

--------------------------------------------------------------------------------
/src/command-executor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { spawn } from 'child_process';
  2 | import { dangerous_commands, wsl_config } from './constants.js';
  3 | import { CommandValidationError, CommandTimeoutError } from './errors.js';
  4 | import { CommandResponse } from './types.js';
  5 | 
  6 | export class CommandExecutor {
  7 | 	private sanitize_command(command: string): string {
  8 | 		// Enhanced command sanitization
  9 | 		const sanitized = command
 10 | 			.replace(/[;&|`$]/g, '') // Remove shell metacharacters
 11 | 			.replace(/\\/g, '/') // Normalize path separators
 12 | 			.replace(/\.\./g, '') // Remove parent directory references
 13 | 			.replace(/~/g, '') // Remove home directory references
 14 | 			.trim(); // Remove leading/trailing whitespace
 15 | 
 16 | 		// Check for empty command after sanitization
 17 | 		if (!sanitized) {
 18 | 			throw new CommandValidationError(
 19 | 				'Invalid command: Empty after sanitization',
 20 | 			);
 21 | 		}
 22 | 
 23 | 		return sanitized;
 24 | 	}
 25 | 
 26 | 	private validate_working_dir(working_dir?: string): string | undefined {
 27 | 		if (!working_dir) return undefined;
 28 | 
 29 | 		// Sanitize and validate working directory
 30 | 		const sanitized = working_dir
 31 | 			.replace(/[;&|`$]/g, '')
 32 | 			.replace(/\\/g, '/')
 33 | 			.trim();
 34 | 
 35 | 		if (!sanitized) {
 36 | 			throw new CommandValidationError('Invalid working directory');
 37 | 		}
 38 | 
 39 | 		return sanitized;
 40 | 	}
 41 | 
 42 | 	private validate_timeout(timeout?: number): number | undefined {
 43 | 		if (!timeout) return undefined;
 44 | 
 45 | 		if (isNaN(timeout) || timeout < 0) {
 46 | 			throw new CommandValidationError('Invalid timeout value');
 47 | 		}
 48 | 
 49 | 		return timeout;
 50 | 	}
 51 | 
 52 | 	public is_dangerous_command(command: string): boolean {
 53 | 		return dangerous_commands.some(
 54 | 			(dangerous) =>
 55 | 				command.toLowerCase().includes(dangerous.toLowerCase()) ||
 56 | 				command.match(new RegExp(`\\b${dangerous}\\b`, 'i')),
 57 | 		);
 58 | 	}
 59 | 
 60 | 	public async execute_command(
 61 | 		command: string,
 62 | 		working_dir?: string,
 63 | 		timeout?: number,
 64 | 	): Promise<CommandResponse> {
 65 | 		return new Promise((resolve, reject) => {
 66 | 			const sanitized_command = this.sanitize_command(command);
 67 | 			const validated_dir = this.validate_working_dir(working_dir);
 68 | 			const validated_timeout = this.validate_timeout(timeout);
 69 | 
 70 | 			const cd_command = validated_dir ? `cd "${validated_dir}" && ` : '';
 71 | 			const full_command = `${cd_command}${sanitized_command}`;
 72 | 
 73 | 			const wsl_process = spawn(wsl_config.executable, [
 74 | 				'--exec',
 75 | 				wsl_config.shell,
 76 | 				'-c',
 77 | 				full_command,
 78 | 			]);
 79 | 
 80 | 			let stdout = '';
 81 | 			let stderr = '';
 82 | 
 83 | 			wsl_process.stdout.on('data', (data) => {
 84 | 				stdout += data.toString();
 85 | 			});
 86 | 
 87 | 			wsl_process.stderr.on('data', (data) => {
 88 | 				stderr += data.toString();
 89 | 			});
 90 | 
 91 | 			let timeout_id: NodeJS.Timeout | undefined;
 92 | 			if (validated_timeout) {
 93 | 				timeout_id = setTimeout(() => {
 94 | 					wsl_process.kill();
 95 | 					reject(new CommandTimeoutError(validated_timeout));
 96 | 				}, validated_timeout);
 97 | 			}
 98 | 
 99 | 			wsl_process.on('close', (code) => {
100 | 				if (timeout_id) {
101 | 					clearTimeout(timeout_id);
102 | 				}
103 | 				resolve({
104 | 					stdout,
105 | 					stderr,
106 | 					exit_code: code,
107 | 					command: sanitized_command,
108 | 					working_dir: validated_dir,
109 | 				});
110 | 			});
111 | 
112 | 			wsl_process.on('error', (error) => {
113 | 				if (timeout_id) {
114 | 					clearTimeout(timeout_id);
115 | 				}
116 | 				reject(error);
117 | 			});
118 | 		});
119 | 	}
120 | }
121 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from 'tmcp';
  4 | import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
  5 | import { StdioTransport } from '@tmcp/transport-stdio';
  6 | import * as v from 'valibot';
  7 | import type { GenericSchema } from 'valibot';
  8 | import { readFileSync } from 'fs';
  9 | import { dirname, join } from 'path';
 10 | import { fileURLToPath } from 'url';
 11 | import { CommandExecutor } from './command-executor.js';
 12 | import { InvalidConfirmationError } from './errors.js';
 13 | import { CommandResponse, PendingConfirmation } from './types.js';
 14 | 
 15 | const __filename = fileURLToPath(import.meta.url);
 16 | const __dirname = dirname(__filename);
 17 | const pkg = JSON.parse(
 18 | 	readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
 19 | );
 20 | const { name, version } = pkg;
 21 | 
 22 | class WslServer {
 23 | 	private server: McpServer<GenericSchema>;
 24 | 	private command_executor: CommandExecutor;
 25 | 	private pending_confirmations: Map<string, PendingConfirmation>;
 26 | 
 27 | 	constructor() {
 28 | 		const adapter = new ValibotJsonSchemaAdapter();
 29 | 		this.server = new McpServer<GenericSchema>(
 30 | 			{
 31 | 				name,
 32 | 				version,
 33 | 				description: 'A secure MCP server for executing commands in WSL with built-in safety features',
 34 | 			},
 35 | 			{
 36 | 				adapter,
 37 | 				capabilities: {
 38 | 					tools: { listChanged: true },
 39 | 				},
 40 | 			},
 41 | 		);
 42 | 		this.command_executor = new CommandExecutor();
 43 | 		this.pending_confirmations = new Map();
 44 | 		this.setup_tool_handlers();
 45 | 	}
 46 | 
 47 | 	private format_output(result: CommandResponse): string {
 48 | 		return [
 49 | 			`Command: ${result.command}`,
 50 | 			result.working_dir
 51 | 				? `Working Directory: ${result.working_dir}`
 52 | 				: null,
 53 | 			`Exit Code: ${result.exit_code}`,
 54 | 			result.stdout.trim()
 55 | 				? `Output:\n${result.stdout.trim()}`
 56 | 				: 'No output',
 57 | 			result.stderr.trim()
 58 | 				? `Errors:\n${result.stderr.trim()}`
 59 | 				: 'No errors',
 60 | 			result.error ? `Error: ${result.error}` : null,
 61 | 		]
 62 | 			.filter(Boolean)
 63 | 			.join('\n');
 64 | 	}
 65 | 
 66 | 	private async execute_wsl_command(
 67 | 		command: string,
 68 | 		working_dir?: string,
 69 | 		timeout?: number,
 70 | 	): Promise<CommandResponse> {
 71 | 		return new Promise((resolve, reject) => {
 72 | 			const requires_confirmation =
 73 | 				this.command_executor.is_dangerous_command(command);
 74 | 
 75 | 			if (requires_confirmation) {
 76 | 				// Generate a unique confirmation ID
 77 | 				const confirmation_id = Math.random()
 78 | 					.toString(36)
 79 | 					.substring(7);
 80 | 				this.pending_confirmations.set(confirmation_id, {
 81 | 					command,
 82 | 					working_dir,
 83 | 					timeout,
 84 | 					resolve,
 85 | 					reject,
 86 | 				});
 87 | 
 88 | 				// Return early with confirmation request
 89 | 				resolve({
 90 | 					stdout: '',
 91 | 					stderr: `Command "${command}" requires confirmation. Use confirm_command with ID: ${confirmation_id}`,
 92 | 					exit_code: null,
 93 | 					command,
 94 | 					requires_confirmation: true,
 95 | 				});
 96 | 				return;
 97 | 			}
 98 | 
 99 | 			this.command_executor
100 | 				.execute_command(command, working_dir, timeout)
101 | 				.then(resolve)
102 | 				.catch(reject);
103 | 		});
104 | 	}
105 | 
106 | 	private setup_tool_handlers() {
107 | 		// get_system_info tool - read-only
108 | 		this.server.tool(
109 | 			{
110 | 				name: 'get_system_info',
111 | 				description: 'Get WSL system information',
112 | 				annotations: {
113 | 					readOnlyHint: true,
114 | 				},
115 | 			},
116 | 			async () => {
117 | 				try {
118 | 					const result = await this.command_executor.execute_command(
119 | 						'uname -a && lsb_release -a 2>/dev/null || cat /etc/os-release',
120 | 					);
121 | 					return {
122 | 						content: [
123 | 							{
124 | 								type: 'text' as const,
125 | 								text: this.format_output(result),
126 | 							},
127 | 						],
128 | 					};
129 | 				} catch (error) {
130 | 					return {
131 | 						content: [
132 | 							{
133 | 								type: 'text' as const,
134 | 								text: `Error: ${error instanceof Error ? error.message : String(error)}`,
135 | 							},
136 | 						],
137 | 						isError: true,
138 | 					};
139 | 				}
140 | 			},
141 | 		);
142 | 
143 | 		// get_environment tool - read-only
144 | 		this.server.tool(
145 | 			{
146 | 				name: 'get_environment',
147 | 				description: 'Get WSL environment variables',
148 | 				schema: v.object({
149 | 					filter: v.optional(
150 | 						v.pipe(
151 | 							v.string(),
152 | 							v.description('Filter pattern (grep)'),
153 | 						),
154 | 					),
155 | 				}),
156 | 				annotations: {
157 | 					readOnlyHint: true,
158 | 				},
159 | 			},
160 | 			async ({ filter }) => {
161 | 				try {
162 | 					const cmd = filter ? `env | grep -i "${filter}"` : 'env';
163 | 					const result = await this.command_executor.execute_command(cmd);
164 | 					return {
165 | 						content: [
166 | 							{
167 | 								type: 'text' as const,
168 | 								text: this.format_output(result),
169 | 							},
170 | 						],
171 | 					};
172 | 				} catch (error) {
173 | 					return {
174 | 						content: [
175 | 							{
176 | 								type: 'text' as const,
177 | 								text: `Error: ${error instanceof Error ? error.message : String(error)}`,
178 | 							},
179 | 						],
180 | 						isError: true,
181 | 					};
182 | 				}
183 | 			},
184 | 		);
185 | 
186 | 		// list_processes tool - read-only
187 | 		this.server.tool(
188 | 			{
189 | 				name: 'list_processes',
190 | 				description: 'List running processes in WSL',
191 | 				schema: v.object({
192 | 					filter: v.optional(
193 | 						v.pipe(
194 | 							v.string(),
195 | 							v.description('Filter by name'),
196 | 						),
197 | 					),
198 | 				}),
199 | 				annotations: {
200 | 					readOnlyHint: true,
201 | 				},
202 | 			},
203 | 			async ({ filter }) => {
204 | 				try {
205 | 					const cmd = filter
206 | 						? `ps aux | grep -i "${filter}" | grep -v grep`
207 | 						: 'ps aux';
208 | 					const result = await this.command_executor.execute_command(cmd);
209 | 					return {
210 | 						content: [
211 | 							{
212 | 								type: 'text' as const,
213 | 								text: this.format_output(result),
214 | 							},
215 | 						],
216 | 					};
217 | 				} catch (error) {
218 | 					return {
219 | 						content: [
220 | 							{
221 | 								type: 'text' as const,
222 | 								text: `Error: ${error instanceof Error ? error.message : String(error)}`,
223 | 							},
224 | 						],
225 | 						isError: true,
226 | 					};
227 | 				}
228 | 			},
229 | 		);
230 | 
231 | 		// get_disk_usage tool - read-only
232 | 		this.server.tool(
233 | 			{
234 | 				name: 'get_disk_usage',
235 | 				description: 'Get disk space information',
236 | 				schema: v.object({
237 | 					path: v.optional(
238 | 						v.pipe(
239 | 							v.string(),
240 | 							v.description('Path to check'),
241 | 						),
242 | 					),
243 | 				}),
244 | 				annotations: {
245 | 					readOnlyHint: true,
246 | 				},
247 | 			},
248 | 			async ({ path }) => {
249 | 				try {
250 | 					const cmd = path ? `df -h "${path}"` : 'df -h';
251 | 					const result = await this.command_executor.execute_command(cmd);
252 | 					return {
253 | 						content: [
254 | 							{
255 | 								type: 'text' as const,
256 | 								text: this.format_output(result),
257 | 							},
258 | 						],
259 | 					};
260 | 				} catch (error) {
261 | 					return {
262 | 						content: [
263 | 							{
264 | 								type: 'text' as const,
265 | 								text: `Error: ${error instanceof Error ? error.message : String(error)}`,
266 | 							},
267 | 						],
268 | 						isError: true,
269 | 					};
270 | 				}
271 | 			},
272 | 		);
273 | 
274 | 		// get_directory_info tool - read-only
275 | 		this.server.tool(
276 | 			{
277 | 				name: 'get_directory_info',
278 | 				description: 'Get directory contents and info',
279 | 				schema: v.object({
280 | 					path: v.optional(
281 | 						v.pipe(
282 | 							v.string(),
283 | 							v.description('Directory path'),
284 | 						),
285 | 					),
286 | 					details: v.optional(
287 | 						v.pipe(
288 | 							v.boolean(),
289 | 							v.description('Show detailed info'),
290 | 						),
291 | 					),
292 | 				}),
293 | 				annotations: {
294 | 					readOnlyHint: true,
295 | 				},
296 | 			},
297 | 			async ({ path, details }) => {
298 | 				try {
299 | 					const dir = path || '.';
300 | 					const cmd = details ? `ls -lah "${dir}"` : `ls -A "${dir}"`;
301 | 					const result = await this.command_executor.execute_command(cmd);
302 | 					return {
303 | 						content: [
304 | 							{
305 | 								type: 'text' as const,
306 | 								text: this.format_output(result),
307 | 							},
308 | 						],
309 | 					};
310 | 				} catch (error) {
311 | 					return {
312 | 						content: [
313 | 							{
314 | 								type: 'text' as const,
315 | 								text: `Error: ${error instanceof Error ? error.message : String(error)}`,
316 | 							},
317 | 						],
318 | 						isError: true,
319 | 					};
320 | 				}
321 | 			},
322 | 		);
323 | 
324 | 		// execute_command tool - potentially destructive
325 | 		this.server.tool(
326 | 			{
327 | 				name: 'execute_command',
328 | 				description: 'Execute a command in WSL (use read-only tools when possible)',
329 | 				schema: v.object({
330 | 					command: v.pipe(
331 | 						v.string(),
332 | 						v.description('Command to execute'),
333 | 					),
334 | 					working_dir: v.optional(
335 | 						v.pipe(
336 | 							v.string(),
337 | 							v.description('Working directory'),
338 | 						),
339 | 					),
340 | 					timeout: v.optional(
341 | 						v.pipe(
342 | 							v.number(),
343 | 							v.description('Timeout (ms)'),
344 | 						),
345 | 					),
346 | 				}),
347 | 				annotations: {
348 | 					readOnlyHint: false,
349 | 					destructiveHint: true,
350 | 				},
351 | 			},
352 | 			async ({ command, working_dir, timeout }) => {
353 | 				try {
354 | 					const result = await this.execute_wsl_command(
355 | 						command,
356 | 						working_dir,
357 | 						timeout,
358 | 					);
359 | 
360 | 					if (result.requires_confirmation) {
361 | 						return {
362 | 							content: [
363 | 								{
364 | 									type: 'text' as const,
365 | 									text: result.stderr,
366 | 								},
367 | 							],
368 | 						};
369 | 					}
370 | 
371 | 					return {
372 | 						content: [
373 | 							{
374 | 								type: 'text' as const,
375 | 								text: this.format_output(result),
376 | 							},
377 | 						],
378 | 					};
379 | 				} catch (error) {
380 | 					return {
381 | 						content: [
382 | 							{
383 | 								type: 'text' as const,
384 | 								text: `Error executing command: ${
385 | 									error instanceof Error
386 | 										? error.message
387 | 										: String(error)
388 | 								}`,
389 | 							},
390 | 						],
391 | 						isError: true,
392 | 					};
393 | 				}
394 | 			},
395 | 		);
396 | 
397 | 		// confirm_command tool
398 | 		this.server.tool(
399 | 			{
400 | 				name: 'confirm_command',
401 | 				description: 'Confirm dangerous command execution',
402 | 				schema: v.object({
403 | 					confirmation_id: v.pipe(
404 | 						v.string(),
405 | 						v.description('Confirmation ID'),
406 | 					),
407 | 					confirm: v.pipe(
408 | 						v.boolean(),
409 | 						v.description('Proceed with execution'),
410 | 					),
411 | 				}),
412 | 				annotations: {
413 | 					readOnlyHint: false,
414 | 					destructiveHint: true,
415 | 				},
416 | 			},
417 | 			async ({ confirmation_id, confirm }) => {
418 | 				try {
419 | 					const pending = this.pending_confirmations.get(confirmation_id);
420 | 					if (!pending) {
421 | 						throw new InvalidConfirmationError(confirmation_id);
422 | 					}
423 | 
424 | 					this.pending_confirmations.delete(confirmation_id);
425 | 
426 | 					if (!confirm) {
427 | 						return {
428 | 							content: [
429 | 								{
430 | 									type: 'text' as const,
431 | 									text: 'Command execution cancelled.',
432 | 								},
433 | 							],
434 | 						};
435 | 					}
436 | 
437 | 					const result = await this.command_executor.execute_command(
438 | 						pending.command,
439 | 						pending.working_dir,
440 | 						pending.timeout,
441 | 					);
442 | 
443 | 					return {
444 | 						content: [
445 | 							{
446 | 								type: 'text' as const,
447 | 								text: this.format_output(result),
448 | 							},
449 | 						],
450 | 					};
451 | 				} catch (error) {
452 | 					return {
453 | 						content: [
454 | 							{
455 | 								type: 'text' as const,
456 | 								text: `Error confirming command: ${
457 | 									error instanceof Error
458 | 										? error.message
459 | 										: String(error)
460 | 								}`,
461 | 							},
462 | 						],
463 | 						isError: true,
464 | 					};
465 | 				}
466 | 			},
467 | 		);
468 | 	}
469 | 
470 | 	async run() {
471 | 		const transport = new StdioTransport(this.server);
472 | 		transport.listen();
473 | 		console.error('WSL MCP server running on stdio');
474 | 	}
475 | }
476 | 
477 | const server = new WslServer();
478 | server.run().catch(console.error);
479 | 
```