UNPKG

ez-mcp

Version:

A simple Model Context Protocol (MCP) server for executing command-line tools across different shell environments (WSL, PowerShell, CMD, Bash). Easy setup for Claude Desktop, GitHub Copilot, LM Studio, and Cursor.

661 lines (645 loc) • 28.8 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { spawn, exec } from "child_process"; import { promisify } from "util"; const execAsync = promisify(exec); // Zod schemas for tool arguments const ExecuteCommandSchema = z.object({ command: z.string().describe("The command to execute"), shell: z.enum(["wsl", "powershell", "cmd", "bash", "zsh", "sh"]).optional().default("powershell").describe("The shell environment to use"), workingDirectory: z.string().optional().describe("Working directory for the command (optional)"), timeout: z.number().optional().default(30000).describe("Command timeout in milliseconds (default: 30000)"), wslDistribution: z.string().optional().describe("WSL distribution name (only for WSL shell)"), confirmed: z.boolean().optional().default(false).describe("Confirmation required for sensitive commands - set to true to bypass safety check"), }); const ListDirectorySchema = z.object({ path: z.string().describe("Directory path to list"), shell: z.enum(["wsl", "powershell", "cmd", "bash", "zsh", "sh"]).optional().default("powershell").describe("The shell environment to use"), wslDistribution: z.string().optional().describe("WSL distribution name (only for WSL shell)"), }); const CheckPathSchema = z.object({ path: z.string().describe("Path to check"), shell: z.enum(["wsl", "powershell", "cmd", "bash", "zsh", "sh"]).optional().default("powershell").describe("The shell environment to use"), wslDistribution: z.string().optional().describe("WSL distribution name (only for WSL shell)"), }); const OpenWindowsAppSchema = z.object({ app: z.string().describe("Application to open - can be executable name, file path, or app name"), arguments: z.string().optional().describe("Command line arguments to pass to the application"), workingDirectory: z.string().optional().describe("Working directory for the application"), method: z.enum(["start", "direct", "explorer"]).optional().default("start").describe("Method to open the app: 'start' (Windows start command), 'direct' (direct execution), 'explorer' (Windows Explorer)"), waitForExit: z.boolean().optional().default(false).describe("Whether to wait for the application to exit before returning"), confirmed: z.boolean().optional().default(false).describe("Confirmation required for sensitive operations"), }); // Define sensitive command patterns that require confirmation const SENSITIVE_PATTERNS = [ // CRITICAL SYSTEM DESTRUCTION COMMANDS - NEVER ALLOW WITHOUT CONFIRMATION /\b(format|fdisk|diskpart)\b/i, /\b(rm\s+.*-rf|rm\s+-rf)\b/i, /\b(del|delete)\s+.*[\\\/]\*|rmdir\s+.*\/s/i, /\b(shutdown|restart|reboot|halt)\b/i, // Registry and system configuration /\b(reg\s+delete|regedit)\b/i, /\b(bcdedit|bootrec)\b/i, // File system dangerous operations /\b(move|mv|copy|cp)\s+.*\s+(c:|d:|system32|windows|program\s+files)/i, /\b(attrib|cacls|icacls)\s+.*\/s\b/i, /\b(takeown|robocopy)\s+.*\/purge/i, // Network and security modifications /\b(netsh|net\s+user|net\s+localgroup)\b/i, /\b(sc\s+delete|sc\s+create|sc\s+config)\b/i, /\b(powershell.*-encodedcommand)\b/i, /\b(wevtutil.*cl)\b/i, // Process and service dangerous operations /\b(taskkill|stop-process).*(-force|-f)\b/i, /\b(taskkill).*\/f\b/i, /\b(wmic.*delete)\b/i, /\b(get-process.*stop-process)\b/i, // Administrative and system tools /\b(sfc|dism)\s+.*\/online/i, /\b(chkdsk)\s+.*\/f/i, // Package managers with global changes /\b(msiexec|chocolatey|choco\s+uninstall)\b/i, /\b(npm\s+(uninstall).*-g)\b/i, /\b(pip\s+uninstall)\b/i, // System paths and critical directories /[cC]:\\(windows|system32|program\s+files)/, /\/etc\/|\/usr\/bin\/|\/bin\/|\/sbin\//, /\/(root|home)\/.*rm/, // Database operations /\b(drop\s+database|truncate\s+table)\b/i, // Docker/Container dangerous operations /\b(docker\s+(system\s+prune|rmi).*-f)\b/i, /\b(kubectl\s+delete)\b/i, // Git destructive operations /\b(git\s+(reset\s+--hard|clean\s+-fd|branch\s+-D))\b/i, ]; class CommandLineMCPServer { server; constructor() { this.server = new Server({ name: "commandline-tools-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); this.setupErrorHandling(); } setupErrorHandling() { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "execute_command", description: "Execute a command in the specified shell environment (WSL, PowerShell, CMD, Bash, etc.)", inputSchema: { type: "object", properties: { command: { type: "string", description: "The command to execute", }, shell: { type: "string", enum: ["wsl", "powershell", "cmd", "bash", "zsh", "sh"], default: "powershell", description: "The shell environment to use", }, workingDirectory: { type: "string", description: "Working directory for the command (optional)", }, timeout: { type: "number", default: 30000, description: "Command timeout in milliseconds (default: 30000)", }, wslDistribution: { type: "string", description: "WSL distribution name (only for WSL shell)", }, confirmed: { type: "boolean", default: false, description: "Confirmation required for sensitive commands - set to true to bypass safety check", }, }, required: ["command"], }, }, { name: "list_directory", description: "List contents of a directory in the specified shell environment", inputSchema: { type: "object", properties: { path: { type: "string", description: "Directory path to list", }, shell: { type: "string", enum: ["wsl", "powershell", "cmd", "bash", "zsh", "sh"], default: "powershell", description: "The shell environment to use", }, wslDistribution: { type: "string", description: "WSL distribution name (only for WSL shell)", }, }, required: ["path"], }, }, { name: "check_path", description: "Check if a path exists and get its information", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to check", }, shell: { type: "string", enum: ["wsl", "powershell", "cmd", "bash", "zsh", "sh"], default: "powershell", description: "The shell environment to use", }, wslDistribution: { type: "string", description: "WSL distribution name (only for WSL shell)", }, }, required: ["path"], }, }, { name: "open_windows_app", description: "Open a Windows application by name, executable path, or through Windows Start menu", inputSchema: { type: "object", properties: { app: { type: "string", description: "Application to open - can be executable name (notepad, calc), file path (C:\\Program Files\\...), or app name (Visual Studio Code)", }, arguments: { type: "string", description: "Command line arguments to pass to the application", }, workingDirectory: { type: "string", description: "Working directory for the application", }, method: { type: "string", enum: ["start", "direct", "explorer"], default: "start", description: "Method to open the app: 'start' (Windows start command), 'direct' (direct execution), 'explorer' (Windows Explorer)", }, waitForExit: { type: "boolean", default: false, description: "Whether to wait for the application to exit before returning", }, confirmed: { type: "boolean", default: false, description: "Confirmation required for sensitive operations", }, }, required: ["app"], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case "execute_command": return await this.executeCommand(request.params.arguments); case "list_directory": return await this.listDirectory(request.params.arguments); case "check_path": return await this.checkPath(request.params.arguments); case "open_windows_app": return await this.openWindowsApp(request.params.arguments); default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } isSensitiveCommand(command) { return SENSITIVE_PATTERNS.some(pattern => pattern.test(command)); } getSensitiveCommandWarning(command) { const matchedPatterns = []; SENSITIVE_PATTERNS.forEach(pattern => { if (pattern.test(command)) { // Extract the pattern description based on common sensitive operations if (pattern.source.includes("format|fdisk|diskpart")) { matchedPatterns.push("šŸ”„ DISK FORMATTING/PARTITIONING - PERMANENT DATA LOSS"); } else if (pattern.source.includes("rm.*-rf")) { matchedPatterns.push("šŸ”„ RECURSIVE FORCE DELETE - IRREVERSIBLE"); } else if (pattern.source.includes("del|delete|rmdir")) { matchedPatterns.push("[WARN] File/Directory Deletion"); } else if (pattern.source.includes("shutdown|restart|reboot")) { matchedPatterns.push("[RESTART] System Shutdown/Restart"); } else if (pattern.source.includes("reg|bcdedit")) { matchedPatterns.push("āš™ļø Critical System Configuration"); } else if (pattern.source.includes("net|netsh")) { matchedPatterns.push("🌐 Network Security Configuration"); } else if (pattern.source.includes("taskkill.*-f")) { matchedPatterns.push("šŸ’€ Force Process Termination"); } else if (pattern.source.includes("system32|windows|etc|usr")) { matchedPatterns.push("🚨 CRITICAL SYSTEM DIRECTORY ACCESS"); } else if (pattern.source.includes("docker.*-f|kubectl")) { matchedPatterns.push("🐳 Container/Infrastructure Destruction"); } else if (pattern.source.includes("git.*reset.*hard")) { matchedPatterns.push("šŸ“¦ Git History Destruction"); } else { matchedPatterns.push("[WARN] Potentially Dangerous Administrative Operation"); } } }); return `🚨 DANGEROUS COMMAND BLOCKED 🚨 [BLOCKED] EXECUTION REFUSED FOR SAFETY The following command contains HIGHLY DANGEROUS operations: ${matchedPatterns.map(p => ` ${p}`).join('\n')} šŸ” Command: "${command}" [WARN] POTENTIAL CONSEQUENCES: • Permanent data loss • System corruption or instability • Security vulnerabilities • Irreversible changes • Complete system failure šŸ›”ļø SECURITY REQUIREMENT: This command will ONLY execute if you explicitly set "confirmed": true [WARN] USE WITH EXTREME CAUTION: { "command": "${command}", "confirmed": true } šŸ’” Consider safer alternatives or verify the command is absolutely necessary. šŸ”’ This protection exists to prevent accidental system damage.`; } buildCommand(command, shell, wslDistribution, workingDirectory) { switch (shell) { case "wsl": const wslCmd = wslDistribution ? ["wsl", "-d", wslDistribution] : ["wsl"]; if (workingDirectory) { return { cmd: "wsl", args: wslDistribution ? ["-d", wslDistribution, "--cd", workingDirectory, "--", "bash", "-c", command] : ["--cd", workingDirectory, "--", "bash", "-c", command] }; } return { cmd: "wsl", args: wslDistribution ? ["-d", wslDistribution, "--", "bash", "-c", command] : ["--", "bash", "-c", command] }; case "powershell": const psArgs = ["-NoProfile", "-Command"]; if (workingDirectory) { psArgs.push(`Set-Location '${workingDirectory}'; ${command}`); } else { psArgs.push(command); } return { cmd: "powershell.exe", args: psArgs }; case "cmd": const cmdArgs = ["/c"]; if (workingDirectory) { cmdArgs.push(`cd /d "${workingDirectory}" && ${command}`); } else { cmdArgs.push(command); } return { cmd: "cmd.exe", args: cmdArgs }; case "bash": case "zsh": case "sh": const shellArgs = ["-c"]; if (workingDirectory) { shellArgs.push(`cd "${workingDirectory}" && ${command}`); } else { shellArgs.push(command); } return { cmd: shell, args: shellArgs }; default: throw new Error(`Unsupported shell: ${shell}`); } } async executeCommand(args) { const parsed = ExecuteCommandSchema.parse(args); const { command, shell, workingDirectory, timeout, wslDistribution, confirmed } = parsed; // CRITICAL SECURITY CHECK: Block dangerous commands without confirmation if (this.isSensitiveCommand(command)) { if (!confirmed) { return { content: [ { type: "text", text: this.getSensitiveCommandWarning(command), }, ], isError: true, }; } else { // Even with confirmation, log the dangerous command execution console.error(`[WARN] DANGEROUS COMMAND EXECUTED WITH CONFIRMATION: ${command}`); console.error(`Shell: ${shell}, Working Dir: ${workingDirectory || 'default'}`); console.error(`Timestamp: ${new Date().toISOString()}`); } } try { const { cmd, args: cmdArgs } = this.buildCommand(command, shell, wslDistribution, workingDirectory); return new Promise((resolve) => { const child = spawn(cmd, cmdArgs, { stdio: ["pipe", "pipe", "pipe"], shell: false, }); let stdout = ""; let stderr = ""; child.stdout?.on("data", (data) => { stdout += data.toString(); }); child.stderr?.on("data", (data) => { stderr += data.toString(); }); const timeoutId = setTimeout(() => { child.kill("SIGTERM"); resolve({ content: [ { type: "text", text: `Command timed out after ${timeout}ms\nPartial stdout: ${stdout}\nPartial stderr: ${stderr}`, }, ], isError: true, }); }, timeout); child.on("close", (code) => { clearTimeout(timeoutId); const result = { command, shell, workingDirectory, exitCode: code, stdout: stdout.trim(), stderr: stderr.trim(), }; resolve({ content: [ { type: "text", text: `Command executed successfully: Shell: ${shell}${wslDistribution ? ` (${wslDistribution})` : ""} Working Directory: ${workingDirectory || "default"} Exit Code: ${code} Command: ${command} --- STDOUT --- ${result.stdout || "(no output)"} ${result.stderr ? `--- STDERR ---\n${result.stderr}` : ""}`, }, ], }); }); child.on("error", (error) => { clearTimeout(timeoutId); resolve({ content: [ { type: "text", text: `Failed to execute command: ${error.message}`, }, ], isError: true, }); }); }); } catch (error) { return { content: [ { type: "text", text: `Error executing command: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } async listDirectory(args) { const parsed = ListDirectorySchema.parse(args); const { path: dirPath, shell, wslDistribution } = parsed; const listCommand = shell === "wsl" ? `ls -la "${dirPath}"` : shell === "powershell" ? `Get-ChildItem -Path "${dirPath}" | Format-Table -AutoSize` : `dir "${dirPath}"`; return await this.executeCommand({ command: listCommand, shell, wslDistribution, }); } async checkPath(args) { const parsed = CheckPathSchema.parse(args); const { path: checkPath, shell, wslDistribution } = parsed; const checkCommand = shell === "wsl" ? `stat "${checkPath}" 2>/dev/null || echo "Path does not exist"` : shell === "powershell" ? `Test-Path "${checkPath}"; if (Test-Path "${checkPath}") { Get-Item "${checkPath}" | Format-List }` : `if exist "${checkPath}" (echo Exists && dir "${checkPath}") else (echo "Path does not exist")`; return await this.executeCommand({ command: checkCommand, shell, wslDistribution, }); } async openWindowsApp(args) { const parsed = OpenWindowsAppSchema.parse(args); const { app, arguments: appArgs, workingDirectory, method, waitForExit, confirmed } = parsed; // Check if opening certain system apps or executables requires confirmation const isSensitiveApp = (appName, args) => { const sensitiveApps = [ /regedit/i, /cmd|command/i, /powershell/i, /control/i, /msconfig/i, /services\.msc/i, /gpedit\.msc/i, /compmgmt\.msc/i, /\\system32\\/i, /\\windows\\/i, /format/i, /diskpart/i, ]; const fullCommand = `${appName} ${args || ''}`; return sensitiveApps.some(pattern => pattern.test(fullCommand)) || this.isSensitiveCommand(fullCommand); }; if (isSensitiveApp(app, appArgs) && !confirmed) { return { content: [ { type: "text", text: `[WARN] SENSITIVE APPLICATION DETECTED [WARN] You are attempting to open: ${app}${appArgs ? ` with arguments: ${appArgs}` : ''} This application may: - Modify system settings - Access system files - Require administrative privileges - Affect system stability To proceed, you must set the 'confirmed' parameter to true. Example: { "app": "${app}", "confirmed": true }`, }, ], isError: true, }; } try { let command; let shell = "cmd"; switch (method) { case "start": // Use Windows 'start' command which can handle both executables and registered apps command = `start`; if (workingDirectory) { command += ` /D "${workingDirectory}"`; } if (waitForExit) { command += ` /WAIT`; } command += ` "" "${app}"`; if (appArgs) { command += ` ${appArgs}`; } break; case "direct": // Direct execution of the application command = `"${app}"`; if (appArgs) { command += ` ${appArgs}`; } break; case "explorer": // Use Windows Explorer to open the application/file command = `explorer`; if (appArgs) { command += ` ${appArgs}`; } command += ` "${app}"`; break; default: throw new Error(`Unsupported method: ${method}`); } // For PowerShell, we might want to handle certain apps better if (method === "start" && (app.includes("\\") || app.includes("/"))) { // If it's a path, use PowerShell for better handling shell = "powershell"; command = `Start-Process`; if (workingDirectory) { command += ` -WorkingDirectory "${workingDirectory}"`; } if (waitForExit) { command += ` -Wait`; } command += ` -FilePath "${app}"`; if (appArgs) { command += ` -ArgumentList "${appArgs}"`; } } const result = await this.executeCommand({ command, shell, workingDirectory, timeout: waitForExit ? 300000 : 10000, // 5 minutes if waiting, 10 seconds otherwise }); // Enhance the response with app opening context if (result.content?.[0]?.type === "text") { const originalText = result.content[0].text; result.content[0].text = `Windows Application Opened: App: ${app} Method: ${method} Arguments: ${appArgs || "none"} Working Directory: ${workingDirectory || "default"} Wait for Exit: ${waitForExit} ${originalText}`; } return result; } catch (error) { return { content: [ { type: "text", text: `Error opening Windows application: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Command-line Tools MCP Server running on stdio"); } } // Check command line arguments const args = process.argv.slice(2); if (args.length > 0 && args[0] === 'setup') { // Import and run setup utility import('./setup.js').then(async () => { // The setup.js module will handle the setup logic }).catch((error) => { console.error("Failed to load setup utility:", error); process.exit(1); }); } else { // Start the MCP server const server = new CommandLineMCPServer(); server.start().catch((error) => { console.error("Failed to start server:", error); process.exit(1); }); }