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
JavaScript
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);
});
}