# 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.9.2",
"typescript": "^5.9.3"
},
"dependencies": {
"@tmcp/adapter-valibot": "^0.1.4",
"@tmcp/transport-stdio": "^0.4.0",
"tmcp": "^1.16.1",
"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);
```