# 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 | ```