# 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: -------------------------------------------------------------------------------- ``` # Package Managers package-lock.json pnpm-lock.yaml yarn.lock ``` -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` { "useTabs": true, "singleQuote": true, "trailingComma": "all", "printWidth": 70, "proseWrap": "always" } ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ .pnpm-store/ # Build output dist/ build/ # Environment variables .env .env.local .env.*.local # IDE .vscode/ .idea/ *.swp *.swo # Logs *.log npm-debug.log* pnpm-debug.log* # Testing coverage/ # Database files *.db *.db-journal # OS .DS_Store Thumbs.db ``` -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- ```markdown # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # mcp-wsl-exec 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. <a href="https://glama.ai/mcp/servers/wv6df94kb8"> <img width="380" height="200" src="https://glama.ai/mcp/servers/wv6df94kb8/badge" /> </a> ## ⚠️ Important: Who Should Use This? **✅ You SHOULD use this if:** - You're using **Claude Desktop on Windows** - You need to interact with your WSL environment - You want to provide WSL context to Claude (system info, processes, files, etc.) **❌ You DON'T need this if:** - You're using **Claude Code** (it has native bash access) - You're on Linux/macOS (use native tools instead) - You only need Windows PowerShell/CMD (use a different MCP server) ## Features ### 📊 Information Gathering (Read-Only) - 🖥️ Get system information (OS, kernel, hostname) - 📁 Browse directory contents - 💾 Check disk usage - ⚙️ List environment variables - 🔄 Monitor running processes ### 🔧 Command Execution (With Safety) - 🔒 Secure command execution in WSL environments - ⚡ Built-in safety features: - Dangerous command detection - Command confirmation system - Path traversal prevention - Command sanitization - 📁 Working directory support - ⏱️ Command timeout functionality - 🛡️ Protection against shell injection ## Configuration This server requires configuration through your MCP client. Here are examples for different environments: ### Cline Configuration Add this to your Cline MCP settings: ```json { "mcpServers": { "mcp-wsl-exec": { "command": "npx", "args": ["-y", "mcp-wsl-exec"] } } } ``` ### Claude Desktop Configuration Add this to your Claude Desktop configuration: ```json { "mcpServers": { "mcp-wsl-exec": { "command": "npx", "args": ["-y", "mcp-wsl-exec"] } } } ``` ## API The server provides 7 MCP tools: ### Information Gathering (Read-Only) 📊 These tools provide context about your WSL environment without making changes: #### get_system_info Get system information (OS version, kernel, hostname). **Parameters:** None #### get_directory_info Get directory contents and file information. **Parameters:** - `path` (string, optional): Directory path (defaults to current directory) - `details` (boolean, optional): Show detailed information (permissions, sizes, etc.) #### get_disk_usage Get disk space information. **Parameters:** - `path` (string, optional): Specific path to check (defaults to all filesystems) #### get_environment Get environment variables. **Parameters:** - `filter` (string, optional): Filter pattern to search for specific variables #### list_processes List running processes. **Parameters:** - `filter` (string, optional): Filter by process name ### Command Execution (Potentially Destructive) 🔧 Use these tools when you need to make changes or run custom commands: #### execute_command Execute a command in WSL with safety checks and validation. **Parameters:** - `command` (string, required): Command to execute - `working_dir` (string, optional): Working directory for command execution - `timeout` (number, optional): Timeout in milliseconds **Note:** Dangerous commands will require confirmation via `confirm_command`. #### confirm_command Confirm execution of a dangerous command that was flagged by safety checks. **Parameters:** - `confirmation_id` (string, required): Confirmation ID received from execute_command - `confirm` (boolean, required): Whether to proceed with the command execution ## Safety Features ### Dangerous Command Detection The server maintains a list of potentially dangerous commands that require explicit confirmation before execution, including: - File system operations (rm, rmdir, mv) - System commands (shutdown, reboot) - Package management (apt, yum, dnf) - File redirections (>, >>) - Permission changes (chmod, chown) - And more... ### Command Sanitization All commands are sanitized to prevent: - Shell metacharacter injection - Path traversal attempts - Home directory references - Dangerous command chaining ## Development ### Setup 1. Clone the repository 2. Install dependencies: ```bash pnpm install ``` 3. Build the project: ```bash pnpm build ``` 4. Run in development mode: ```bash pnpm dev ``` ### Publishing The project uses changesets for version management. To publish: 1. Create a changeset: ```bash pnpm changeset ``` 2. Version the package: ```bash pnpm changeset version ``` 3. Publish to npm: ```bash pnpm release ``` ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License MIT License - see the [LICENSE](LICENSE) file for details. ## Acknowledgments - Built on the [Model Context Protocol](https://github.com/modelcontextprotocol) - Designed for secure WSL command execution ``` -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- ```json { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ] } ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # mcp-wsl-exec ## 0.0.3 ### Patch Changes - dd1155f: Migrate to tmcp, add read-only info tools, clarify Windows+Claude Desktop focus ## 0.0.2 ### Patch Changes - glama badge ## 0.0.1 ### Patch Changes - init ``` -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- ```json { "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "ES2020", "moduleResolution": "node", "esModuleInterop": true, "strict": true, "outDir": "dist", "rootDir": "src", "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript export interface CommandResponse { stdout: string; stderr: string; exit_code: number | null; command: string; requires_confirmation?: boolean; error?: string; working_dir?: string; } export interface PendingConfirmation { command: string; working_dir?: string; timeout?: number; resolve: (value: CommandResponse) => void; reject: (reason?: any) => void; } ``` -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- ```typescript // Define dangerous commands that require confirmation export const dangerous_commands = [ 'rm', 'rmdir', 'dd', 'mkfs', 'mkswap', 'fdisk', 'shutdown', 'reboot', '>', // redirect that could overwrite '>>', // append redirect that could modify files 'format', 'chmod', 'chown', 'sudo', 'su', 'passwd', 'mv', // moving files can be dangerous 'find -delete', 'truncate', 'shred', 'kill', 'pkill', 'service', 'systemctl', 'mount', 'umount', 'apt', 'apt-get', 'dpkg', 'yum', 'dnf', 'pacman', ] as const; // WSL process configuration export const wsl_config = { executable: 'wsl.exe', shell: 'bash', default_timeout: 30000, // 30 seconds } as const; ``` -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- ```typescript export class WslExecutionError extends Error { constructor( message: string, public readonly details?: any, ) { super(message); this.name = 'WslExecutionError'; } } export class CommandValidationError extends WslExecutionError { constructor(message: string, details?: any) { super(message, details); this.name = 'CommandValidationError'; } } export class CommandTimeoutError extends WslExecutionError { constructor(timeout: number) { super(`Command timed out after ${timeout}ms`, { timeout }); this.name = 'CommandTimeoutError'; } } export class InvalidConfirmationError extends WslExecutionError { constructor(confirmation_id: string) { super('Invalid or expired confirmation ID', { confirmation_id }); this.name = 'InvalidConfirmationError'; } } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-wsl-exec", "version": "0.0.3", "description": "A secure Model Context Protocol (MCP) server for executing commands in Windows Subsystem for Linux (WSL) with built-in safety features and validation", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { "mcp-wsl-exec": "./dist/index.js" }, "files": [ "dist", "README.md", "LICENSE" ], "scripts": { "build": "tsc && chmod +x dist/index.js", "start": "node dist/index.js", "dev": "npx @modelcontextprotocol/inspector dist/index.js", "changeset": "changeset", "version": "changeset version", "release": "pnpm run build && changeset publish" }, "keywords": [ "mcp", "model-context-protocol", "wsl", "exec", "command-execution", "windows-subsystem-linux", "security", "command-line", "cli", "shell", "bash", "linux", "windows", "safe-execution", "command-validation", "path-validation", "timeout", "error-handling" ], "author": "Scott Spence", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/spences10/mcp-wsl-exec.git" }, "bugs": { "url": "https://github.com/spences10/mcp-wsl-exec/issues" }, "homepage": "https://github.com/spences10/mcp-wsl-exec#readme", "devDependencies": { "@changesets/cli": "^2.29.7", "@types/node": "^24.6.1", "typescript": "^5.9.3" }, "dependencies": { "@tmcp/adapter-valibot": "^0.1.4", "@tmcp/transport-stdio": "^0.3.1", "tmcp": "^1.14.0", "valibot": "^1.1.0" } } ``` -------------------------------------------------------------------------------- /src/command-executor.ts: -------------------------------------------------------------------------------- ```typescript import { spawn } from 'child_process'; import { dangerous_commands, wsl_config } from './constants.js'; import { CommandValidationError, CommandTimeoutError } from './errors.js'; import { CommandResponse } from './types.js'; export class CommandExecutor { private sanitize_command(command: string): string { // Enhanced command sanitization const sanitized = command .replace(/[;&|`$]/g, '') // Remove shell metacharacters .replace(/\\/g, '/') // Normalize path separators .replace(/\.\./g, '') // Remove parent directory references .replace(/~/g, '') // Remove home directory references .trim(); // Remove leading/trailing whitespace // Check for empty command after sanitization if (!sanitized) { throw new CommandValidationError( 'Invalid command: Empty after sanitization', ); } return sanitized; } private validate_working_dir(working_dir?: string): string | undefined { if (!working_dir) return undefined; // Sanitize and validate working directory const sanitized = working_dir .replace(/[;&|`$]/g, '') .replace(/\\/g, '/') .trim(); if (!sanitized) { throw new CommandValidationError('Invalid working directory'); } return sanitized; } private validate_timeout(timeout?: number): number | undefined { if (!timeout) return undefined; if (isNaN(timeout) || timeout < 0) { throw new CommandValidationError('Invalid timeout value'); } return timeout; } public is_dangerous_command(command: string): boolean { return dangerous_commands.some( (dangerous) => command.toLowerCase().includes(dangerous.toLowerCase()) || command.match(new RegExp(`\\b${dangerous}\\b`, 'i')), ); } public async execute_command( command: string, working_dir?: string, timeout?: number, ): Promise<CommandResponse> { return new Promise((resolve, reject) => { const sanitized_command = this.sanitize_command(command); const validated_dir = this.validate_working_dir(working_dir); const validated_timeout = this.validate_timeout(timeout); const cd_command = validated_dir ? `cd "${validated_dir}" && ` : ''; const full_command = `${cd_command}${sanitized_command}`; const wsl_process = spawn(wsl_config.executable, [ '--exec', wsl_config.shell, '-c', full_command, ]); let stdout = ''; let stderr = ''; wsl_process.stdout.on('data', (data) => { stdout += data.toString(); }); wsl_process.stderr.on('data', (data) => { stderr += data.toString(); }); let timeout_id: NodeJS.Timeout | undefined; if (validated_timeout) { timeout_id = setTimeout(() => { wsl_process.kill(); reject(new CommandTimeoutError(validated_timeout)); }, validated_timeout); } wsl_process.on('close', (code) => { if (timeout_id) { clearTimeout(timeout_id); } resolve({ stdout, stderr, exit_code: code, command: sanitized_command, working_dir: validated_dir, }); }); wsl_process.on('error', (error) => { if (timeout_id) { clearTimeout(timeout_id); } reject(error); }); }); } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { McpServer } from 'tmcp'; import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot'; import { StdioTransport } from '@tmcp/transport-stdio'; import * as v from 'valibot'; import type { GenericSchema } from 'valibot'; import { readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { CommandExecutor } from './command-executor.js'; import { InvalidConfirmationError } from './errors.js'; import { CommandResponse, PendingConfirmation } from './types.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const pkg = JSON.parse( readFileSync(join(__dirname, '..', 'package.json'), 'utf8'), ); const { name, version } = pkg; class WslServer { private server: McpServer<GenericSchema>; private command_executor: CommandExecutor; private pending_confirmations: Map<string, PendingConfirmation>; constructor() { const adapter = new ValibotJsonSchemaAdapter(); this.server = new McpServer<GenericSchema>( { name, version, description: 'A secure MCP server for executing commands in WSL with built-in safety features', }, { adapter, capabilities: { tools: { listChanged: true }, }, }, ); this.command_executor = new CommandExecutor(); this.pending_confirmations = new Map(); this.setup_tool_handlers(); } private format_output(result: CommandResponse): string { return [ `Command: ${result.command}`, result.working_dir ? `Working Directory: ${result.working_dir}` : null, `Exit Code: ${result.exit_code}`, result.stdout.trim() ? `Output:\n${result.stdout.trim()}` : 'No output', result.stderr.trim() ? `Errors:\n${result.stderr.trim()}` : 'No errors', result.error ? `Error: ${result.error}` : null, ] .filter(Boolean) .join('\n'); } private async execute_wsl_command( command: string, working_dir?: string, timeout?: number, ): Promise<CommandResponse> { return new Promise((resolve, reject) => { const requires_confirmation = this.command_executor.is_dangerous_command(command); if (requires_confirmation) { // Generate a unique confirmation ID const confirmation_id = Math.random() .toString(36) .substring(7); this.pending_confirmations.set(confirmation_id, { command, working_dir, timeout, resolve, reject, }); // Return early with confirmation request resolve({ stdout: '', stderr: `Command "${command}" requires confirmation. Use confirm_command with ID: ${confirmation_id}`, exit_code: null, command, requires_confirmation: true, }); return; } this.command_executor .execute_command(command, working_dir, timeout) .then(resolve) .catch(reject); }); } private setup_tool_handlers() { // get_system_info tool - read-only this.server.tool( { name: 'get_system_info', description: 'Get WSL system information', annotations: { readOnlyHint: true, }, }, async () => { try { const result = await this.command_executor.execute_command( 'uname -a && lsb_release -a 2>/dev/null || cat /etc/os-release', ); return { content: [ { type: 'text' as const, text: this.format_output(result), }, ], }; } catch (error) { return { content: [ { type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); // get_environment tool - read-only this.server.tool( { name: 'get_environment', description: 'Get WSL environment variables', schema: v.object({ filter: v.optional( v.pipe( v.string(), v.description('Filter pattern (grep)'), ), ), }), annotations: { readOnlyHint: true, }, }, async ({ filter }) => { try { const cmd = filter ? `env | grep -i "${filter}"` : 'env'; const result = await this.command_executor.execute_command(cmd); return { content: [ { type: 'text' as const, text: this.format_output(result), }, ], }; } catch (error) { return { content: [ { type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); // list_processes tool - read-only this.server.tool( { name: 'list_processes', description: 'List running processes in WSL', schema: v.object({ filter: v.optional( v.pipe( v.string(), v.description('Filter by name'), ), ), }), annotations: { readOnlyHint: true, }, }, async ({ filter }) => { try { const cmd = filter ? `ps aux | grep -i "${filter}" | grep -v grep` : 'ps aux'; const result = await this.command_executor.execute_command(cmd); return { content: [ { type: 'text' as const, text: this.format_output(result), }, ], }; } catch (error) { return { content: [ { type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); // get_disk_usage tool - read-only this.server.tool( { name: 'get_disk_usage', description: 'Get disk space information', schema: v.object({ path: v.optional( v.pipe( v.string(), v.description('Path to check'), ), ), }), annotations: { readOnlyHint: true, }, }, async ({ path }) => { try { const cmd = path ? `df -h "${path}"` : 'df -h'; const result = await this.command_executor.execute_command(cmd); return { content: [ { type: 'text' as const, text: this.format_output(result), }, ], }; } catch (error) { return { content: [ { type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); // get_directory_info tool - read-only this.server.tool( { name: 'get_directory_info', description: 'Get directory contents and info', schema: v.object({ path: v.optional( v.pipe( v.string(), v.description('Directory path'), ), ), details: v.optional( v.pipe( v.boolean(), v.description('Show detailed info'), ), ), }), annotations: { readOnlyHint: true, }, }, async ({ path, details }) => { try { const dir = path || '.'; const cmd = details ? `ls -lah "${dir}"` : `ls -A "${dir}"`; const result = await this.command_executor.execute_command(cmd); return { content: [ { type: 'text' as const, text: this.format_output(result), }, ], }; } catch (error) { return { content: [ { type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); // execute_command tool - potentially destructive this.server.tool( { name: 'execute_command', description: 'Execute a command in WSL (use read-only tools when possible)', schema: v.object({ command: v.pipe( v.string(), v.description('Command to execute'), ), working_dir: v.optional( v.pipe( v.string(), v.description('Working directory'), ), ), timeout: v.optional( v.pipe( v.number(), v.description('Timeout (ms)'), ), ), }), annotations: { readOnlyHint: false, destructiveHint: true, }, }, async ({ command, working_dir, timeout }) => { try { const result = await this.execute_wsl_command( command, working_dir, timeout, ); if (result.requires_confirmation) { return { content: [ { type: 'text' as const, text: result.stderr, }, ], }; } return { content: [ { type: 'text' as const, text: this.format_output(result), }, ], }; } catch (error) { return { content: [ { type: 'text' as const, text: `Error executing command: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } }, ); // confirm_command tool this.server.tool( { name: 'confirm_command', description: 'Confirm dangerous command execution', schema: v.object({ confirmation_id: v.pipe( v.string(), v.description('Confirmation ID'), ), confirm: v.pipe( v.boolean(), v.description('Proceed with execution'), ), }), annotations: { readOnlyHint: false, destructiveHint: true, }, }, async ({ confirmation_id, confirm }) => { try { const pending = this.pending_confirmations.get(confirmation_id); if (!pending) { throw new InvalidConfirmationError(confirmation_id); } this.pending_confirmations.delete(confirmation_id); if (!confirm) { return { content: [ { type: 'text' as const, text: 'Command execution cancelled.', }, ], }; } const result = await this.command_executor.execute_command( pending.command, pending.working_dir, pending.timeout, ); return { content: [ { type: 'text' as const, text: this.format_output(result), }, ], }; } catch (error) { return { content: [ { type: 'text' as const, text: `Error confirming command: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } }, ); } async run() { const transport = new StdioTransport(this.server); transport.listen(); console.error('WSL MCP server running on stdio'); } } const server = new WslServer(); server.run().catch(console.error); ```