# Directory Structure ``` ├── .claude │ └── settings.local.json ├── .github │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── checks.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ └── npm-publish.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── docs │ └── adr │ ├── 001-command-security-levels.md │ ├── 002-mcp-for-shell-commands.md │ ├── 003-command-approval-workflow.md │ └── 004-cross-platform-support.md ├── eslint.config.js ├── examples │ └── client-example.js ├── jest.config.cjs ├── jest.setup.cjs ├── LICENSE ├── logs │ └── .gitkeep ├── manifest.json ├── mcp-settings-example.json ├── package-lock.json ├── package.json ├── README.md ├── run.sh ├── scripts │ └── make-executable.js ├── smithery.yaml ├── src │ ├── index.ts │ ├── services │ │ └── command-service.ts │ └── utils │ ├── command-whitelist-utils.ts │ ├── logger.ts │ └── platform-utils.ts ├── super-shell-mcp.dxt ├── tests │ ├── command-service.platform.test.js │ └── command-service.test.js └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- ``` 1 | # This file ensures the logs directory is tracked by Git 2 | # Log files themselves are ignored via .gitignore ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | yarn.lock 7 | 8 | # Build output 9 | build/ 10 | dist/ 11 | *.tsbuildinfo 12 | 13 | # Environment variables 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | # IDE and editor files 21 | .idea/ 22 | .vscode/ 23 | *.swp 24 | *.swo 25 | .DS_Store 26 | 27 | # Logs 28 | logs/*.log ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://mseep.ai/app/cfdude-super-shell-mcp) 2 | 3 | # Super Shell MCP Server 4 | 5 | [](https://smithery.ai/package/@cfdude/super-shell-mcp) 6 | 7 | An MCP (Model Context Protocol) server for executing shell commands across multiple platforms (Windows, macOS, Linux). This server provides a secure way to execute shell commands with built-in whitelisting and approval mechanisms. 8 | 9 | > 🎉 **Now available as a Claude Desktop Extension!** Install with one click using the `.dxt` package - no developer tools or configuration required. 10 | 11 | ## Features 12 | 13 | - Execute shell commands through MCP on Windows, macOS, and Linux 14 | - Automatic platform detection and shell selection 15 | - Support for multiple shells: 16 | - **Windows**: cmd.exe, PowerShell 17 | - **macOS**: zsh, bash, sh 18 | - **Linux**: bash, sh, zsh 19 | - Command whitelisting with security levels: 20 | - **Safe**: Commands that can be executed without approval 21 | - **Requires Approval**: Commands that need explicit approval before execution 22 | - **Forbidden**: Commands that are explicitly blocked 23 | - Platform-specific command whitelists 24 | - Non-blocking approval workflow for potentially dangerous commands 25 | - Comprehensive logging system with file-based logs 26 | - Comprehensive command management tools 27 | - Platform information tool for diagnostics 28 | 29 | ## Installation 30 | 31 | ### Option 1: Claude Desktop Extension (.dxt) - Recommended 32 | 33 | **One-Click Installation for Claude Desktop:** 34 | 35 | 1. **Download** the `super-shell-mcp.dxt` file from the [latest release](https://github.com/cfdude/super-shell-mcp/releases) 36 | 2. **Quick Install**: Double-click the `.dxt` file while Claude Desktop is open 37 | 38 | **OR** 39 | 40 | **Manual Install**: 41 | - Open Claude Desktop 42 | - Go to **Settings** > **Extensions** 43 | - Click **"Add Extension"** 44 | - Select the downloaded `super-shell-mcp.dxt` file 45 | 46 | 3. **Configure** (optional): Set custom shell path if needed 47 | 4. **Start using** - The extension is ready to use immediately! 48 | 49 | ✅ **Benefits of DXT Installation:** 50 | - No developer tools required (Node.js, Python, etc.) 51 | - No manual configuration files 52 | - Automatic dependency management 53 | - One-click installation and updates 54 | - Secure credential storage in OS keychain 55 | 56 | ### Option 2: Installing via Smithery 57 | 58 | To install Super Shell MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/package/@cfdude/super-shell-mcp): 59 | 60 | ```bash 61 | npx -y @smithery/cli install @cfdude/super-shell-mcp --client claude 62 | ``` 63 | 64 | ### Option 3: Installing Manually 65 | 66 | ```bash 67 | # Clone the repository 68 | git clone https://github.com/cfdude/super-shell-mcp.git 69 | cd super-shell-mcp 70 | 71 | # Install dependencies 72 | npm install 73 | 74 | # Build the project 75 | npm run build 76 | ``` 77 | 78 | ## Usage 79 | 80 | ### For Claude Desktop Extension Users (.dxt) 81 | 82 | If you installed using the `.dxt` extension (Option 1), **you're ready to go!** No additional configuration needed. The extension handles everything automatically: 83 | 84 | - ✅ **Automatic startup** when Claude Desktop launches 85 | - ✅ **Platform detection** and appropriate shell selection 86 | - ✅ **Built-in security** with command whitelisting and approval workflows 87 | - ✅ **Optional configuration** via Claude Desktop's extension settings 88 | 89 | ### For Manual Installation Users 90 | 91 | If you installed manually (Option 2 or 3), you'll need to configure Claude Desktop or your MCP client: 92 | 93 | #### Starting the Server Manually 94 | 95 | ```bash 96 | npm start 97 | ``` 98 | 99 | Or directly: 100 | 101 | ```bash 102 | node build/index.js 103 | ``` 104 | 105 | #### Manual Configuration for MCP Clients 106 | 107 | For manual installations, both Roo Code and Claude Desktop use a similar configuration format for MCP servers: 108 | 109 | ##### Using NPX (Recommended for Manual Setup) 110 | 111 | The easiest way to use Super Shell MCP is with NPX, which automatically installs and runs the package from npm without requiring manual setup. The package is available on NPM at [https://www.npmjs.com/package/super-shell-mcp](https://www.npmjs.com/package/super-shell-mcp). 112 | 113 | ##### Roo Code Configuration with NPX 114 | 115 | ```json 116 | "super-shell": { 117 | "command": "npx", 118 | "args": [ 119 | "-y", 120 | "super-shell-mcp" 121 | ], 122 | "alwaysAllow": [], 123 | "disabled": false 124 | } 125 | ``` 126 | 127 | ##### Claude Desktop Configuration with NPX 128 | 129 | ```json 130 | "super-shell": { 131 | "command": "npx", 132 | "args": [ 133 | "-y", 134 | "super-shell-mcp" 135 | ], 136 | "alwaysAllow": false, 137 | "disabled": false 138 | } 139 | ``` 140 | 141 | #### Option 2: Using Local Installation 142 | 143 | If you prefer to use a local installation, add the following to your Roo Code MCP settings configuration file (located at `~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json`): 144 | 145 | ```json 146 | "super-shell": { 147 | "command": "node", 148 | "args": [ 149 | "/path/to/super-shell-mcp/build/index.js" 150 | ], 151 | "alwaysAllow": [], 152 | "disabled": false 153 | } 154 | ``` 155 | 156 | You can optionally specify a custom shell by adding a shell parameter: 157 | 158 | ```json 159 | "super-shell": { 160 | "command": "node", 161 | "args": [ 162 | "/path/to/super-shell-mcp/build/index.js", 163 | "--shell=/usr/bin/bash" 164 | ], 165 | "alwaysAllow": [], 166 | "disabled": false 167 | } 168 | ``` 169 | Windows 11 example 170 | ```json 171 | "super-shell": { 172 | "command": "C:\\Program Files\\nodejs\\node.exe", 173 | "args": [ 174 | "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js", 175 | "-y", 176 | "super-shell-mcp", 177 | "C:\\Users\\username" 178 | ], 179 | "alwaysAllow": [], 180 | "disabled": false 181 | } 182 | ``` 183 | 184 | #### Claude Desktop Configuration 185 | 186 | Add the following to your Claude Desktop configuration file (located at `~/Library/Application Support/Claude/claude_desktop_config.json`): 187 | 188 | ```json 189 | "super-shell": { 190 | "command": "node", 191 | "args": [ 192 | "/path/to/super-shell-mcp/build/index.js" 193 | ], 194 | "alwaysAllow": false, 195 | "disabled": false 196 | } 197 | ``` 198 | For Windows users, the configuration file is typically located at `%APPDATA%\Claude\claude_desktop_config.json`. 199 | 200 | ### Platform-Specific Configuration 201 | 202 | #### Windows 203 | - Default shell: cmd.exe (or PowerShell if available) 204 | - Configuration paths: 205 | - Roo Code: `%APPDATA%\Code\User\globalStorage\rooveterinaryinc.roo-cline\settings\cline_mcp_settings.json` 206 | - Claude Desktop: `%APPDATA%\Claude\claude_desktop_config.json` 207 | - Shell path examples: 208 | - cmd.exe: `C:\\Windows\\System32\\cmd.exe` 209 | - PowerShell: `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` 210 | - PowerShell Core: `C:\\Program Files\\PowerShell\\7\\pwsh.exe` 211 | 212 | #### macOS 213 | - Default shell: /bin/zsh 214 | - Configuration paths: 215 | - Roo Code: `~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` 216 | - Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` 217 | - Shell path examples: 218 | - zsh: `/bin/zsh` 219 | - bash: `/bin/bash` 220 | - sh: `/bin/sh` 221 | 222 | #### Linux 223 | - Default shell: /bin/bash (or $SHELL environment variable) 224 | - Configuration paths: 225 | - Roo Code: `~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` 226 | - Claude Desktop: `~/.config/Claude/claude_desktop_config.json` 227 | - Shell path examples: 228 | - bash: `/bin/bash` 229 | - sh: `/bin/sh` 230 | - zsh: `/usr/bin/zsh` 231 | 232 | 233 | You can optionally specify a custom shell: 234 | 235 | ```json 236 | "super-shell": { 237 | "command": "node", 238 | "args": [ 239 | "/path/to/super-shell-mcp/build/index.js", 240 | "--shell=C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" 241 | ], 242 | "alwaysAllow": false, 243 | "disabled": false 244 | } 245 | ``` 246 | 247 | Replace `/path/to/super-shell-mcp` with the actual path where you cloned the repository. 248 | 249 | > **Note**: 250 | > - For Roo Code: Setting `alwaysAllow` to an empty array `[]` is recommended for security reasons, as it will prompt for approval before executing any commands. If you want to allow specific commands without prompting, you can add their names to the array, for example: `"alwaysAllow": ["execute_command", "get_whitelist"]`. 251 | > - For Claude Desktop: Setting `alwaysAllow` to `false` is recommended for security reasons. Claude Desktop uses a boolean value instead of an array, where `false` means all commands require approval and `true` means all commands are allowed without prompting. 252 | > 253 | > **Important**: The `alwaysAllow` parameter is processed by the MCP client (Roo Code or Claude Desktop), not by the Super Shell MCP server itself. The server will work correctly with either format, as the client handles the approval process before sending requests to the server. 254 | 255 | ### Available Tools 256 | The server exposes the following MCP tools: 257 | 258 | #### `get_platform_info` 259 | 260 | Get information about the current platform and shell. 261 | 262 | ```json 263 | {} 264 | ``` 265 | 266 | 267 | #### `execute_command` 268 | 269 | Execute a shell command on the current platform. 270 | 271 | ```json 272 | { 273 | "command": "ls", 274 | "args": ["-la"] 275 | } 276 | ``` 277 | 278 | #### `get_whitelist` 279 | 280 | Get the list of whitelisted commands. 281 | 282 | ```json 283 | {} 284 | ``` 285 | 286 | #### `add_to_whitelist` 287 | 288 | Add a command to the whitelist. 289 | 290 | ```json 291 | { 292 | "command": "python3", 293 | "securityLevel": "safe", 294 | "description": "Run Python 3 scripts" 295 | } 296 | ``` 297 | 298 | #### `update_security_level` 299 | 300 | Update the security level of a whitelisted command. 301 | 302 | ```json 303 | { 304 | "command": "python3", 305 | "securityLevel": "requires_approval" 306 | } 307 | ``` 308 | 309 | #### `remove_from_whitelist` 310 | 311 | Remove a command from the whitelist. 312 | 313 | ```json 314 | { 315 | "command": "python3" 316 | } 317 | ``` 318 | 319 | #### `get_pending_commands` 320 | 321 | Get the list of commands pending approval. 322 | 323 | ```json 324 | {} 325 | ``` 326 | 327 | #### `approve_command` 328 | 329 | Approve a pending command. 330 | 331 | ```json 332 | { 333 | "commandId": "command-uuid-here" 334 | } 335 | ``` 336 | 337 | #### `deny_command` 338 | 339 | Deny a pending command. 340 | 341 | ```json 342 | { 343 | "commandId": "command-uuid-here", 344 | "reason": "This command is potentially dangerous" 345 | } 346 | ``` 347 | 348 | ## Default Whitelisted Commands 349 | 350 | The server includes platform-specific command whitelists that are automatically selected based on the detected platform. 351 | 352 | ### Common Safe Commands (All Platforms) 353 | 354 | - `echo` - Print text to standard output 355 | 356 | ### Unix-like Safe Commands (macOS/Linux) 357 | 358 | - `ls` - List directory contents 359 | - `pwd` - Print working directory 360 | - `echo` - Print text to standard output 361 | - `cat` - Concatenate and print files 362 | - `grep` - Search for patterns in files 363 | - `find` - Find files in a directory hierarchy 364 | - `cd` - Change directory 365 | - `head` - Output the first part of files 366 | - `tail` - Output the last part of files 367 | - `wc` - Print newline, word, and byte counts 368 | 369 | ### Windows-specific Safe Commands 370 | 371 | - `dir` - List directory contents 372 | - `type` - Display the contents of a text file 373 | - `findstr` - Search for strings in files 374 | - `where` - Locate programs 375 | - `whoami` - Display current user 376 | - `hostname` - Display computer name 377 | - `ver` - Display operating system version 378 | ### Commands Requiring Approval 379 | 380 | #### Windows Commands Requiring Approval 381 | 382 | - `copy` - Copy files 383 | - `move` - Move files 384 | - `mkdir` - Create directories 385 | - `rmdir` - Remove directories 386 | - `rename` - Rename files 387 | - `attrib` - Change file attributes 388 | 389 | #### Unix Commands Requiring Approval 390 | 391 | 392 | - `mv` - Move (rename) files 393 | - `cp` - Copy files and directories 394 | - `mkdir` - Create directories 395 | - `touch` - Change file timestamps or create empty files 396 | - `chmod` - Change file mode bits 397 | - `chown` - Change file owner and group 398 | 399 | ### Forbidden Commands 400 | 401 | #### Windows Forbidden Commands 402 | 403 | - `del` - Delete files 404 | - `erase` - Delete files 405 | - `format` - Format a disk 406 | - `runas` - Execute a program as another user 407 | 408 | #### Unix Forbidden Commands 409 | 410 | - `rm` - Remove files or directories 411 | - `sudo` - Execute a command as another user 412 | 413 | ## Security Considerations 414 | 415 | - All commands are executed with the permissions of the user running the MCP server 416 | - Commands requiring approval are held in a queue until explicitly approved 417 | - Forbidden commands are never executed 418 | - The server uses Node.js's `execFile` instead of `exec` to prevent shell injection 419 | - Arguments are validated against allowed patterns when specified 420 | 421 | ## Extending the Whitelist 422 | 423 | You can extend the whitelist by using the `add_to_whitelist` tool. For example: 424 | 425 | ```json 426 | { 427 | "command": "npm", 428 | "securityLevel": "requires_approval", 429 | "description": "Node.js package manager" 430 | } 431 | ``` 432 | 433 | ## NPM Package Information 434 | 435 | Super Shell MCP is available as an npm package at [https://www.npmjs.com/package/super-shell-mcp](https://www.npmjs.com/package/super-shell-mcp). 436 | 437 | ### Benefits of Using NPX 438 | 439 | Using the NPX method (as shown in Option 1 of the Configuration section) offers several advantages: 440 | 441 | 1. **No Manual Setup**: No need to clone the repository, install dependencies, or build the project 442 | 2. **Automatic Updates**: Always uses the latest published version 443 | 3. **Cross-Platform Compatibility**: Works the same way on Windows, macOS, and Linux 444 | 4. **Simplified Configuration**: Shorter configuration with no absolute paths 445 | 5. **Reduced Maintenance**: No local files to manage or update 446 | 447 | ### Using from GitHub 448 | 449 | If you prefer to use the latest development version directly from GitHub: 450 | 451 | ```json 452 | "super-shell": { 453 | "command": "npx", 454 | "args": [ 455 | "-y", 456 | "github:cfdude/super-shell-mcp" 457 | ], 458 | "alwaysAllow": [], // For Roo Code 459 | "disabled": false 460 | } 461 | ``` 462 | 463 | ### Publishing Your Own Version 464 | 465 | If you want to publish your own modified version to npm: 466 | 467 | 1. Update the package.json with your details 468 | 2. Ensure the "bin" field is properly configured: 469 | ```json 470 | "bin": { 471 | "super-shell-mcp": "./build/index.js" 472 | } 473 | ``` 474 | 3. Publish to npm: 475 | ```bash 476 | npm publish 477 | ``` 478 | 479 | ## NPX Best Practices 480 | 481 | For optimal integration with MCP clients using NPX, this project follows these best practices: 482 | 483 | 1. **Executable Entry Point**: The main file includes a shebang line (`#!/usr/bin/env node`) and is made executable during build. 484 | 485 | 2. **Package Configuration**: 486 | - `"type": "module"` - Ensures ES Modules are used 487 | - `"bin"` field - Maps the command name to the entry point 488 | - `"files"` field - Specifies which files to include when publishing 489 | - `"prepare"` script - Ensures compilation happens on install 490 | 491 | 3. **TypeScript Configuration**: 492 | - `"module": "NodeNext"` - Proper ES Modules support 493 | - `"moduleResolution": "NodeNext"` - Consistent with ES Modules 494 | 495 | 4. **Automatic Installation and Execution**: 496 | - The MCP client configuration uses `npx -y` to automatically install and run the package 497 | - No terminal window is tied up as the process runs in the background 498 | 499 | 5. **Publishing Process**: 500 | ```bash 501 | # Update version in package.json 502 | npm version patch # or minor/major as appropriate 503 | 504 | # Build and publish 505 | npm publish 506 | ``` 507 | 508 | These practices ensure the MCP server can be started automatically by the MCP client without requiring a separate terminal window, improving user experience and operational efficiency. 509 | 510 | ## Troubleshooting 511 | 512 | ### Cross-Platform Issues 513 | 514 | #### Windows-Specific Issues 515 | 516 | 1. **PowerShell Script Execution Policy** 517 | - **Issue**: PowerShell may block script execution with the error "Execution of scripts is disabled on this system" 518 | - **Solution**: Run PowerShell as Administrator and execute `Set-ExecutionPolicy RemoteSigned` or use the `-ExecutionPolicy Bypass` parameter when configuring the shell 519 | 520 | 2. **Path Separators** 521 | - **Issue**: Windows uses backslashes (`\`) in paths, which need to be escaped in JSON 522 | - **Solution**: Use double backslashes (`\\`) in JSON configuration files, e.g., `C:\\Windows\\System32\\cmd.exe` 523 | 524 | 3. **Command Not Found** 525 | - **Issue**: Windows doesn't have Unix commands like `ls`, `grep`, etc. 526 | - **Solution**: Use Windows equivalents (`dir` instead of `ls`, `findstr` instead of `grep`) 527 | 528 | #### macOS/Linux-Specific Issues 529 | 530 | 1. **Shell Permissions** 531 | - **Issue**: Permission denied when executing commands 532 | - **Solution**: Ensure the shell has appropriate permissions with `chmod +x /path/to/shell` 533 | 534 | 2. **Environment Variables** 535 | - **Issue**: Environment variables not available in MCP server 536 | - **Solution**: Set environment variables in the shell's profile file (`.zshrc`, `.bashrc`, etc.) 537 | 538 | ### General Troubleshooting 539 | 540 | 1. **Shell Detection Issues** 541 | - **Issue**: Server fails to detect the correct shell 542 | - **Solution**: Explicitly specify the shell path in the configuration 543 | 544 | 2. **Command Execution Timeout** 545 | - **Issue**: Commands taking too long and timing out 546 | - **Solution**: Increase the timeout value in the command service constructor 547 | 548 | ### Logging System 549 | 550 | The server includes a comprehensive logging system that writes logs to a file for easier debugging and monitoring: 551 | 552 | 1. **Log File Location** 553 | - Default: `logs/super-shell-mcp.log` in the server's directory 554 | - The logs directory is created automatically and tracked by Git (with a .gitkeep file) 555 | - Log files themselves are excluded from Git via .gitignore 556 | - Contains detailed information about server operations, command execution, and approval workflow 557 | 558 | 2. **Log Levels** 559 | - **INFO**: General operational information 560 | - **DEBUG**: Detailed debugging information 561 | - **ERROR**: Error conditions and exceptions 562 | 563 | 3. **Viewing Logs** 564 | - Use standard file viewing commands to check logs: 565 | ```bash 566 | # View the entire log 567 | cat logs/super-shell-mcp.log 568 | 569 | # Follow log updates in real-time 570 | tail -f logs/super-shell-mcp.log 571 | ``` 572 | 573 | 4. **Log Content** 574 | - Server startup and configuration 575 | - Command execution requests and results 576 | - Approval workflow events (pending, approved, denied) 577 | - Error conditions and troubleshooting information 578 | 579 | 3. **Whitelist Management** 580 | - **Issue**: Need to add custom commands to whitelist 581 | - **Solution**: Use the `add_to_whitelist` tool to add commands specific to your environment 582 | 583 | ## License 584 | 585 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. 586 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | 2 | # Contributing to This Project 3 | 4 | Thank you for your interest in contributing! We welcome pull requests and contributions from the community. Please take a moment to read the guidelines below to ensure a smooth collaboration process. 5 | 6 | ## 📦 Getting Started 7 | 8 | 1. **Fork the Repository** 9 | Click the **Fork** button at the top of the repository page to create your own copy of the project. 10 | 11 | 2. **Clone Your Fork** 12 | ```bash 13 | git clone https://github.com/your-username/your-fork.git 14 | cd your-fork 15 | ``` 16 | 17 | 18 | 3. Create a Feature Branch 19 | Create a new branch based on main for your changes: 20 | ```bash 21 | git checkout -b feature/your-feature-name 22 | ``` 23 | 24 | 25 | 🚀 Submitting a Pull Request 26 | 27 | Once you’re ready to share your changes: 28 | 1. Ensure Code Quality 29 | • Run all linting checks. 30 | • Use Prettier to format your code. 31 | • Confirm all tests pass (if applicable). 32 | 2. Document Your Work 33 | • Add meaningful comments to your code. 34 | • Update or add documentation where necessary (e.g., README, inline comments, or docs folder). 35 | 3. Submit Your PR 36 | • Push your branch to your fork: 37 | ```bash 38 | git push origin feature/your-feature-name 39 | ``` 40 | 41 | * Open a Pull Request against the main branch of the original repository. 42 | * Include a clear summary of your changes and why they are beneficial. 43 | 44 | ✅ Code Standards 45 | * Follow the existing code style and formatting conventions. 46 | * All code must pass linting and Prettier formatting before submission. 47 | * Keep your changes focused and limited to the scope of your feature or fix. 48 | 49 | ❤️ Be Kind 50 | * Be respectful and constructive in code reviews and discussions. 51 | * Ask questions if you’re unsure—collaboration is key. 52 | 53 | We appreciate your help in improving the project for everyone. Thank you for contributing! 54 | ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | ``` -------------------------------------------------------------------------------- /mcp-settings-example.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "mac-shell": { 4 | "command": "node", 5 | "args": ["/path/to/mac-shell-mcp/build/index.js"], 6 | "disabled": false, 7 | "alwaysAllow": [] 8 | } 9 | } 10 | } ``` -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Build the project if needed 4 | if [ ! -d "./build" ] || [ ! -f "./build/index.js" ]; then 5 | echo "Building project..." 6 | npm run build 7 | fi 8 | 9 | # Run the MCP server 10 | echo "Starting Mac Shell MCP Server..." 11 | node build/index.js ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | - run: npm ci 16 | - run: npm test 17 | ``` -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(gh pr create:*)", 5 | "Bash(gh browse:*)", 6 | "Bash(docker build:*)", 7 | "Bash(docker run:*)", 8 | "mcp__super-shell__execute_command", 9 | "mcp__git__git_status", 10 | "mcp__git__git_add", 11 | "mcp__git__git_commit" 12 | ], 13 | "deny": [] 14 | } 15 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "types": ["node"], 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "outDir": "./build", 10 | "rootDir": "./src", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "build"] 18 | } ``` -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- ``` 1 | module.exports = { 2 | transform: {}, 3 | testEnvironment: 'node', 4 | testMatch: ['**/tests/**/*.test.js'], 5 | verbose: true, 6 | testRunner: 'jest-circus/runner', 7 | transformIgnorePatterns: [ 8 | 'node_modules/(?!(.*\\.mjs$))' 9 | ], 10 | setupFiles: ['./jest.setup.cjs'], 11 | moduleNameMapper: { 12 | '^../build/services/command-service.js$': '<rootDir>/jest.setup.cjs', 13 | '^../build/utils/platform-utils.js$': '<rootDir>/jest.setup.cjs' 14 | } 15 | }; ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | properties: {} 9 | default: {} 10 | commandFunction: 11 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 12 | |- 13 | (config) => ({ command: 'npm', args: ['start'] }) 14 | exampleConfig: {} 15 | ``` -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint-and-build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '18' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Run Linter 25 | run: npm run lint 26 | 27 | - name: Build Verification 28 | run: npm run build ``` -------------------------------------------------------------------------------- /scripts/make-executable.js: -------------------------------------------------------------------------------- ```javascript 1 | import fs from 'fs'; 2 | import { platform } from 'os'; 3 | 4 | const isWindows = platform() === 'win32'; 5 | const indexPath = './build/index.js'; 6 | 7 | if (isWindows) { 8 | console.log('Windows detected, skipping chmod operation'); 9 | } else { 10 | try { 11 | // On Unix-like systems, make the file executable 12 | fs.chmodSync(indexPath, '755'); 13 | console.log(`Made ${indexPath} executable`); 14 | } catch (error) { 15 | console.error(`Error making ${indexPath} executable:`, error); 16 | process.exit(1); 17 | } 18 | } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- ```yaml 1 | # These are supported funding model platforms 2 | 3 | github: [cfdude] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: cfdude 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ['paypal.me/cfdude', 'https://account.venmo.com/u/cfdude', 'https://cash.app/$cfdude'] 16 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Use Node.js 18 Alpine for smaller image size 2 | FROM node:18-alpine 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Create logs directory 8 | RUN mkdir -p logs 9 | 10 | # Copy package files first for better caching 11 | COPY package*.json ./ 12 | 13 | # Copy TypeScript configuration and source code 14 | COPY tsconfig.json ./ 15 | COPY src/ ./src/ 16 | 17 | # Install all dependencies (including dev dependencies for build) 18 | RUN npm ci 19 | 20 | # Build the project (compiles TypeScript and sets executable permissions) 21 | RUN npm run build 22 | 23 | # Remove dev dependencies to reduce image size 24 | RUN npm prune --omit=dev && npm cache clean --force 25 | 26 | # Ensure the built file is executable 27 | RUN chmod +x build/index.js 28 | 29 | # Expose stdio for MCP communication 30 | # Note: MCP servers typically communicate via stdio, not network ports 31 | 32 | # Set the entrypoint to the built application 33 | ENTRYPOINT ["node", "build/index.js"] 34 | ``` -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: "CodeQL Analysis" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 0 * * 0' # Run once a week at midnight on Sunday 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'javascript' ] # Add other languages as needed: python, java, go, cpp, etc. 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | # Autobuild attempts to build any compiled languages 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{matrix.language}}" 42 | ``` -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- ```yaml 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Publish Package to npmjs 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | - run: npm ci 22 | - run: npm test 23 | 24 | publish-npm: 25 | needs: build 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: read 29 | id-token: write 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | registry-url: https://registry.npmjs.org/ 36 | - run: npm ci 37 | - run: npm publish --provenance 38 | env: 39 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 40 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "super-shell-mcp", 3 | "version": "2.0.13", 4 | "description": "MCP server for executing shell commands across multiple platforms", 5 | "type": "module", 6 | "main": "build/index.js", 7 | "bin": { 8 | "super-shell-mcp": "./build/index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/cfdude/super-shell-mcp.git" 13 | }, 14 | "files": [ 15 | "build" 16 | ], 17 | "scripts": { 18 | "build": "tsc && chmod +x build/index.js", 19 | "prepare": "npm run build", 20 | "start": "node build/index.js", 21 | "dev": "ts-node --esm src/index.ts", 22 | "lint": "eslint .", 23 | "test": "jest --config=jest.config.cjs", 24 | "test:quiet": "jest --config=jest.config.cjs --silent" 25 | }, 26 | "keywords": [ 27 | "mcp", 28 | "shell", 29 | "macos", 30 | "windows", 31 | "linux", 32 | "zsh", 33 | "bash", 34 | "powershell", 35 | "cmd", 36 | "terminal", 37 | "cross-platform" 38 | ], 39 | "author": "", 40 | "license": "MIT", 41 | "dependencies": { 42 | "@modelcontextprotocol/sdk": "^1.6.1", 43 | "zod": "^3.22.4" 44 | }, 45 | "devDependencies": { 46 | "@eslint/js": "^9.30.1", 47 | "@types/jest": "^29.5.11", 48 | "@types/node": "^20.17.24", 49 | "@typescript-eslint/eslint-plugin": "^8.0.0", 50 | "@typescript-eslint/parser": "^8.0.0", 51 | "eslint": "^9.30.1", 52 | "jest": "^29.7.0", 53 | "jest-circus": "^29.7.0", 54 | "ts-node": "^10.9.2", 55 | "typescript": "^5.3.3" 56 | } 57 | } ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "dxt_version": "0.1", 3 | "name": "super-shell-mcp", 4 | "version": "2.0.13", 5 | "description": "Execute shell commands across multiple platforms with built-in security controls", 6 | "long_description": "An MCP server for executing shell commands on Windows, macOS, and Linux with automatic platform detection, command whitelisting, and approval workflows for security.", 7 | "author": { 8 | "name": "cfdude", 9 | "url": "https://github.com/cfdude/super-shell-mcp" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/cfdude/super-shell-mcp" 14 | }, 15 | "license": "MIT", 16 | "keywords": ["shell", "commands", "security", "cross-platform", "terminal"], 17 | "server": { 18 | "type": "node", 19 | "entry_point": "server/index.js", 20 | "mcp_config": { 21 | "command": "node", 22 | "args": ["${__dirname}/server/index.js"], 23 | "env": { 24 | "CUSTOM_SHELL": "${user_config.shell_path}" 25 | } 26 | } 27 | }, 28 | "tools": [ 29 | { 30 | "name": "execute_command", 31 | "description": "Execute shell commands with security controls" 32 | }, 33 | { 34 | "name": "get_whitelist", 35 | "description": "Get list of whitelisted commands" 36 | }, 37 | { 38 | "name": "add_to_whitelist", 39 | "description": "Add commands to the security whitelist" 40 | }, 41 | { 42 | "name": "get_platform_info", 43 | "description": "Get current platform and shell information" 44 | } 45 | ], 46 | "user_config": { 47 | "shell_path": { 48 | "type": "string", 49 | "title": "Custom Shell Path", 50 | "description": "Optional: Specify a custom shell path (e.g., /bin/zsh, C:\\Windows\\System32\\cmd.exe)", 51 | "required": false 52 | } 53 | } 54 | } ``` -------------------------------------------------------------------------------- /docs/adr/001-command-security-levels.md: -------------------------------------------------------------------------------- ```markdown 1 | # ADR 001: Command Security Levels 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | When executing shell commands through an MCP server, there's a significant security risk if all commands are allowed without restrictions. Different commands have varying levels of potential impact on the system: 10 | 11 | 1. Some commands are relatively safe (e.g., `ls`, `pwd`, `echo`) 12 | 2. Some commands can modify the system but in limited ways (e.g., `mkdir`, `cp`, `mv`) 13 | 3. Some commands can cause significant damage (e.g., `rm -rf`, `sudo`) 14 | 15 | We need a mechanism to categorize commands based on their potential risk and handle them accordingly. 16 | 17 | ## Decision 18 | 19 | We will implement a three-tier security level system for commands: 20 | 21 | 1. **Safe Commands**: These commands can be executed immediately without approval. They are read-only or have minimal impact on the system. 22 | 23 | 2. **Commands Requiring Approval**: These commands can modify the system but are not inherently dangerous. They will be queued for explicit approval before execution. 24 | 25 | 3. **Forbidden Commands**: These commands are considered too dangerous and will be rejected outright. 26 | 27 | Each command will be categorized in a whitelist, and the security level will determine how the command is handled when execution is requested. 28 | 29 | ## Consequences 30 | 31 | ### Positive 32 | 33 | - Provides a clear security model for command execution 34 | - Allows safe commands to be executed without friction 35 | - Creates an approval workflow for potentially dangerous commands 36 | - Completely blocks high-risk commands 37 | - Makes the security policy explicit and configurable 38 | 39 | ### Negative 40 | 41 | - Requires maintaining a whitelist of commands 42 | - May introduce friction for legitimate use cases of commands requiring approval 43 | - Initial categorization may not be perfect and could require adjustment 44 | 45 | ## Implementation 46 | 47 | The security levels will be implemented as an enum in the `CommandService` class: 48 | 49 | ```typescript 50 | export enum CommandSecurityLevel { 51 | SAFE = 'safe', 52 | REQUIRES_APPROVAL = 'requires_approval', 53 | FORBIDDEN = 'forbidden' 54 | } 55 | ``` 56 | 57 | Commands will be stored in a whitelist with their security level: 58 | 59 | ```typescript 60 | export interface CommandWhitelistEntry { 61 | command: string; 62 | securityLevel: CommandSecurityLevel; 63 | allowedArgs?: Array<string | RegExp>; 64 | description?: string; 65 | } 66 | ``` 67 | 68 | When a command is executed, its security level will determine the behavior: 69 | - Safe commands are executed immediately 70 | - Commands requiring approval are queued for explicit approval 71 | - Forbidden commands are rejected with an error ``` -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | /** 5 | * Simple logging utility that writes to a file 6 | */ 7 | export class Logger { 8 | private logFile: string; 9 | private enabled: boolean; 10 | private fileStream: fs.WriteStream | null = null; 11 | 12 | /** 13 | * Create a new logger 14 | * @param logFile Path to the log file 15 | * @param enabled Whether logging is enabled 16 | */ 17 | constructor(logFile: string, enabled = true) { 18 | this.logFile = logFile; 19 | this.enabled = enabled; 20 | 21 | if (this.enabled) { 22 | // Create the directory if it doesn't exist 23 | const logDir = path.dirname(this.logFile); 24 | console.error(`Creating log directory: ${logDir}`); 25 | try { 26 | if (!fs.existsSync(logDir)) { 27 | fs.mkdirSync(logDir, { recursive: true }); 28 | } 29 | } catch (error) { 30 | console.error(`Error creating log directory: ${error}`); 31 | // Fall back to a directory we know exists 32 | this.logFile = './super-shell-mcp.log'; 33 | console.error(`Falling back to log file: ${this.logFile}`); 34 | } 35 | 36 | // Create or truncate the log file 37 | this.fileStream = fs.createWriteStream(this.logFile, { flags: 'w' }); 38 | 39 | // Write a header to the log file 40 | this.log('INFO', `Logging started at ${new Date().toISOString()}`); 41 | } 42 | } 43 | 44 | /** 45 | * Log a message 46 | * @param level Log level (INFO, DEBUG, ERROR, etc.) 47 | * @param message Message to log 48 | */ 49 | public log(level: string, message: string): void { 50 | if (!this.enabled || !this.fileStream) { 51 | return; 52 | } 53 | 54 | const timestamp = new Date().toISOString(); 55 | const logMessage = `[${timestamp}] [${level}] ${message}\n`; 56 | 57 | this.fileStream.write(logMessage); 58 | } 59 | 60 | /** 61 | * Log an info message 62 | * @param message Message to log 63 | */ 64 | public info(message: string): void { 65 | this.log('INFO', message); 66 | } 67 | 68 | /** 69 | * Log a debug message 70 | * @param message Message to log 71 | */ 72 | public debug(message: string): void { 73 | this.log('DEBUG', message); 74 | } 75 | 76 | /** 77 | * Log an error message 78 | * @param message Message to log 79 | */ 80 | public error(message: string): void { 81 | this.log('ERROR', message); 82 | } 83 | 84 | /** 85 | * Close the logger 86 | */ 87 | public close(): void { 88 | if (this.fileStream) { 89 | this.fileStream.end(); 90 | this.fileStream = null; 91 | } 92 | } 93 | } 94 | 95 | // Create a singleton logger instance 96 | let loggerInstance: Logger | null = null; 97 | 98 | /** 99 | * Get the logger instance 100 | * @param logFile Path to the log file 101 | * @param enabled Whether logging is enabled 102 | * @returns Logger instance 103 | */ 104 | export function getLogger(logFile?: string, enabled?: boolean): Logger { 105 | if (!loggerInstance && logFile) { 106 | loggerInstance = new Logger(logFile, enabled); 107 | } 108 | 109 | if (!loggerInstance) { 110 | throw new Error('Logger not initialized'); 111 | } 112 | 113 | return loggerInstance; 114 | } ``` -------------------------------------------------------------------------------- /docs/adr/002-mcp-for-shell-commands.md: -------------------------------------------------------------------------------- ```markdown 1 | # ADR 002: Using MCP for Shell Command Execution 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | There are several ways to provide shell command execution capabilities to AI assistants: 10 | 11 | 1. Custom API endpoints 12 | 2. Direct integration with specific AI platforms 13 | 3. Standardized protocols like MCP (Model Context Protocol) 14 | 15 | Each approach has different tradeoffs in terms of flexibility, security, and integration complexity. We need to decide on the most appropriate approach for our shell command execution service. 16 | 17 | ## Decision 18 | 19 | We will implement shell command execution as an MCP server for the following reasons: 20 | 21 | 1. **Standardization**: MCP is an emerging standard for AI tool integration, supported by major AI platforms like Anthropic's Claude. 22 | 23 | 2. **Discoverability**: MCP provides built-in tool discovery, allowing AI assistants to automatically learn about available commands and their parameters. 24 | 25 | 3. **Security**: MCP's structured approach allows for clear security boundaries and validation of inputs. 26 | 27 | 4. **Flexibility**: MCP servers can be used with any MCP-compatible client, not just specific AI platforms. 28 | 29 | 5. **Future-proofing**: As more AI platforms adopt MCP, our implementation will be compatible without changes. 30 | 31 | ## Consequences 32 | 33 | ### Positive 34 | 35 | - Works with any MCP-compatible client (Claude Desktop, etc.) 36 | - Provides structured tool definitions with clear parameter schemas 37 | - Enables dynamic tool discovery 38 | - Follows an emerging industry standard 39 | - Separates concerns between command execution and AI integration 40 | 41 | ### Negative 42 | 43 | - MCP is still an evolving standard 44 | - Requires understanding of MCP concepts and implementation details 45 | - May have more overhead than a direct, custom integration 46 | 47 | ## Implementation 48 | 49 | We will implement the MCP server using the official TypeScript SDK: 50 | 51 | ```typescript 52 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 53 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 54 | ``` 55 | 56 | The server will expose tools for command execution and whitelist management: 57 | 58 | ```typescript 59 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 60 | tools: [ 61 | { 62 | name: 'execute_command', 63 | description: 'Execute a shell command on macOS', 64 | inputSchema: { 65 | type: 'object', 66 | properties: { 67 | command: { 68 | type: 'string', 69 | description: 'The command to execute', 70 | }, 71 | args: { 72 | type: 'array', 73 | items: { 74 | type: 'string', 75 | }, 76 | description: 'Command arguments', 77 | }, 78 | }, 79 | required: ['command'], 80 | }, 81 | }, 82 | // Additional tools... 83 | ], 84 | })); 85 | ``` 86 | 87 | The server will use stdio transport for compatibility with Claude Desktop and other MCP clients: 88 | 89 | ```typescript 90 | const transport = new StdioServerTransport(); 91 | await this.server.connect(transport); ``` -------------------------------------------------------------------------------- /docs/adr/003-command-approval-workflow.md: -------------------------------------------------------------------------------- ```markdown 1 | # ADR 003: Command Approval Workflow 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | When executing shell commands, there's a middle ground between completely safe commands and forbidden commands. Some commands can modify the system in potentially harmful ways but are still necessary for legitimate use cases. We need a mechanism to handle these commands safely. 10 | 11 | Options considered: 12 | 1. Reject all potentially dangerous commands 13 | 2. Allow all commands with appropriate warnings 14 | 3. Implement an approval workflow for commands that require additional verification 15 | 16 | ## Decision 17 | 18 | We will implement an approval workflow for commands that are potentially dangerous but still necessary. This workflow will: 19 | 20 | 1. Queue commands marked as requiring approval 21 | 2. Provide tools to list pending commands 22 | 3. Allow explicit approval or denial of pending commands 23 | 4. Execute approved commands and reject denied commands 24 | 25 | This approach balances security with usability by allowing potentially dangerous commands to be executed after explicit approval. 26 | 27 | ## Consequences 28 | 29 | ### Positive 30 | 31 | - Provides a middle ground between allowing and forbidding commands 32 | - Creates an audit trail of command approvals 33 | - Allows for human judgment in borderline cases 34 | - Enables safe use of necessary system-modifying commands 35 | - Prevents accidental execution of dangerous commands 36 | 37 | ### Negative 38 | 39 | - Introduces asynchronous workflow for command execution 40 | - Requires additional user interaction for approval 41 | - May create confusion if approvals are delayed or forgotten 42 | 43 | ## Implementation 44 | 45 | The approval workflow will be implemented using a queue of pending commands: 46 | 47 | ```typescript 48 | interface PendingCommand { 49 | id: string; 50 | command: string; 51 | args: string[]; 52 | requestedAt: Date; 53 | requestedBy?: string; 54 | resolve: (value: CommandResult) => void; 55 | reject: (reason: Error) => void; 56 | } 57 | ``` 58 | 59 | When a command requiring approval is executed, it will be added to the queue: 60 | 61 | ```typescript 62 | private queueCommandForApproval( 63 | command: string, 64 | args: string[] = [], 65 | requestedBy?: string 66 | ): Promise<CommandResult> { 67 | return new Promise((resolve, reject) => { 68 | const id = randomUUID(); 69 | const pendingCommand: PendingCommand = { 70 | id, 71 | command, 72 | args, 73 | requestedAt: new Date(), 74 | requestedBy, 75 | resolve, 76 | reject 77 | }; 78 | 79 | this.pendingCommands.set(id, pendingCommand); 80 | this.emit('command:pending', pendingCommand); 81 | }); 82 | } 83 | ``` 84 | 85 | The MCP server will expose tools to list, approve, and deny pending commands: 86 | 87 | ```typescript 88 | // Get pending commands 89 | const pendingCommands = await client.callTool('get_pending_commands', {}); 90 | 91 | // Approve a command 92 | await client.callTool('approve_command', { commandId }); 93 | 94 | // Deny a command 95 | await client.callTool('deny_command', { commandId, reason: 'Not allowed' }); 96 | ``` 97 | 98 | This workflow ensures that potentially dangerous commands are only executed after explicit approval, providing an additional layer of security. ``` -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- ```javascript 1 | import js from '@eslint/js'; 2 | import tsEslint from '@typescript-eslint/eslint-plugin'; 3 | import tsParser from '@typescript-eslint/parser'; 4 | 5 | export default [ 6 | js.configs.recommended, 7 | { 8 | files: ['**/*.js'], 9 | languageOptions: { 10 | ecmaVersion: 2022, 11 | sourceType: 'module', 12 | globals: { 13 | console: 'readonly', 14 | process: 'readonly', 15 | Buffer: 'readonly', 16 | __dirname: 'readonly', 17 | __filename: 'readonly', 18 | global: 'readonly', 19 | module: 'readonly', 20 | require: 'readonly', 21 | exports: 'readonly', 22 | setTimeout: 'readonly', 23 | clearTimeout: 'readonly', 24 | setInterval: 'readonly', 25 | clearInterval: 'readonly', 26 | setImmediate: 'readonly', 27 | clearImmediate: 'readonly' 28 | } 29 | }, 30 | rules: { 31 | 'no-unused-vars': 'warn', 32 | 'no-console': 'off', 33 | 'prefer-const': 'error', 34 | 'no-var': 'error' 35 | } 36 | }, 37 | { 38 | files: ['**/*.ts'], 39 | languageOptions: { 40 | parser: tsParser, 41 | ecmaVersion: 2022, 42 | sourceType: 'module', 43 | globals: { 44 | console: 'readonly', 45 | process: 'readonly', 46 | Buffer: 'readonly', 47 | __dirname: 'readonly', 48 | __filename: 'readonly', 49 | global: 'readonly', 50 | module: 'readonly', 51 | require: 'readonly', 52 | exports: 'readonly', 53 | setTimeout: 'readonly', 54 | clearTimeout: 'readonly', 55 | setInterval: 'readonly', 56 | clearInterval: 'readonly', 57 | setImmediate: 'readonly', 58 | clearImmediate: 'readonly' 59 | } 60 | }, 61 | plugins: { 62 | '@typescript-eslint': tsEslint 63 | }, 64 | rules: { 65 | 'no-unused-vars': 'off', 66 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 67 | 'no-console': 'off', 68 | 'prefer-const': 'error', 69 | 'no-var': 'error' 70 | } 71 | }, 72 | { 73 | files: ['**/*.cjs'], 74 | languageOptions: { 75 | ecmaVersion: 2022, 76 | sourceType: 'commonjs', 77 | globals: { 78 | console: 'readonly', 79 | process: 'readonly', 80 | Buffer: 'readonly', 81 | __dirname: 'readonly', 82 | __filename: 'readonly', 83 | global: 'readonly', 84 | module: 'readonly', 85 | require: 'readonly', 86 | exports: 'readonly', 87 | setTimeout: 'readonly', 88 | clearTimeout: 'readonly', 89 | setInterval: 'readonly', 90 | clearInterval: 'readonly', 91 | setImmediate: 'readonly', 92 | clearImmediate: 'readonly' 93 | } 94 | }, 95 | rules: { 96 | 'no-unused-vars': 'warn', 97 | 'no-console': 'off', 98 | 'prefer-const': 'error', 99 | 'no-var': 'error' 100 | } 101 | }, 102 | { 103 | files: ['**/*.test.js', '**/*.test.ts', '**/tests/**/*.js', '**/tests/**/*.ts'], 104 | languageOptions: { 105 | globals: { 106 | describe: 'readonly', 107 | test: 'readonly', 108 | it: 'readonly', 109 | expect: 'readonly', 110 | beforeAll: 'readonly', 111 | afterAll: 'readonly', 112 | beforeEach: 'readonly', 113 | afterEach: 'readonly', 114 | jest: 'readonly' 115 | } 116 | } 117 | }, 118 | { 119 | ignores: ['build/**', 'node_modules/**', '*.config.js', '*.config.cjs'] 120 | } 121 | ]; ``` -------------------------------------------------------------------------------- /src/utils/platform-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as os from 'os'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | 5 | /** 6 | * Supported platform types 7 | */ 8 | export enum PlatformType { 9 | WINDOWS = 'windows', 10 | MACOS = 'macos', 11 | LINUX = 'linux', 12 | UNKNOWN = 'unknown' 13 | } 14 | 15 | /** 16 | * Detect the current platform 17 | * @returns The detected platform type 18 | */ 19 | export function detectPlatform(): PlatformType { 20 | const platform = process.platform; 21 | 22 | if (platform === 'win32') return PlatformType.WINDOWS; 23 | if (platform === 'darwin') return PlatformType.MACOS; 24 | if (platform === 'linux') return PlatformType.LINUX; 25 | 26 | return PlatformType.UNKNOWN; 27 | } 28 | 29 | /** 30 | * Get the default shell for the current platform 31 | * @returns Path to the default shell 32 | */ 33 | export function getDefaultShell(): string { 34 | const platform = detectPlatform(); 35 | 36 | switch (platform) { 37 | case PlatformType.WINDOWS: 38 | return process.env.COMSPEC || 'cmd.exe'; 39 | case PlatformType.MACOS: 40 | return '/bin/zsh'; 41 | case PlatformType.LINUX: 42 | return process.env.SHELL || '/bin/bash'; 43 | default: 44 | return process.env.SHELL || '/bin/sh'; 45 | } 46 | } 47 | 48 | /** 49 | * Validate if a shell path exists and is executable 50 | * @param shellPath Path to the shell 51 | * @returns True if the shell is valid 52 | */ 53 | export function validateShellPath(shellPath: string): boolean { 54 | try { 55 | return fs.existsSync(shellPath) && fs.statSync(shellPath).isFile(); 56 | } catch (error) { 57 | return false; 58 | } 59 | } 60 | 61 | /** 62 | * Get shell suggestions for each platform 63 | * @returns Record of platform types to array of suggested shells 64 | */ 65 | export function getShellSuggestions(): Record<PlatformType, string[]> { 66 | return { 67 | [PlatformType.WINDOWS]: ['cmd.exe', 'powershell.exe', 'pwsh.exe'], 68 | [PlatformType.MACOS]: ['/bin/zsh', '/bin/bash', '/bin/sh'], 69 | [PlatformType.LINUX]: ['/bin/bash', '/bin/sh', '/bin/zsh'], 70 | [PlatformType.UNKNOWN]: ['/bin/sh'] 71 | }; 72 | } 73 | 74 | /** 75 | * Get common locations for shells on the current platform 76 | * @returns Array of common shell locations 77 | */ 78 | export function getCommonShellLocations(): string[] { 79 | const platform = detectPlatform(); 80 | 81 | switch (platform) { 82 | case PlatformType.WINDOWS: 83 | return [ 84 | process.env.COMSPEC || 'C:\\Windows\\System32\\cmd.exe', 85 | 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 86 | 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' 87 | ]; 88 | case PlatformType.MACOS: 89 | return ['/bin/zsh', '/bin/bash', '/bin/sh']; 90 | case PlatformType.LINUX: 91 | return ['/bin/bash', '/bin/sh', '/usr/bin/bash', '/usr/bin/zsh']; 92 | default: 93 | return ['/bin/sh']; 94 | } 95 | } 96 | 97 | /** 98 | * Get helpful message for shell configuration 99 | * @returns A helpful message with shell configuration guidance 100 | */ 101 | export function getShellConfigurationHelp(): string { 102 | const platform = detectPlatform(); 103 | const suggestions = getShellSuggestions()[platform]; 104 | const locations = getCommonShellLocations(); 105 | 106 | let message = 'Shell Configuration Help:\n\n'; 107 | 108 | message += `Detected platform: ${platform}\n\n`; 109 | message += 'Suggested shells for this platform:\n'; 110 | suggestions.forEach(shell => { 111 | message += `- ${shell}\n`; 112 | }); 113 | 114 | message += '\nCommon shell locations on this platform:\n'; 115 | locations.forEach(location => { 116 | message += `- ${location}\n`; 117 | }); 118 | 119 | message += '\nTo configure a custom shell, provide the full path to the shell executable.'; 120 | 121 | return message; 122 | } 123 | ``` -------------------------------------------------------------------------------- /examples/client-example.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | import { McpClient } from '@modelcontextprotocol/sdk/client/index.js'; 3 | import { ChildProcessClientTransport } from '@modelcontextprotocol/sdk/client/child-process.js'; 4 | 5 | /** 6 | * Example client for the Mac Shell MCP Server 7 | * This demonstrates how to connect to the server and use its tools 8 | */ 9 | async function main() { 10 | // Create a client transport that connects to the server 11 | const transport = new ChildProcessClientTransport({ 12 | command: 'node', 13 | args: ['../build/index.js'], 14 | }); 15 | 16 | // Create the MCP client 17 | const client = new McpClient(); 18 | 19 | try { 20 | // Connect to the server 21 | await client.connect(transport); 22 | console.log('Connected to Mac Shell MCP Server'); 23 | 24 | // List available tools 25 | const tools = await client.listTools(); 26 | console.log('Available tools:'); 27 | tools.forEach(tool => { 28 | console.log(`- ${tool.name}: ${tool.description}`); 29 | }); 30 | 31 | // Get the whitelist 32 | console.log('\nWhitelisted commands:'); 33 | const whitelist = await client.callTool('get_whitelist', {}); 34 | console.log(whitelist.content[0].text); 35 | 36 | // Execute a safe command 37 | console.log('\nExecuting a safe command (ls -la):'); 38 | const lsResult = await client.callTool('execute_command', { 39 | command: 'ls', 40 | args: ['-la'], 41 | }); 42 | console.log(lsResult.content[0].text); 43 | 44 | // Add a command to the whitelist 45 | console.log('\nAdding a command to the whitelist:'); 46 | const addResult = await client.callTool('add_to_whitelist', { 47 | command: 'node', 48 | securityLevel: 'safe', 49 | description: 'Execute Node.js scripts', 50 | }); 51 | console.log(addResult.content[0].text); 52 | 53 | // Try executing a command that requires approval 54 | console.log('\nExecuting a command that requires approval (mkdir test-dir):'); 55 | try { 56 | const mkdirResult = await client.callTool('execute_command', { 57 | command: 'mkdir', 58 | args: ['test-dir'], 59 | }); 60 | console.log(mkdirResult.content[0].text); 61 | } catch (error) { 62 | console.log('Command requires approval. Getting pending commands...'); 63 | 64 | // Get pending commands 65 | const pendingCommands = await client.callTool('get_pending_commands', {}); 66 | const pendingCommandsObj = JSON.parse(pendingCommands.content[0].text); 67 | 68 | if (pendingCommandsObj.length > 0) { 69 | const commandId = pendingCommandsObj[0].id; 70 | console.log(`Approving command with ID: ${commandId}`); 71 | 72 | // Approve the command 73 | const approveResult = await client.callTool('approve_command', { 74 | commandId, 75 | }); 76 | console.log(approveResult.content[0].text); 77 | } 78 | } 79 | 80 | // Clean up - remove the test directory 81 | console.log('\nCleaning up:'); 82 | try { 83 | // This will fail because 'rm' is forbidden 84 | const rmResult = await client.callTool('execute_command', { 85 | command: 'rm', 86 | args: ['-rf', 'test-dir'], 87 | }); 88 | console.log(rmResult.content[0].text); 89 | } catch (error) { 90 | console.log(`Clean-up failed: ${error.message}`); 91 | console.log('Note: This is expected because "rm" is a forbidden command'); 92 | } 93 | 94 | } catch (error) { 95 | console.error('Error:', error); 96 | } finally { 97 | // Disconnect from the server 98 | await client.disconnect(); 99 | console.log('\nDisconnected from Mac Shell MCP Server'); 100 | } 101 | } 102 | 103 | main().catch(console.error); ``` -------------------------------------------------------------------------------- /docs/adr/004-cross-platform-support.md: -------------------------------------------------------------------------------- ```markdown 1 | # ADR 004: Cross-Platform Shell Support 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | The original Mac Shell MCP server was designed specifically for macOS with ZSH shell. However, there's a need to support multiple platforms (Windows, macOS, Linux) and various shells (Bash, ZSH, PowerShell, CMD, etc.) to make the tool more widely usable. 10 | 11 | Key limitations in the original implementation: 12 | 13 | 1. **Shell Path Hardcoding**: The server was hardcoded to use `/bin/zsh` as the default shell 14 | 2. **Command Set Assumptions**: The default whitelist included macOS/Unix commands that don't exist natively on Windows 15 | 3. **Path Handling**: Command validation extracted the base command by splitting on '/' which doesn't work for Windows backslash paths 16 | 4. **Naming and Documentation**: The server was explicitly named "mac-shell-mcp" and documented for macOS 17 | 18 | ## Decision 19 | 20 | We will refactor the server to be platform-agnostic with the following changes: 21 | 22 | 1. **Platform Detection**: Implement platform detection using `process.platform` to identify the current operating system 23 | 2. **Shell Selection**: Select appropriate default shell based on platform and allow shell path to be configurable 24 | 3. **Path Normalization**: Use Node.js `path` module for cross-platform path handling 25 | 4. **Platform-Specific Command Whitelists**: Implement separate command whitelists for each supported platform 26 | 5. **Rename and Rebrand**: Rename to "super-shell-mcp" and update documentation to reflect cross-platform support 27 | 28 | ## Consequences 29 | 30 | ### Positive 31 | 32 | - Works across Windows, macOS, and Linux 33 | - Supports various shells based on user preference 34 | - Maintains the same security model across platforms 35 | - Provides consistent experience regardless of platform 36 | - Increases the potential user base by supporting multiple platforms 37 | 38 | ### Negative 39 | 40 | - Increased complexity in command handling 41 | - Need to maintain separate command whitelists for each platform 42 | - Some commands may behave differently across platforms 43 | - Testing becomes more complex, requiring validation on multiple platforms 44 | 45 | ## Implementation 46 | 47 | The implementation uses a platform detection utility: 48 | 49 | ```typescript 50 | export function detectPlatform(): PlatformType { 51 | const platform = process.platform; 52 | 53 | if (platform === 'win32') return PlatformType.WINDOWS; 54 | if (platform === 'darwin') return PlatformType.MACOS; 55 | if (platform === 'linux') return PlatformType.LINUX; 56 | 57 | return PlatformType.UNKNOWN; 58 | } 59 | ``` 60 | 61 | Platform-specific shell detection: 62 | 63 | ```typescript 64 | export function getDefaultShell(): string { 65 | const platform = detectPlatform(); 66 | 67 | switch (platform) { 68 | case PlatformType.WINDOWS: 69 | return process.env.COMSPEC || 'cmd.exe'; 70 | case PlatformType.MACOS: 71 | return '/bin/zsh'; 72 | case PlatformType.LINUX: 73 | return process.env.SHELL || '/bin/bash'; 74 | default: 75 | return process.env.SHELL || '/bin/sh'; 76 | } 77 | } 78 | ``` 79 | 80 | Platform-specific command whitelists: 81 | 82 | ```typescript 83 | private initializeDefaultWhitelist(): void { 84 | const platformCommands = getPlatformSpecificCommands(); 85 | 86 | platformCommands.forEach(entry => { 87 | this.whitelist.set(entry.command, entry); 88 | }); 89 | } 90 | ``` 91 | 92 | Cross-platform path handling: 93 | 94 | ```typescript 95 | private validateCommand(command: string, args: string[]): CommandSecurityLevel | null { 96 | // Extract the base command (without path) using path.basename 97 | const baseCommand = path.basename(command); 98 | 99 | // Check if the command is in the whitelist 100 | const entry = this.whitelist.get(baseCommand); 101 | if (!entry) { 102 | return null; 103 | } 104 | 105 | // Rest of validation... 106 | } ``` -------------------------------------------------------------------------------- /tests/command-service.platform.test.js: -------------------------------------------------------------------------------- ```javascript 1 | const { CommandService, CommandSecurityLevel } = require('../build/services/command-service.js'); 2 | const { detectPlatform, PlatformType } = require('../build/utils/platform-utils.js'); 3 | 4 | describe('CommandService Platform Tests', () => { 5 | let commandService; 6 | const currentPlatform = detectPlatform(); 7 | 8 | beforeEach(() => { 9 | // Create a new CommandService instance for each test with auto-detected shell 10 | commandService = new CommandService(); 11 | }); 12 | 13 | test('should initialize with platform-specific whitelist', () => { 14 | const whitelist = commandService.getWhitelist(); 15 | expect(whitelist).toBeDefined(); 16 | expect(whitelist.length).toBeGreaterThan(0); 17 | 18 | // Check for common command across all platforms 19 | const echoCommand = whitelist.find(entry => entry.command === 'echo'); 20 | expect(echoCommand).toBeDefined(); 21 | expect(echoCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); 22 | 23 | // Platform-specific command checks 24 | if (currentPlatform === PlatformType.WINDOWS) { 25 | // Windows-specific commands 26 | const dirCommand = whitelist.find(entry => entry.command === 'dir'); 27 | expect(dirCommand).toBeDefined(); 28 | expect(dirCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); 29 | 30 | const delCommand = whitelist.find(entry => entry.command === 'del'); 31 | expect(delCommand).toBeDefined(); 32 | expect(delCommand.securityLevel).toBe(CommandSecurityLevel.FORBIDDEN); 33 | } else { 34 | // Unix-like platforms (macOS, Linux) 35 | const lsCommand = whitelist.find(entry => entry.command === 'ls'); 36 | expect(lsCommand).toBeDefined(); 37 | expect(lsCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); 38 | 39 | const rmCommand = whitelist.find(entry => entry.command === 'rm'); 40 | expect(rmCommand).toBeDefined(); 41 | expect(rmCommand.securityLevel).toBe(CommandSecurityLevel.FORBIDDEN); 42 | } 43 | }); 44 | 45 | test('should execute platform-specific safe command', async () => { 46 | // Choose a command based on platform 47 | const command = currentPlatform === PlatformType.WINDOWS ? 'echo' : 'echo'; 48 | const args = ['test']; 49 | 50 | const result = await commandService.executeCommand(command, args); 51 | 52 | expect(result).toBeDefined(); 53 | expect(result.stdout.trim()).toBe('test'); 54 | }); 55 | 56 | test('should reject platform-specific forbidden command', async () => { 57 | // Choose a forbidden command based on platform 58 | const command = currentPlatform === PlatformType.WINDOWS ? 'del' : 'rm'; 59 | const args = currentPlatform === PlatformType.WINDOWS ? ['test.txt'] : ['-rf', 'test']; 60 | 61 | await expect(commandService.executeCommand(command, args)).rejects.toThrow(); 62 | }); 63 | 64 | test('should queue platform-specific command requiring approval', async () => { 65 | // Set up event listener to capture pending command 66 | let pendingCommandId = null; 67 | commandService.on('command:pending', (pendingCommand) => { 68 | pendingCommandId = pendingCommand.id; 69 | }); 70 | 71 | // Choose a command requiring approval based on platform 72 | // Use a command that doesn't actually create anything to avoid test failures 73 | const command = currentPlatform === PlatformType.WINDOWS ? 'copy' : 'cp'; 74 | const args = ['nonexistent-file', 'nonexistent-copy']; 75 | 76 | // Execute a command that requires approval 77 | const executePromise = commandService.executeCommand(command, args); 78 | 79 | // Wait a bit for the event to fire 80 | await new Promise(resolve => setTimeout(resolve, 100)); 81 | 82 | // Check if we got a pending command 83 | expect(pendingCommandId).not.toBeNull(); 84 | 85 | // Get pending commands 86 | const pendingCommands = commandService.getPendingCommands(); 87 | expect(pendingCommands.length).toBe(1); 88 | expect(pendingCommands[0].id).toBe(pendingCommandId); 89 | 90 | // Approve the command 91 | const approvePromise = commandService.approveCommand(pendingCommandId); 92 | 93 | try { 94 | // Wait for both promises to resolve 95 | await Promise.all([executePromise, approvePromise]); 96 | } catch (error) { 97 | // Expect an error since we're trying to copy a non-existent file 98 | // This is expected and we can ignore it 99 | } 100 | 101 | // Check that there are no more pending commands 102 | expect(commandService.getPendingCommands().length).toBe(0); 103 | }); 104 | 105 | test('should deny platform-specific command requiring approval', async () => { 106 | // Set up event listener to capture pending command 107 | let pendingCommandId = null; 108 | commandService.on('command:pending', (pendingCommand) => { 109 | pendingCommandId = pendingCommand.id; 110 | }); 111 | 112 | // Choose a command requiring approval based on platform 113 | const command = currentPlatform === PlatformType.WINDOWS ? 'mkdir' : 'mkdir'; 114 | const args = ['test-dir']; 115 | 116 | // Execute a command that requires approval 117 | const executePromise = commandService.executeCommand(command, args); 118 | 119 | // Wait a bit for the event to fire 120 | await new Promise(resolve => setTimeout(resolve, 100)); 121 | 122 | // Check if we got a pending command 123 | expect(pendingCommandId).not.toBeNull(); 124 | 125 | // Deny the command 126 | commandService.denyCommand(pendingCommandId, 'Test denial'); 127 | 128 | // The execute promise should be rejected 129 | await expect(executePromise).rejects.toThrow('Test denial'); 130 | 131 | // Check that there are no more pending commands 132 | expect(commandService.getPendingCommands().length).toBe(0); 133 | }); 134 | }); ``` -------------------------------------------------------------------------------- /tests/command-service.test.js: -------------------------------------------------------------------------------- ```javascript 1 | const { CommandService, CommandSecurityLevel } = require('../build/services/command-service.js'); 2 | const { getDefaultShell } = require('../build/utils/platform-utils.js'); 3 | 4 | describe('CommandService', () => { 5 | let commandService; 6 | 7 | beforeEach(() => { 8 | // Create a new CommandService instance for each test with auto-detected shell 9 | commandService = new CommandService(); 10 | }); 11 | 12 | test('should initialize with default whitelist', () => { 13 | const whitelist = commandService.getWhitelist(); 14 | expect(whitelist).toBeDefined(); 15 | expect(whitelist.length).toBeGreaterThan(0); 16 | 17 | // Check if common commands are in the whitelist 18 | const lsCommand = whitelist.find(entry => entry.command === 'ls'); 19 | expect(lsCommand).toBeDefined(); 20 | expect(lsCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); 21 | 22 | const rmCommand = whitelist.find(entry => entry.command === 'rm'); 23 | expect(rmCommand).toBeDefined(); 24 | expect(rmCommand.securityLevel).toBe(CommandSecurityLevel.FORBIDDEN); 25 | }); 26 | 27 | test('should add command to whitelist', () => { 28 | const testCommand = { 29 | command: 'test-command', 30 | securityLevel: CommandSecurityLevel.SAFE, 31 | description: 'Test command' 32 | }; 33 | 34 | commandService.addToWhitelist(testCommand); 35 | 36 | const whitelist = commandService.getWhitelist(); 37 | const addedCommand = whitelist.find(entry => entry.command === 'test-command'); 38 | 39 | expect(addedCommand).toBeDefined(); 40 | expect(addedCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); 41 | expect(addedCommand.description).toBe('Test command'); 42 | }); 43 | 44 | test('should update command security level', () => { 45 | // First add a command 46 | const testCommand = { 47 | command: 'test-command', 48 | securityLevel: CommandSecurityLevel.SAFE, 49 | description: 'Test command' 50 | }; 51 | 52 | commandService.addToWhitelist(testCommand); 53 | 54 | // Then update its security level 55 | commandService.updateSecurityLevel('test-command', CommandSecurityLevel.REQUIRES_APPROVAL); 56 | 57 | const whitelist = commandService.getWhitelist(); 58 | const updatedCommand = whitelist.find(entry => entry.command === 'test-command'); 59 | 60 | expect(updatedCommand).toBeDefined(); 61 | expect(updatedCommand.securityLevel).toBe(CommandSecurityLevel.REQUIRES_APPROVAL); 62 | }); 63 | 64 | test('should remove command from whitelist', () => { 65 | // First add a command 66 | const testCommand = { 67 | command: 'test-command', 68 | securityLevel: CommandSecurityLevel.SAFE, 69 | description: 'Test command' 70 | }; 71 | 72 | commandService.addToWhitelist(testCommand); 73 | 74 | // Then remove it 75 | commandService.removeFromWhitelist('test-command'); 76 | 77 | const whitelist = commandService.getWhitelist(); 78 | const removedCommand = whitelist.find(entry => entry.command === 'test-command'); 79 | 80 | expect(removedCommand).toBeUndefined(); 81 | }); 82 | 83 | test('should execute safe command', async () => { 84 | // Execute a safe command (echo) 85 | const result = await commandService.executeCommand('echo', ['test']); 86 | 87 | expect(result).toBeDefined(); 88 | expect(result.stdout.trim()).toBe('test'); 89 | }); 90 | 91 | test('should reject forbidden command', async () => { 92 | // Try to execute a forbidden command (rm) 93 | await expect(commandService.executeCommand('rm', ['-rf', 'test'])).rejects.toThrow(); 94 | }); 95 | 96 | test('should queue command requiring approval', async () => { 97 | // Set up event listener to capture pending command 98 | let pendingCommandId = null; 99 | commandService.on('command:pending', (pendingCommand) => { 100 | pendingCommandId = pendingCommand.id; 101 | }); 102 | 103 | // Execute a command that requires approval 104 | // Use a command that doesn't actually create anything to avoid test failures 105 | const executePromise = commandService.executeCommand('cp', ['nonexistent-file', 'nonexistent-copy']); 106 | 107 | // Wait a bit for the event to fire 108 | await new Promise(resolve => setTimeout(resolve, 100)); 109 | 110 | // Check if we got a pending command 111 | expect(pendingCommandId).not.toBeNull(); 112 | 113 | // Get pending commands 114 | const pendingCommands = commandService.getPendingCommands(); 115 | expect(pendingCommands.length).toBe(1); 116 | expect(pendingCommands[0].id).toBe(pendingCommandId); 117 | 118 | // Approve the command 119 | const approvePromise = commandService.approveCommand(pendingCommandId); 120 | 121 | try { 122 | // Wait for both promises to resolve 123 | await Promise.all([executePromise, approvePromise]); 124 | } catch (error) { 125 | // Expect an error since we're trying to copy a non-existent file 126 | // This is expected and we can ignore it 127 | } 128 | 129 | // Check that there are no more pending commands 130 | expect(commandService.getPendingCommands().length).toBe(0); 131 | 132 | // Clean up 133 | try { 134 | await commandService.executeCommand('rmdir', ['test-dir']); 135 | } catch (error) { 136 | // Ignore cleanup errors 137 | } 138 | }); 139 | 140 | test('should deny command requiring approval', async () => { 141 | // Set up event listener to capture pending command 142 | let pendingCommandId = null; 143 | commandService.on('command:pending', (pendingCommand) => { 144 | pendingCommandId = pendingCommand.id; 145 | }); 146 | 147 | // Execute a command that requires approval (mkdir) 148 | const executePromise = commandService.executeCommand('mkdir', ['test-dir']); 149 | 150 | // Wait a bit for the event to fire 151 | await new Promise(resolve => setTimeout(resolve, 100)); 152 | 153 | // Check if we got a pending command 154 | expect(pendingCommandId).not.toBeNull(); 155 | 156 | // Deny the command 157 | commandService.denyCommand(pendingCommandId, 'Test denial'); 158 | 159 | // The execute promise should be rejected 160 | await expect(executePromise).rejects.toThrow('Test denial'); 161 | 162 | // Check that there are no more pending commands 163 | expect(commandService.getPendingCommands().length).toBe(0); 164 | }); 165 | }); ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | ## [2.0.13] - 2025-03-14 8 | 9 | ### Fixed 10 | - Updated the package.json file to include the proper github repo information 11 | 12 | ## [2.0.12] - 2025-03-14 13 | 14 | ### Changed 15 | - Converted project from CommonJS to ESM module system 16 | - Updated TypeScript configuration to use NodeNext module system 17 | - Added ESM-compatible workaround for `__dirname` in ESM context 18 | 19 | ### Fixed 20 | - Fixed Jest test suite to work with ESM modules 21 | - Added CommonJS-compatible mock modules for testing 22 | - Updated module imports to use ESM syntax with .js extensions 23 | 24 | ## [2.0.11] - 2025-03-14 25 | 26 | ### Added 27 | - Published package to npm at https://www.npmjs.com/package/super-shell-mcp 28 | - Updated README.md to highlight NPX installation method as the recommended approach 29 | - Added benefits of using NPX in documentation 30 | - Enhanced configuration examples for easier setup 31 | 32 | ### Changed 33 | - Reorganized documentation to prioritize NPX installation method 34 | - Simplified GitHub installation instructions 35 | - Improved package publishing documentation 36 | 37 | ## [2.0.10] - 2025-03-14 38 | 39 | ### Added 40 | - Added logs directory with .gitkeep file to ensure logs directory is tracked by Git 41 | - Enhanced error handling for command approval process 42 | - Improved logging for command execution and approval workflow 43 | 44 | ### Fixed 45 | - Fixed version inconsistencies across package.json, package-lock.json, and src/index.ts 46 | - Improved documentation for logging system 47 | - Enhanced cross-platform compatibility for log file paths 48 | 49 | ## [2.0.9] - 2025-03-14 50 | 51 | ### Added 52 | - Added additional logging for command approval workflow 53 | - Improved error handling for command execution 54 | - Enhanced debugging capabilities with more detailed logs 55 | 56 | ### Fixed 57 | - Fixed minor issues with command approval workflow 58 | - Improved reliability of command execution across platforms 59 | - Enhanced error messages for better troubleshooting 60 | 61 | ## [2.0.8] - 2025-03-13 62 | 63 | ### Added 64 | - Added comprehensive logging system with file-based logs 65 | - Implemented non-blocking command approval workflow 66 | - Added new `queueCommandForApprovalNonBlocking` method to CommandService 67 | 68 | ### Fixed 69 | - Fixed timeout issue with commands requiring approval by implementing non-blocking approval workflow 70 | - Improved user experience by providing immediate feedback for commands requiring approval 71 | - Enhanced error handling for commands requiring approval 72 | - Fixed issue where pending commands would cause client timeouts 73 | 74 | ## [2.0.7] - 2025-03-13 75 | 76 | ### Fixed 77 | - Fixed timeout issue when using the "Approve" button in Roo Code client 78 | - Improved error handling in `handleApproveCommand` method to bypass Promise resolution mechanism 79 | - Added detailed logging for command approval process to aid debugging 80 | - Enhanced direct command execution in approval workflow to prevent timeouts 81 | - Fixed TypeScript errors related to error handling in command execution 82 | 83 | ## [2.0.6] - 2025-03-13 84 | 85 | ### Fixed 86 | - Fixed command approval workflow to prevent timeout errors 87 | - Added immediate detection of commands requiring approval 88 | - Improved error messages for commands requiring approval with clear instructions 89 | - Added direct guidance to use get_pending_commands and approve_command functions 90 | 91 | ## [2.0.5] - 2025-03-13 92 | 93 | ### Added 94 | - Improved command approval workflow with timeout detection 95 | - Added guidance for AI assistants when command approval times out 96 | - Enhanced error messages for commands requiring approval 97 | 98 | ### Fixed 99 | - Module compatibility issues between ES Modules and CommonJS in tests 100 | - Updated TypeScript configuration to use CommonJS module system for better test compatibility 101 | - Removed unnecessary CommonJS compatibility code from source files 102 | - Changed package.json "type" from "module" to "commonjs" for consistent module system 103 | 104 | ## [2.0.4] - 2025-03-13 105 | 106 | ### Fixed 107 | - Module compatibility issues between ES Modules and CommonJS in tests 108 | - Updated TypeScript configuration to use CommonJS module system for better test compatibility 109 | - Removed unnecessary CommonJS compatibility code from source files 110 | - Changed package.json "type" from "module" to "commonjs" for consistent module system 111 | 112 | ## [2.0.3] - 2025-03-12 113 | 114 | ### Added 115 | - NPX best practices documentation in README.md 116 | - Improved package.json configuration for NPX compatibility 117 | 118 | ### Changed 119 | - Updated TypeScript configuration to use NodeNext module system for better ES Modules support 120 | - Added prepare script to ensure compilation happens on install 121 | - Added files field to package.json to specify which files to include when publishing 122 | - Simplified build script to use chmod directly instead of custom script 123 | 124 | ## [2.0.2] - 2025-03-12 125 | 126 | ### Fixed 127 | - Test suite compatibility with cross-platform environments 128 | - Module system compatibility between ESM and CommonJS 129 | - Fixed platform-specific test cases to work on all operating systems 130 | - Updated TypeScript configuration for better CommonJS compatibility 131 | - Improved test reliability by using non-filesystem-modifying commands 132 | 133 | ## [2.0.1] - 2025-03-12 134 | 135 | ### Added 136 | - Platform-aware test suite that adapts to the current operating system 137 | - Cross-platform build script that works on Windows, macOS, and Linux 138 | - Enhanced platform-specific documentation with configuration examples 139 | - Troubleshooting guide for common cross-platform issues 140 | - Detailed shell path examples for each supported platform 141 | 142 | ### Fixed 143 | - Build script compatibility with Windows (removed Unix-specific chmod) 144 | - Test suite compatibility with Windows command sets 145 | - Path handling in shell validation for Windows paths 146 | 147 | ## [2.0.0] - 2025-03-12 148 | 149 | ### Added 150 | - Cross-platform support for Windows, macOS, and Linux 151 | - Platform detection using `process.platform` 152 | - Auto-detection of appropriate shell based on platform 153 | - Platform-specific command whitelists 154 | - New `get_platform_info` tool to retrieve platform and shell information 155 | - Support for Windows shells (cmd.exe, PowerShell) 156 | - Support for Linux shells (bash, sh) 157 | - New ADR for cross-platform support 158 | 159 | ### Changed 160 | - Renamed from "mac-shell-mcp" to "super-shell-mcp" 161 | - Updated path handling to use Node.js path module for cross-platform compatibility 162 | - Modified command validation to work with Windows paths 163 | - Updated documentation to reflect cross-platform support 164 | - Refactored code to be platform-agnostic 165 | - Made shell configurable with auto-detection as fallback 166 | 167 | ## [1.0.3] - 2025-03-12 168 | 169 | ### Fixed 170 | 171 | - Improved documentation for Claude Desktop configuration which uses boolean value for `alwaysAllow` 172 | - Added separate configuration examples for Roo Code and Claude Desktop 173 | - Clarified that Roo Code uses array format while Claude Desktop uses boolean format 174 | - Added explicit note that the `alwaysAllow` parameter is processed by the MCP client, not the server 175 | 176 | ## [1.0.2] - 2025-03-12 177 | 178 | ### Fixed 179 | 180 | - Fixed MCP configuration format to use an empty array `[]` for `alwaysAllow` instead of `false` 181 | - Updated all configuration examples in README.md to use the correct format 182 | - Fixed error "Invalid config: missing or invalid parameters" when adding to MCP settings 183 | 184 | ## [1.0.1] - 2025-03-12 185 | 186 | ### Added 187 | 188 | - Support for using the server as an npm package with npx 189 | - Added bin field to package.json for CLI usage 190 | - Improved MCP configuration instructions for Roo Code and Claude Desktop 191 | - Added examples for using with npx directly from GitHub 192 | 193 | ## [1.0.0] - 2025-03-12 194 | 195 | ### Added 196 | 197 | - Initial release of the Mac Shell MCP Server 198 | - Command execution service with ZSH shell support 199 | - Command whitelisting system with three security levels: 200 | - Safe commands (no approval required) 201 | - Commands requiring approval 202 | - Forbidden commands 203 | - Pre-configured whitelist with common safe commands 204 | - Approval workflow for potentially dangerous commands 205 | - MCP tools for command execution and whitelist management: 206 | - `execute_command`: Execute shell commands 207 | - `get_whitelist`: Get the list of whitelisted commands 208 | - `add_to_whitelist`: Add a command to the whitelist 209 | - `update_security_level`: Update a command's security level 210 | - `remove_from_whitelist`: Remove a command from the whitelist 211 | - `get_pending_commands`: Get commands pending approval 212 | - `approve_command`: Approve a pending command 213 | - `deny_command`: Deny a pending command 214 | - Comprehensive test suite for the command service 215 | - Example client implementation 216 | - Documentation and configuration examples ``` -------------------------------------------------------------------------------- /src/utils/command-whitelist-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CommandSecurityLevel, CommandWhitelistEntry } from '../services/command-service.js'; 2 | import { PlatformType, detectPlatform } from './platform-utils.js'; 3 | 4 | /** 5 | * Get common safe commands that work across all platforms 6 | * @returns Array of common safe command whitelist entries 7 | */ 8 | export function getCommonSafeCommands(): CommandWhitelistEntry[] { 9 | return [ 10 | { 11 | command: 'echo', 12 | securityLevel: CommandSecurityLevel.SAFE, 13 | description: 'Print text to standard output' 14 | } 15 | ]; 16 | } 17 | 18 | /** 19 | * Get Windows-specific safe commands 20 | * @returns Array of Windows safe command whitelist entries 21 | */ 22 | export function getWindowsSafeCommands(): CommandWhitelistEntry[] { 23 | return [ 24 | { 25 | command: 'dir', 26 | securityLevel: CommandSecurityLevel.SAFE, 27 | description: 'List directory contents' 28 | }, 29 | { 30 | command: 'type', 31 | securityLevel: CommandSecurityLevel.SAFE, 32 | description: 'Display the contents of a text file' 33 | }, 34 | { 35 | command: 'cd', 36 | securityLevel: CommandSecurityLevel.SAFE, 37 | description: 'Change directory' 38 | }, 39 | { 40 | command: 'findstr', 41 | securityLevel: CommandSecurityLevel.SAFE, 42 | description: 'Search for strings in files' 43 | }, 44 | { 45 | command: 'where', 46 | securityLevel: CommandSecurityLevel.SAFE, 47 | description: 'Locate programs' 48 | }, 49 | { 50 | command: 'whoami', 51 | securityLevel: CommandSecurityLevel.SAFE, 52 | description: 'Display current user' 53 | }, 54 | { 55 | command: 'hostname', 56 | securityLevel: CommandSecurityLevel.SAFE, 57 | description: 'Display computer name' 58 | }, 59 | { 60 | command: 'ver', 61 | securityLevel: CommandSecurityLevel.SAFE, 62 | description: 'Display operating system version' 63 | } 64 | ]; 65 | } 66 | 67 | /** 68 | * Get macOS-specific safe commands 69 | * @returns Array of macOS safe command whitelist entries 70 | */ 71 | export function getMacOSSafeCommands(): CommandWhitelistEntry[] { 72 | return [ 73 | { 74 | command: 'ls', 75 | securityLevel: CommandSecurityLevel.SAFE, 76 | description: 'List directory contents' 77 | }, 78 | { 79 | command: 'pwd', 80 | securityLevel: CommandSecurityLevel.SAFE, 81 | description: 'Print working directory' 82 | }, 83 | { 84 | command: 'cat', 85 | securityLevel: CommandSecurityLevel.SAFE, 86 | description: 'Concatenate and print files' 87 | }, 88 | { 89 | command: 'grep', 90 | securityLevel: CommandSecurityLevel.SAFE, 91 | description: 'Search for patterns in files' 92 | }, 93 | { 94 | command: 'find', 95 | securityLevel: CommandSecurityLevel.SAFE, 96 | description: 'Find files in a directory hierarchy' 97 | }, 98 | { 99 | command: 'cd', 100 | securityLevel: CommandSecurityLevel.SAFE, 101 | description: 'Change directory' 102 | }, 103 | { 104 | command: 'head', 105 | securityLevel: CommandSecurityLevel.SAFE, 106 | description: 'Output the first part of files' 107 | }, 108 | { 109 | command: 'tail', 110 | securityLevel: CommandSecurityLevel.SAFE, 111 | description: 'Output the last part of files' 112 | }, 113 | { 114 | command: 'wc', 115 | securityLevel: CommandSecurityLevel.SAFE, 116 | description: 'Print newline, word, and byte counts' 117 | } 118 | ]; 119 | } 120 | 121 | /** 122 | * Get Linux-specific safe commands 123 | * @returns Array of Linux safe command whitelist entries 124 | */ 125 | export function getLinuxSafeCommands(): CommandWhitelistEntry[] { 126 | // Linux safe commands are similar to macOS 127 | return getMacOSSafeCommands(); 128 | } 129 | 130 | /** 131 | * Get Windows-specific commands requiring approval 132 | * @returns Array of Windows command whitelist entries requiring approval 133 | */ 134 | export function getWindowsApprovalCommands(): CommandWhitelistEntry[] { 135 | return [ 136 | { 137 | command: 'copy', 138 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 139 | description: 'Copy files' 140 | }, 141 | { 142 | command: 'move', 143 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 144 | description: 'Move files' 145 | }, 146 | { 147 | command: 'mkdir', 148 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 149 | description: 'Create directories' 150 | }, 151 | { 152 | command: 'rmdir', 153 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 154 | description: 'Remove directories' 155 | }, 156 | { 157 | command: 'rename', 158 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 159 | description: 'Rename files' 160 | }, 161 | { 162 | command: 'attrib', 163 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 164 | description: 'Change file attributes' 165 | } 166 | ]; 167 | } 168 | 169 | /** 170 | * Get macOS-specific commands requiring approval 171 | * @returns Array of macOS command whitelist entries requiring approval 172 | */ 173 | export function getMacOSApprovalCommands(): CommandWhitelistEntry[] { 174 | return [ 175 | { 176 | command: 'mv', 177 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 178 | description: 'Move (rename) files' 179 | }, 180 | { 181 | command: 'cp', 182 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 183 | description: 'Copy files and directories' 184 | }, 185 | { 186 | command: 'mkdir', 187 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 188 | description: 'Create directories' 189 | }, 190 | { 191 | command: 'touch', 192 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 193 | description: 'Change file timestamps or create empty files' 194 | }, 195 | { 196 | command: 'chmod', 197 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 198 | description: 'Change file mode bits' 199 | }, 200 | { 201 | command: 'chown', 202 | securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, 203 | description: 'Change file owner and group' 204 | } 205 | ]; 206 | } 207 | 208 | /** 209 | * Get Linux-specific commands requiring approval 210 | * @returns Array of Linux command whitelist entries requiring approval 211 | */ 212 | export function getLinuxApprovalCommands(): CommandWhitelistEntry[] { 213 | // Linux approval commands are similar to macOS 214 | return getMacOSApprovalCommands(); 215 | } 216 | 217 | /** 218 | * Get Windows-specific forbidden commands 219 | * @returns Array of Windows forbidden command whitelist entries 220 | */ 221 | export function getWindowsForbiddenCommands(): CommandWhitelistEntry[] { 222 | return [ 223 | { 224 | command: 'del', 225 | securityLevel: CommandSecurityLevel.FORBIDDEN, 226 | description: 'Delete files' 227 | }, 228 | { 229 | command: 'erase', 230 | securityLevel: CommandSecurityLevel.FORBIDDEN, 231 | description: 'Delete files' 232 | }, 233 | { 234 | command: 'format', 235 | securityLevel: CommandSecurityLevel.FORBIDDEN, 236 | description: 'Format a disk' 237 | }, 238 | { 239 | command: 'runas', 240 | securityLevel: CommandSecurityLevel.FORBIDDEN, 241 | description: 'Execute a program as another user' 242 | } 243 | ]; 244 | } 245 | 246 | /** 247 | * Get macOS-specific forbidden commands 248 | * @returns Array of macOS forbidden command whitelist entries 249 | */ 250 | export function getMacOSForbiddenCommands(): CommandWhitelistEntry[] { 251 | return [ 252 | { 253 | command: 'rm', 254 | securityLevel: CommandSecurityLevel.FORBIDDEN, 255 | description: 'Remove files or directories' 256 | }, 257 | { 258 | command: 'sudo', 259 | securityLevel: CommandSecurityLevel.FORBIDDEN, 260 | description: 'Execute a command as another user' 261 | } 262 | ]; 263 | } 264 | 265 | /** 266 | * Get Linux-specific forbidden commands 267 | * @returns Array of Linux forbidden command whitelist entries 268 | */ 269 | export function getLinuxForbiddenCommands(): CommandWhitelistEntry[] { 270 | // Linux forbidden commands are similar to macOS 271 | return getMacOSForbiddenCommands(); 272 | } 273 | 274 | /** 275 | * Get platform-specific command whitelist entries 276 | * @returns Array of command whitelist entries for the current platform 277 | */ 278 | export function getPlatformSpecificCommands(): CommandWhitelistEntry[] { 279 | const platform = detectPlatform(); 280 | 281 | let safeCommands: CommandWhitelistEntry[] = []; 282 | let approvalCommands: CommandWhitelistEntry[] = []; 283 | let forbiddenCommands: CommandWhitelistEntry[] = []; 284 | 285 | // Add common safe commands that work across all platforms 286 | const commonSafeCommands = getCommonSafeCommands(); 287 | 288 | // Add platform-specific commands 289 | switch (platform) { 290 | case PlatformType.WINDOWS: 291 | safeCommands = getWindowsSafeCommands(); 292 | approvalCommands = getWindowsApprovalCommands(); 293 | forbiddenCommands = getWindowsForbiddenCommands(); 294 | break; 295 | case PlatformType.MACOS: 296 | safeCommands = getMacOSSafeCommands(); 297 | approvalCommands = getMacOSApprovalCommands(); 298 | forbiddenCommands = getMacOSForbiddenCommands(); 299 | break; 300 | case PlatformType.LINUX: 301 | safeCommands = getLinuxSafeCommands(); 302 | approvalCommands = getLinuxApprovalCommands(); 303 | forbiddenCommands = getLinuxForbiddenCommands(); 304 | break; 305 | default: 306 | // Use Unix-like defaults for unknown platforms 307 | safeCommands = getLinuxSafeCommands(); 308 | approvalCommands = getLinuxApprovalCommands(); 309 | forbiddenCommands = getLinuxForbiddenCommands(); 310 | } 311 | 312 | // Combine all commands 313 | return [...commonSafeCommands, ...safeCommands, ...approvalCommands, ...forbiddenCommands]; 314 | } 315 | ``` -------------------------------------------------------------------------------- /jest.setup.cjs: -------------------------------------------------------------------------------- ``` 1 | // Mock the ESM modules with CommonJS equivalents for Jest 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { EventEmitter } = require('events'); 5 | const { execFile } = require('child_process'); 6 | const { promisify } = require('util'); 7 | const { randomUUID } = require('crypto'); 8 | 9 | // Define the CommandSecurityLevel enum 10 | const CommandSecurityLevel = { 11 | SAFE: 'safe', 12 | REQUIRES_APPROVAL: 'requires_approval', 13 | FORBIDDEN: 'forbidden' 14 | }; 15 | 16 | // Define the PlatformType enum 17 | const PlatformType = { 18 | WINDOWS: 'windows', 19 | MACOS: 'macos', 20 | LINUX: 'linux', 21 | UNKNOWN: 'unknown' 22 | }; 23 | 24 | // Mock the platform-utils module 25 | const detectPlatform = () => { 26 | const platform = process.platform; 27 | if (platform === 'win32') return PlatformType.WINDOWS; 28 | if (platform === 'darwin') return PlatformType.MACOS; 29 | if (platform === 'linux') return PlatformType.LINUX; 30 | return PlatformType.UNKNOWN; 31 | }; 32 | 33 | const getDefaultShell = () => { 34 | const platform = detectPlatform(); 35 | switch (platform) { 36 | case PlatformType.WINDOWS: 37 | return process.env.COMSPEC || 'cmd.exe'; 38 | case PlatformType.MACOS: 39 | return '/bin/zsh'; 40 | case PlatformType.LINUX: 41 | return process.env.SHELL || '/bin/bash'; 42 | default: 43 | return process.env.SHELL || '/bin/sh'; 44 | } 45 | }; 46 | 47 | const validateShellPath = (shellPath) => { 48 | try { 49 | return fs.existsSync(shellPath) && fs.statSync(shellPath).isFile(); 50 | } catch (error) { 51 | return false; 52 | } 53 | }; 54 | 55 | const getShellSuggestions = () => ({ 56 | [PlatformType.WINDOWS]: ['cmd.exe', 'powershell.exe', 'pwsh.exe'], 57 | [PlatformType.MACOS]: ['/bin/zsh', '/bin/bash', '/bin/sh'], 58 | [PlatformType.LINUX]: ['/bin/bash', '/bin/sh', '/bin/zsh'], 59 | [PlatformType.UNKNOWN]: ['/bin/sh'] 60 | }); 61 | 62 | const getCommonShellLocations = () => { 63 | const platform = detectPlatform(); 64 | switch (platform) { 65 | case PlatformType.WINDOWS: 66 | return [ 67 | process.env.COMSPEC || 'C:\\Windows\\System32\\cmd.exe', 68 | 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 69 | 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' 70 | ]; 71 | case PlatformType.MACOS: 72 | return ['/bin/zsh', '/bin/bash', '/bin/sh']; 73 | case PlatformType.LINUX: 74 | return ['/bin/bash', '/bin/sh', '/usr/bin/bash', '/usr/bin/zsh']; 75 | default: 76 | return ['/bin/sh']; 77 | } 78 | }; 79 | 80 | const getShellConfigurationHelp = () => { 81 | const platform = detectPlatform(); 82 | const suggestions = getShellSuggestions()[platform]; 83 | const locations = getCommonShellLocations(); 84 | 85 | let message = 'Shell Configuration Help:\n\n'; 86 | message += `Detected platform: ${platform}\n\n`; 87 | message += 'Suggested shells for this platform:\n'; 88 | suggestions.forEach(shell => { 89 | message += `- ${shell}\n`; 90 | }); 91 | 92 | message += '\nCommon shell locations on this platform:\n'; 93 | locations.forEach(location => { 94 | message += `- ${location}\n`; 95 | }); 96 | 97 | message += '\nTo configure a custom shell, provide the full path to the shell executable.'; 98 | 99 | return message; 100 | }; 101 | 102 | // Mock the CommandService class 103 | class CommandService extends EventEmitter { 104 | constructor(shell, defaultTimeout = 30000) { 105 | super(); 106 | this.shell = shell || getDefaultShell(); 107 | this.whitelist = new Map(); 108 | this.pendingCommands = new Map(); 109 | this.defaultTimeout = defaultTimeout; 110 | this.initializeDefaultWhitelist(); 111 | } 112 | 113 | getShell() { 114 | return this.shell; 115 | } 116 | 117 | initializeDefaultWhitelist() { 118 | const platform = detectPlatform(); 119 | const commands = []; 120 | 121 | // Common commands for all platforms 122 | commands.push({ command: 'echo', securityLevel: CommandSecurityLevel.SAFE, description: 'Print text to standard output' }); 123 | 124 | // Platform-specific commands 125 | if (platform === PlatformType.WINDOWS) { 126 | commands.push({ command: 'dir', securityLevel: CommandSecurityLevel.SAFE, description: 'List directory contents' }); 127 | commands.push({ command: 'copy', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Copy files' }); 128 | commands.push({ command: 'mkdir', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Create directories' }); 129 | commands.push({ command: 'del', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Delete files' }); 130 | } else { 131 | commands.push({ command: 'ls', securityLevel: CommandSecurityLevel.SAFE, description: 'List directory contents' }); 132 | commands.push({ command: 'cat', securityLevel: CommandSecurityLevel.SAFE, description: 'Concatenate and print files' }); 133 | commands.push({ command: 'grep', securityLevel: CommandSecurityLevel.SAFE, description: 'Search for patterns in files' }); 134 | commands.push({ command: 'find', securityLevel: CommandSecurityLevel.SAFE, description: 'Find files in a directory hierarchy' }); 135 | commands.push({ command: 'cd', securityLevel: CommandSecurityLevel.SAFE, description: 'Change directory' }); 136 | commands.push({ command: 'head', securityLevel: CommandSecurityLevel.SAFE, description: 'Output the first part of files' }); 137 | commands.push({ command: 'tail', securityLevel: CommandSecurityLevel.SAFE, description: 'Output the last part of files' }); 138 | commands.push({ command: 'wc', securityLevel: CommandSecurityLevel.SAFE, description: 'Print newline, word, and byte counts' }); 139 | commands.push({ command: 'mv', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Move (rename) files' }); 140 | commands.push({ command: 'cp', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Copy files and directories' }); 141 | commands.push({ command: 'mkdir', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Create directories' }); 142 | commands.push({ command: 'touch', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file timestamps or create empty files' }); 143 | commands.push({ command: 'chmod', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file mode bits' }); 144 | commands.push({ command: 'chown', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file owner and group' }); 145 | commands.push({ command: 'rm', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Remove files or directories' }); 146 | commands.push({ command: 'sudo', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Execute a command as another user' }); 147 | } 148 | 149 | commands.forEach(entry => { 150 | this.whitelist.set(entry.command, entry); 151 | }); 152 | } 153 | 154 | addToWhitelist(entry) { 155 | this.whitelist.set(entry.command, entry); 156 | } 157 | 158 | removeFromWhitelist(command) { 159 | this.whitelist.delete(command); 160 | } 161 | 162 | updateSecurityLevel(command, securityLevel) { 163 | const entry = this.whitelist.get(command); 164 | if (entry) { 165 | entry.securityLevel = securityLevel; 166 | this.whitelist.set(command, entry); 167 | } 168 | } 169 | 170 | getWhitelist() { 171 | return Array.from(this.whitelist.values()); 172 | } 173 | 174 | getPendingCommands() { 175 | return Array.from(this.pendingCommands.values()); 176 | } 177 | 178 | validateCommand(command, args) { 179 | const baseCommand = path.basename(command); 180 | const entry = this.whitelist.get(baseCommand); 181 | 182 | if (!entry) { 183 | return null; 184 | } 185 | 186 | if (entry.securityLevel === CommandSecurityLevel.FORBIDDEN) { 187 | return CommandSecurityLevel.FORBIDDEN; 188 | } 189 | 190 | if (entry.allowedArgs && entry.allowedArgs.length > 0) { 191 | const allArgsValid = args.every((arg, index) => { 192 | if (index >= (entry.allowedArgs?.length || 0)) { 193 | return false; 194 | } 195 | 196 | const pattern = entry.allowedArgs?.[index]; 197 | if (!pattern) { 198 | return false; 199 | } 200 | 201 | if (typeof pattern === 'string') { 202 | return arg === pattern; 203 | } else { 204 | return pattern.test(arg); 205 | } 206 | }); 207 | 208 | if (!allArgsValid) { 209 | return CommandSecurityLevel.REQUIRES_APPROVAL; 210 | } 211 | } 212 | 213 | return entry.securityLevel; 214 | } 215 | 216 | async executeCommand(command, args = [], options = {}) { 217 | const securityLevel = this.validateCommand(command, args); 218 | 219 | if (securityLevel === null) { 220 | throw new Error(`Command not whitelisted: ${command}`); 221 | } 222 | 223 | if (securityLevel === CommandSecurityLevel.FORBIDDEN) { 224 | throw new Error(`Command is forbidden: ${command}`); 225 | } 226 | 227 | if (securityLevel === CommandSecurityLevel.REQUIRES_APPROVAL) { 228 | return this.queueCommandForApproval(command, args, options.requestedBy); 229 | } 230 | 231 | try { 232 | const timeout = options.timeout || this.defaultTimeout; 233 | const execFileAsync = promisify(execFile); 234 | const { stdout, stderr } = await execFileAsync(command, args, { 235 | timeout, 236 | shell: this.shell 237 | }); 238 | 239 | return { stdout, stderr }; 240 | } catch (error) { 241 | if (error instanceof Error) { 242 | throw new Error(`Command execution failed: ${error.message}`); 243 | } 244 | throw error; 245 | } 246 | } 247 | 248 | queueCommandForApproval(command, args = [], requestedBy) { 249 | return new Promise((resolve, reject) => { 250 | const id = randomUUID(); 251 | const pendingCommand = { 252 | id, 253 | command, 254 | args, 255 | requestedAt: new Date(), 256 | requestedBy, 257 | resolve: (result) => resolve(result), 258 | reject: (error) => reject(error) 259 | }; 260 | 261 | this.pendingCommands.set(id, pendingCommand); 262 | this.emit('command:pending', pendingCommand); 263 | 264 | setTimeout(() => { 265 | if (this.pendingCommands.has(id)) { 266 | this.emit('command:approval_timeout', { 267 | commandId: id, 268 | message: 'Command approval timed out. If you approved this command in the UI, please use get_pending_commands and approve_command to complete the process.' 269 | }); 270 | } 271 | }, 5000); 272 | }); 273 | } 274 | 275 | queueCommandForApprovalNonBlocking(command, args = [], requestedBy) { 276 | const id = randomUUID(); 277 | const pendingCommand = { 278 | id, 279 | command, 280 | args, 281 | requestedAt: new Date(), 282 | requestedBy, 283 | resolve: () => {}, 284 | reject: () => {} 285 | }; 286 | 287 | this.pendingCommands.set(id, pendingCommand); 288 | this.emit('command:pending', pendingCommand); 289 | 290 | setTimeout(() => { 291 | if (this.pendingCommands.has(id)) { 292 | this.emit('command:approval_timeout', { 293 | commandId: id, 294 | message: 'Command approval timed out. If you approved this command in the UI, please use get_pending_commands and approve_command to complete the process.' 295 | }); 296 | } 297 | }, 5000); 298 | 299 | return id; 300 | } 301 | 302 | async approveCommand(commandId) { 303 | const pendingCommand = this.pendingCommands.get(commandId); 304 | if (!pendingCommand) { 305 | throw new Error(`No pending command with ID: ${commandId}`); 306 | } 307 | 308 | try { 309 | const execFileAsync = promisify(execFile); 310 | const { stdout, stderr } = await execFileAsync( 311 | pendingCommand.command, 312 | pendingCommand.args, 313 | { shell: this.shell } 314 | ); 315 | 316 | this.pendingCommands.delete(commandId); 317 | this.emit('command:approved', { commandId, stdout, stderr }); 318 | pendingCommand.resolve({ stdout, stderr }); 319 | 320 | return { stdout, stderr }; 321 | } catch (error) { 322 | this.pendingCommands.delete(commandId); 323 | this.emit('command:failed', { commandId, error }); 324 | 325 | if (error instanceof Error) { 326 | pendingCommand.reject(error); 327 | throw error; 328 | } 329 | 330 | const genericError = new Error('Command execution failed'); 331 | pendingCommand.reject(genericError); 332 | throw genericError; 333 | } 334 | } 335 | 336 | denyCommand(commandId, reason = 'Command denied') { 337 | const pendingCommand = this.pendingCommands.get(commandId); 338 | if (!pendingCommand) { 339 | throw new Error(`No pending command with ID: ${commandId}`); 340 | } 341 | 342 | this.pendingCommands.delete(commandId); 343 | this.emit('command:denied', { commandId, reason }); 344 | pendingCommand.reject(new Error(reason)); 345 | } 346 | } 347 | 348 | // Export the mocked modules 349 | module.exports = { 350 | CommandService, 351 | CommandSecurityLevel, 352 | detectPlatform, 353 | PlatformType, 354 | getDefaultShell, 355 | validateShellPath, 356 | getShellSuggestions, 357 | getCommonShellLocations, 358 | getShellConfigurationHelp 359 | }; ``` -------------------------------------------------------------------------------- /src/services/command-service.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { execFile } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { randomUUID } from 'crypto'; 4 | import { EventEmitter } from 'events'; 5 | import * as path from 'path'; 6 | import { getDefaultShell, validateShellPath, getShellConfigurationHelp } from '../utils/platform-utils.js'; 7 | import { getPlatformSpecificCommands } from '../utils/command-whitelist-utils.js'; 8 | 9 | const execFileAsync = promisify(execFile); 10 | 11 | /** 12 | * Command security level classification 13 | */ 14 | export enum CommandSecurityLevel { 15 | /** Safe commands that can be executed without approval */ 16 | SAFE = 'safe', 17 | /** Commands that require approval before execution */ 18 | REQUIRES_APPROVAL = 'requires_approval', 19 | /** Commands that are explicitly forbidden */ 20 | FORBIDDEN = 'forbidden' 21 | } 22 | 23 | /** 24 | * Command whitelist entry 25 | */ 26 | export interface CommandWhitelistEntry { 27 | /** The command path or name */ 28 | command: string; 29 | /** Security level of the command */ 30 | securityLevel: CommandSecurityLevel; 31 | /** Allowed arguments (string for exact match, RegExp for pattern match) */ 32 | allowedArgs?: Array<string | RegExp>; 33 | /** Description of the command for documentation */ 34 | description?: string; 35 | } 36 | 37 | /** 38 | * Pending command awaiting approval 39 | */ 40 | export interface PendingCommand { 41 | /** Unique ID for the command */ 42 | id: string; 43 | /** The command to execute */ 44 | command: string; 45 | /** Arguments for the command */ 46 | args: string[]; 47 | /** When the command was requested */ 48 | requestedAt: Date; 49 | /** Who requested the command */ 50 | requestedBy?: string; 51 | /** Resolve function to call when approved */ 52 | resolve: (value: { stdout: string; stderr: string }) => void; 53 | /** Reject function to call when denied */ 54 | reject: (reason: Error) => void; 55 | } 56 | 57 | /** 58 | * Result of command execution 59 | */ 60 | export interface CommandResult { 61 | /** Standard output from the command */ 62 | stdout: string; 63 | /** Standard error from the command */ 64 | stderr: string; 65 | } 66 | 67 | /** 68 | * Service for securely executing shell commands 69 | */ 70 | export class CommandService extends EventEmitter { 71 | /** Shell to use for commands */ 72 | private shell: string; 73 | /** Command whitelist */ 74 | private whitelist: Map<string, CommandWhitelistEntry>; 75 | /** Pending commands awaiting approval */ 76 | private pendingCommands: Map<string, PendingCommand>; 77 | /** Default timeout for command execution in milliseconds */ 78 | private defaultTimeout: number; 79 | 80 | /** 81 | * Create a new CommandService 82 | * @param shell The shell to use for commands (default: auto-detected based on platform) 83 | * @param defaultTimeout Default timeout for command execution in milliseconds (default: 30000) 84 | */ 85 | constructor(shell?: string, defaultTimeout = 30000) { 86 | super(); 87 | this.shell = shell || getDefaultShell(); 88 | this.whitelist = new Map(); 89 | this.pendingCommands = new Map(); 90 | this.defaultTimeout = defaultTimeout; 91 | 92 | // Initialize with platform-specific commands 93 | this.initializeDefaultWhitelist(); 94 | } 95 | 96 | /** 97 | * Get the current shell being used 98 | * @returns The shell path 99 | */ 100 | public getShell(): string { 101 | return this.shell; 102 | } 103 | 104 | /** 105 | * Initialize the default command whitelist based on the current platform 106 | */ 107 | private initializeDefaultWhitelist(): void { 108 | // Get platform-specific commands 109 | const platformCommands = getPlatformSpecificCommands(); 110 | 111 | // Add all commands to the whitelist 112 | platformCommands.forEach(entry => { 113 | this.whitelist.set(entry.command, entry); 114 | }); 115 | } 116 | 117 | /** 118 | * Add a command to the whitelist 119 | * @param entry The command whitelist entry 120 | */ 121 | public addToWhitelist(entry: CommandWhitelistEntry): void { 122 | this.whitelist.set(entry.command, entry); 123 | } 124 | 125 | /** 126 | * Remove a command from the whitelist 127 | * @param command The command to remove 128 | */ 129 | public removeFromWhitelist(command: string): void { 130 | this.whitelist.delete(command); 131 | } 132 | 133 | /** 134 | * Update a command's security level 135 | * @param command The command to update 136 | * @param securityLevel The new security level 137 | */ 138 | public updateSecurityLevel(command: string, securityLevel: CommandSecurityLevel): void { 139 | const entry = this.whitelist.get(command); 140 | if (entry) { 141 | entry.securityLevel = securityLevel; 142 | this.whitelist.set(command, entry); 143 | } 144 | } 145 | 146 | /** 147 | * Get all whitelisted commands 148 | * @returns Array of command whitelist entries 149 | */ 150 | public getWhitelist(): CommandWhitelistEntry[] { 151 | return Array.from(this.whitelist.values()); 152 | } 153 | 154 | /** 155 | * Get all pending commands awaiting approval 156 | * @returns Array of pending commands 157 | */ 158 | public getPendingCommands(): PendingCommand[] { 159 | return Array.from(this.pendingCommands.values()); 160 | } 161 | 162 | /** 163 | * Validate if a command and its arguments are allowed 164 | * @param command The command to validate 165 | * @param args The command arguments 166 | * @returns The security level of the command or null if not whitelisted 167 | */ 168 | private validateCommand(command: string, args: string[]): CommandSecurityLevel | null { 169 | // Extract the base command (without path) using path.basename 170 | const baseCommand = path.basename(command); 171 | 172 | // Check if the command is in the whitelist 173 | const entry = this.whitelist.get(baseCommand); 174 | if (!entry) { 175 | return null; 176 | } 177 | 178 | // If the command is forbidden, return immediately 179 | if (entry.securityLevel === CommandSecurityLevel.FORBIDDEN) { 180 | return CommandSecurityLevel.FORBIDDEN; 181 | } 182 | 183 | // If there are allowed arguments defined, validate them 184 | if (entry.allowedArgs && entry.allowedArgs.length > 0) { 185 | // Check if all arguments are allowed 186 | const allArgsValid = args.every((arg, index) => { 187 | // If we have more args than allowed patterns, reject 188 | if (index >= (entry.allowedArgs?.length || 0)) { 189 | return false; 190 | } 191 | 192 | const pattern = entry.allowedArgs?.[index]; 193 | if (!pattern) { 194 | return false; 195 | } 196 | 197 | // Check if the argument matches the pattern 198 | if (typeof pattern === 'string') { 199 | return arg === pattern; 200 | } else { 201 | return pattern.test(arg); 202 | } 203 | }); 204 | 205 | if (!allArgsValid) { 206 | return CommandSecurityLevel.REQUIRES_APPROVAL; 207 | } 208 | } 209 | 210 | return entry.securityLevel; 211 | } 212 | 213 | /** 214 | * Execute a shell command 215 | * @param command The command to execute 216 | * @param args Command arguments 217 | * @param options Additional options 218 | * @returns Promise resolving to command output 219 | */ 220 | public async executeCommand( 221 | command: string, 222 | args: string[] = [], 223 | options: { 224 | timeout?: number; 225 | requestedBy?: string; 226 | } = {} 227 | ): Promise<CommandResult> { 228 | const securityLevel = this.validateCommand(command, args); 229 | 230 | // If command is not whitelisted, reject 231 | if (securityLevel === null) { 232 | throw new Error(`Command not whitelisted: ${command}`); 233 | } 234 | 235 | // If command is forbidden, reject 236 | if (securityLevel === CommandSecurityLevel.FORBIDDEN) { 237 | throw new Error(`Command is forbidden: ${command}`); 238 | } 239 | 240 | // If command requires approval, add to pending queue 241 | if (securityLevel === CommandSecurityLevel.REQUIRES_APPROVAL) { 242 | return this.queueCommandForApproval(command, args, options.requestedBy); 243 | } 244 | 245 | // For safe commands, execute immediately 246 | try { 247 | const timeout = options.timeout || this.defaultTimeout; 248 | const { stdout, stderr } = await execFileAsync(command, args, { 249 | timeout, 250 | shell: this.shell 251 | }); 252 | 253 | return { stdout, stderr }; 254 | } catch (error) { 255 | if (error instanceof Error) { 256 | throw new Error(`Command execution failed: ${error.message}`); 257 | } 258 | throw error; 259 | } 260 | } 261 | 262 | /** 263 | * Queue a command for approval 264 | * @param command The command to queue 265 | * @param args Command arguments 266 | * @param requestedBy Who requested the command 267 | * @returns Promise resolving when command is approved and executed 268 | */ 269 | private queueCommandForApproval( 270 | command: string, 271 | args: string[] = [], 272 | requestedBy?: string 273 | ): Promise<CommandResult> { 274 | return new Promise((resolve, reject) => { 275 | const id = randomUUID(); 276 | const pendingCommand: PendingCommand = { 277 | id, 278 | command, 279 | args, 280 | requestedAt: new Date(), 281 | requestedBy, 282 | resolve: (result: CommandResult) => resolve(result), 283 | reject: (error: Error) => reject(error) 284 | }; 285 | 286 | this.pendingCommands.set(id, pendingCommand); 287 | 288 | // Emit event for pending command 289 | this.emit('command:pending', pendingCommand); 290 | 291 | // Set a timeout to check if the command is still pending after a while 292 | // This helps detect if the UI approval didn't properly trigger the approveCommand method 293 | setTimeout(() => { 294 | // If the command is still pending after the timeout 295 | if (this.pendingCommands.has(id)) { 296 | // Emit a warning event that can be handled by the client 297 | this.emit('command:approval_timeout', { 298 | commandId: id, 299 | message: 'Command approval timed out. If you approved this command in the UI, please use get_pending_commands and approve_command to complete the process.' 300 | }); 301 | } 302 | }, 5000); // 5 second timeout to detect UI approval issues 303 | }); 304 | } 305 | 306 | /** 307 | * Queue a command for approval without waiting for the Promise to resolve 308 | * @param command The command to queue 309 | * @param args Command arguments 310 | * @param requestedBy Who requested the command 311 | * @returns The ID of the queued command 312 | */ 313 | public queueCommandForApprovalNonBlocking( 314 | command: string, 315 | args: string[] = [], 316 | requestedBy?: string 317 | ): string { 318 | const id = randomUUID(); 319 | const pendingCommand: PendingCommand = { 320 | id, 321 | command, 322 | args, 323 | requestedAt: new Date(), 324 | requestedBy, 325 | resolve: () => {}, // No-op resolve function 326 | reject: () => {} // No-op reject function 327 | }; 328 | 329 | this.pendingCommands.set(id, pendingCommand); 330 | 331 | // Emit event for pending command 332 | this.emit('command:pending', pendingCommand); 333 | 334 | // Set a timeout to check if the command is still pending after a while 335 | setTimeout(() => { 336 | // If the command is still pending after the timeout 337 | if (this.pendingCommands.has(id)) { 338 | // Emit a warning event that can be handled by the client 339 | this.emit('command:approval_timeout', { 340 | commandId: id, 341 | message: 'Command approval timed out. If you approved this command in the UI, please use get_pending_commands and approve_command to complete the process.' 342 | }); 343 | } 344 | }, 5000); // 5 second timeout to detect UI approval issues 345 | 346 | return id; 347 | } 348 | 349 | /** 350 | * Approve a pending command 351 | * @param commandId ID of the command to approve 352 | * @returns Promise resolving to command output 353 | */ 354 | public async approveCommand(commandId: string): Promise<CommandResult> { 355 | const pendingCommand = this.pendingCommands.get(commandId); 356 | if (!pendingCommand) { 357 | throw new Error(`No pending command with ID: ${commandId}`); 358 | } 359 | 360 | try { 361 | const { stdout, stderr } = await execFileAsync( 362 | pendingCommand.command, 363 | pendingCommand.args, 364 | { shell: this.shell } 365 | ); 366 | 367 | // Remove from pending queue 368 | this.pendingCommands.delete(commandId); 369 | 370 | // Emit event for approved command 371 | this.emit('command:approved', { commandId, stdout, stderr }); 372 | 373 | // Resolve the original promise 374 | pendingCommand.resolve({ stdout, stderr }); 375 | 376 | return { stdout, stderr }; 377 | } catch (error) { 378 | // Remove from pending queue 379 | this.pendingCommands.delete(commandId); 380 | 381 | // Emit event for failed command 382 | this.emit('command:failed', { commandId, error }); 383 | 384 | if (error instanceof Error) { 385 | // Reject the original promise 386 | pendingCommand.reject(error); 387 | throw error; 388 | } 389 | 390 | const genericError = new Error('Command execution failed'); 391 | pendingCommand.reject(genericError); 392 | throw genericError; 393 | } 394 | } 395 | 396 | /** 397 | * Deny a pending command 398 | * @param commandId ID of the command to deny 399 | * @param reason Reason for denial 400 | */ 401 | public denyCommand(commandId: string, reason: string = 'Command denied'): void { 402 | const pendingCommand = this.pendingCommands.get(commandId); 403 | if (!pendingCommand) { 404 | throw new Error(`No pending command with ID: ${commandId}`); 405 | } 406 | 407 | // Remove from pending queue 408 | this.pendingCommands.delete(commandId); 409 | 410 | // Emit event for denied command 411 | this.emit('command:denied', { commandId, reason }); 412 | 413 | // Reject the original promise 414 | pendingCommand.reject(new Error(reason)); 415 | } 416 | } 417 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | } from '@modelcontextprotocol/sdk/types.js'; 10 | import { z } from 'zod'; 11 | import * as path from 'path'; 12 | import * as fs from 'fs'; 13 | import { execFile } from 'child_process'; 14 | import { promisify } from 'util'; 15 | import { randomUUID } from 'crypto'; 16 | import { fileURLToPath } from 'url'; 17 | import { dirname } from 'path'; 18 | import { CommandService, CommandSecurityLevel } from './services/command-service.js'; 19 | import { getLogger, Logger } from './utils/logger.js'; 20 | 21 | // In ESM, __dirname is not available directly, so we create it 22 | const __filename = fileURLToPath(import.meta.url); 23 | const __dirname = dirname(__filename); 24 | 25 | const execFileAsync = promisify(execFile); 26 | // Initialize the logger 27 | // Use __dirname to get the directory of the current file 28 | const LOG_FILE = path.join(__dirname, '../logs/super-shell-mcp.log'); 29 | console.error(`Log file path: ${LOG_FILE}`); 30 | const logger = getLogger(LOG_FILE, true); 31 | 32 | /** 33 | * SuperShellMcpServer - MCP server for executing shell commands across multiple platforms 34 | */ 35 | class SuperShellMcpServer { 36 | private server: Server; 37 | private commandService: CommandService; 38 | private pendingApprovals: Map<string, { command: string; args: string[] }>; 39 | 40 | constructor(options?: { shell?: string }) { 41 | // Initialize the command service with auto-detected or specified shell 42 | this.commandService = new CommandService(options?.shell); 43 | this.pendingApprovals = new Map(); 44 | 45 | // Initialize the MCP server 46 | this.server = new Server( 47 | { 48 | name: 'super-shell-mcp', 49 | version: '2.0.13', 50 | }, 51 | { 52 | capabilities: { 53 | tools: {}, 54 | }, 55 | } 56 | ); 57 | 58 | // Set up event handlers for command service 59 | this.setupCommandServiceEvents(); 60 | 61 | // Set up MCP request handlers 62 | this.setupRequestHandlers(); 63 | 64 | // Error handling 65 | this.server.onerror = (error) => { 66 | logger.error(`[MCP Error] ${error}`); 67 | console.error('[MCP Error]', error); 68 | }; 69 | 70 | process.on('SIGINT', async () => { 71 | logger.info('Received SIGINT signal, shutting down'); 72 | await this.server.close(); 73 | logger.info('Server closed, exiting process'); 74 | logger.close(); 75 | process.exit(0); 76 | }); 77 | } 78 | 79 | /** 80 | * Set up event handlers for the command service 81 | */ 82 | private setupCommandServiceEvents(): void { 83 | this.commandService.on('command:pending', (pendingCommand) => { 84 | logger.info(`[Pending Command] ID: ${pendingCommand.id}, Command: ${pendingCommand.command} ${pendingCommand.args.join(' ')}`); 85 | console.error(`[Pending Command] ID: ${pendingCommand.id}, Command: ${pendingCommand.command} ${pendingCommand.args.join(' ')}`); 86 | this.pendingApprovals.set(pendingCommand.id, { 87 | command: pendingCommand.command, 88 | args: pendingCommand.args, 89 | }); 90 | }); 91 | 92 | this.commandService.on('command:approved', (data) => { 93 | logger.info(`[Approved Command] ID: ${data.commandId}`); 94 | console.error(`[Approved Command] ID: ${data.commandId}`); 95 | this.pendingApprovals.delete(data.commandId); 96 | }); 97 | 98 | this.commandService.on('command:denied', (data) => { 99 | logger.info(`[Denied Command] ID: ${data.commandId}, Reason: ${data.reason}`); 100 | console.error(`[Denied Command] ID: ${data.commandId}, Reason: ${data.reason}`); 101 | this.pendingApprovals.delete(data.commandId); 102 | }); 103 | 104 | this.commandService.on('command:failed', (data) => { 105 | logger.error(`[Failed Command] ID: ${data.commandId}, Error: ${data.error.message}`); 106 | console.error(`[Failed Command] ID: ${data.commandId}, Error: ${data.error.message}`); 107 | this.pendingApprovals.delete(data.commandId); 108 | }); 109 | 110 | // Handle approval timeout events 111 | this.commandService.on('command:approval_timeout', (data) => { 112 | logger.error(`[Approval Timeout] ID: ${data.commandId}, Message: ${data.message}`); 113 | console.error(`[Approval Timeout] ID: ${data.commandId}, Message: ${data.message}`); 114 | // Log the timeout but keep the command in the pending queue 115 | // The AI assistant will need to use get_pending_commands and approve_command to proceed 116 | }); 117 | } 118 | 119 | /** 120 | * Set up MCP request handlers 121 | */ 122 | private setupRequestHandlers(): void { 123 | // List available tools 124 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 125 | tools: [ 126 | { 127 | name: 'get_platform_info', 128 | description: 'Get information about the current platform and shell', 129 | inputSchema: { 130 | type: 'object', 131 | properties: {}, 132 | }, 133 | }, 134 | { 135 | name: 'execute_command', 136 | description: 'Execute a shell command on the current platform', 137 | inputSchema: { 138 | type: 'object', 139 | properties: { 140 | command: { 141 | type: 'string', 142 | description: 'The command to execute', 143 | }, 144 | args: { 145 | type: 'array', 146 | items: { 147 | type: 'string', 148 | }, 149 | description: 'Command arguments', 150 | }, 151 | }, 152 | required: ['command'], 153 | }, 154 | }, 155 | { 156 | name: 'get_whitelist', 157 | description: 'Get the list of whitelisted commands', 158 | inputSchema: { 159 | type: 'object', 160 | properties: {}, 161 | }, 162 | }, 163 | { 164 | name: 'add_to_whitelist', 165 | description: 'Add a command to the whitelist', 166 | inputSchema: { 167 | type: 'object', 168 | properties: { 169 | command: { 170 | type: 'string', 171 | description: 'The command to whitelist', 172 | }, 173 | securityLevel: { 174 | type: 'string', 175 | enum: ['safe', 'requires_approval', 'forbidden'], 176 | description: 'Security level for the command', 177 | }, 178 | description: { 179 | type: 'string', 180 | description: 'Description of the command', 181 | }, 182 | }, 183 | required: ['command', 'securityLevel'], 184 | }, 185 | }, 186 | { 187 | name: 'update_security_level', 188 | description: 'Update the security level of a whitelisted command', 189 | inputSchema: { 190 | type: 'object', 191 | properties: { 192 | command: { 193 | type: 'string', 194 | description: 'The command to update', 195 | }, 196 | securityLevel: { 197 | type: 'string', 198 | enum: ['safe', 'requires_approval', 'forbidden'], 199 | description: 'New security level for the command', 200 | }, 201 | }, 202 | required: ['command', 'securityLevel'], 203 | }, 204 | }, 205 | { 206 | name: 'remove_from_whitelist', 207 | description: 'Remove a command from the whitelist', 208 | inputSchema: { 209 | type: 'object', 210 | properties: { 211 | command: { 212 | type: 'string', 213 | description: 'The command to remove from whitelist', 214 | }, 215 | }, 216 | required: ['command'], 217 | }, 218 | }, 219 | { 220 | name: 'get_pending_commands', 221 | description: 'Get the list of commands pending approval', 222 | inputSchema: { 223 | type: 'object', 224 | properties: {}, 225 | }, 226 | }, 227 | { 228 | name: 'approve_command', 229 | description: 'Approve a pending command', 230 | inputSchema: { 231 | type: 'object', 232 | properties: { 233 | commandId: { 234 | type: 'string', 235 | description: 'ID of the command to approve', 236 | }, 237 | }, 238 | required: ['commandId'], 239 | }, 240 | }, 241 | { 242 | name: 'deny_command', 243 | description: 'Deny a pending command', 244 | inputSchema: { 245 | type: 'object', 246 | properties: { 247 | commandId: { 248 | type: 'string', 249 | description: 'ID of the command to deny', 250 | }, 251 | reason: { 252 | type: 'string', 253 | description: 'Reason for denial', 254 | }, 255 | }, 256 | required: ['commandId'], 257 | }, 258 | }, 259 | ], 260 | })); 261 | 262 | // Handle tool calls 263 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 264 | const { name, arguments: args } = request.params; 265 | 266 | try { 267 | switch (name) { 268 | case 'get_platform_info': 269 | return await this.handleGetPlatformInfo(); 270 | case 'execute_command': 271 | return await this.handleExecuteCommand(args); 272 | case 'get_whitelist': 273 | return await this.handleGetWhitelist(); 274 | case 'add_to_whitelist': 275 | return await this.handleAddToWhitelist(args); 276 | case 'update_security_level': 277 | return await this.handleUpdateSecurityLevel(args); 278 | case 'remove_from_whitelist': 279 | return await this.handleRemoveFromWhitelist(args); 280 | case 'get_pending_commands': 281 | return await this.handleGetPendingCommands(); 282 | case 'approve_command': 283 | return await this.handleApproveCommand(args); 284 | case 'deny_command': 285 | return await this.handleDenyCommand(args); 286 | default: 287 | throw new McpError( 288 | ErrorCode.MethodNotFound, 289 | `Unknown tool: ${name}` 290 | ); 291 | } 292 | } catch (error) { 293 | if (error instanceof McpError) { 294 | throw error; 295 | } 296 | 297 | if (error instanceof Error) { 298 | return { 299 | content: [ 300 | { 301 | type: 'text', 302 | text: `Error: ${error.message}`, 303 | }, 304 | ], 305 | isError: true, 306 | }; 307 | } 308 | 309 | throw new McpError( 310 | ErrorCode.InternalError, 311 | 'An unexpected error occurred' 312 | ); 313 | } 314 | }); 315 | } 316 | 317 | /** 318 | * Handle execute_command tool 319 | */ 320 | private async handleExecuteCommand(args: any) { 321 | const schema = z.object({ 322 | command: z.string(), 323 | args: z.array(z.string()).optional(), 324 | }); 325 | 326 | // Log the start of command execution 327 | logger.debug(`handleExecuteCommand called with args: ${JSON.stringify(args)}`); 328 | 329 | const { command, args: commandArgs = [] } = schema.parse(args); 330 | 331 | // Extract the base command (without path) 332 | const baseCommand = path.basename(command); 333 | 334 | logger.debug(`[Executing Command] Command: ${command} ${commandArgs.join(' ')}`); 335 | logger.debug(`Base command: ${baseCommand}`); 336 | 337 | // Check if the command requires approval before attempting execution 338 | const whitelist = this.commandService.getWhitelist(); 339 | logger.debug(`Whitelist entries: ${whitelist.length}`); 340 | 341 | const whitelistEntry = whitelist.find(entry => entry.command === baseCommand); 342 | logger.debug(`Whitelist entry found: ${whitelistEntry ? 'yes' : 'no'}`); 343 | 344 | if (whitelistEntry) { 345 | logger.debug(`Security level: ${whitelistEntry.securityLevel}`); 346 | } 347 | 348 | if (whitelistEntry && whitelistEntry.securityLevel === CommandSecurityLevel.REQUIRES_APPROVAL) { 349 | logger.debug(`[Command Requires Approval] Command: ${command} ${commandArgs.join(' ')}`); 350 | 351 | // Use the non-blocking method to queue the command for approval 352 | const commandId = this.commandService.queueCommandForApprovalNonBlocking(command, commandArgs); 353 | logger.debug(`Command queued for approval with ID: ${commandId}`); 354 | 355 | // Return immediately with instructions for approval 356 | logger.debug(`Returning response to client`); 357 | return { 358 | content: [ 359 | { 360 | type: 'text', 361 | text: `This command requires approval. It has been queued with ID: ${commandId}\n\nPlease approve this command in the UI or use the 'approve_command' function with this command ID.`, 362 | }, 363 | ], 364 | isError: false, // Not an error, just needs approval 365 | }; 366 | } 367 | 368 | // For safe commands or forbidden commands, use the normal execution path 369 | try { 370 | // Use the CommandService's executeCommand method 371 | const result = await this.commandService.executeCommand(command, commandArgs); 372 | 373 | return { 374 | content: [ 375 | { 376 | type: 'text', 377 | text: result.stdout, 378 | }, 379 | { 380 | type: 'text', 381 | text: result.stderr ? `Error output: ${result.stderr}` : '', 382 | }, 383 | ], 384 | }; 385 | } catch (error: unknown) { 386 | const errorMessage = error instanceof Error 387 | ? error.message 388 | : 'Unknown error occurred'; 389 | 390 | console.error(`[Command Execution Failed] Error: ${errorMessage}`); 391 | 392 | return { 393 | content: [ 394 | { 395 | type: 'text', 396 | text: `Command execution failed: ${errorMessage}`, 397 | }, 398 | ], 399 | isError: true, 400 | }; 401 | } 402 | } 403 | 404 | /** 405 | * Handle get_whitelist tool 406 | */ 407 | private async handleGetWhitelist() { 408 | const whitelist = this.commandService.getWhitelist(); 409 | 410 | return { 411 | content: [ 412 | { 413 | type: 'text', 414 | text: JSON.stringify(whitelist, null, 2), 415 | }, 416 | ], 417 | }; 418 | } 419 | 420 | /** 421 | * Handle add_to_whitelist tool 422 | */ 423 | private async handleAddToWhitelist(args: any) { 424 | const schema = z.object({ 425 | command: z.string(), 426 | securityLevel: z.enum(['safe', 'requires_approval', 'forbidden']), 427 | description: z.string().optional(), 428 | }); 429 | 430 | const { command, securityLevel, description } = schema.parse(args); 431 | 432 | // Map string security level to enum 433 | const securityLevelEnum = securityLevel === 'safe' 434 | ? CommandSecurityLevel.SAFE 435 | : securityLevel === 'requires_approval' 436 | ? CommandSecurityLevel.REQUIRES_APPROVAL 437 | : CommandSecurityLevel.FORBIDDEN; 438 | 439 | this.commandService.addToWhitelist({ 440 | command, 441 | securityLevel: securityLevelEnum, 442 | description, 443 | }); 444 | 445 | return { 446 | content: [ 447 | { 448 | type: 'text', 449 | text: `Command '${command}' added to whitelist with security level '${securityLevel}'`, 450 | }, 451 | ], 452 | }; 453 | } 454 | 455 | /** 456 | * Handle update_security_level tool 457 | */ 458 | private async handleUpdateSecurityLevel(args: any) { 459 | const schema = z.object({ 460 | command: z.string(), 461 | securityLevel: z.enum(['safe', 'requires_approval', 'forbidden']), 462 | }); 463 | 464 | const { command, securityLevel } = schema.parse(args); 465 | 466 | // Map string security level to enum 467 | const securityLevelEnum = securityLevel === 'safe' 468 | ? CommandSecurityLevel.SAFE 469 | : securityLevel === 'requires_approval' 470 | ? CommandSecurityLevel.REQUIRES_APPROVAL 471 | : CommandSecurityLevel.FORBIDDEN; 472 | 473 | this.commandService.updateSecurityLevel(command, securityLevelEnum); 474 | 475 | return { 476 | content: [ 477 | { 478 | type: 'text', 479 | text: `Security level for command '${command}' updated to '${securityLevel}'`, 480 | }, 481 | ], 482 | }; 483 | } 484 | 485 | /** 486 | * Handle remove_from_whitelist tool 487 | */ 488 | private async handleRemoveFromWhitelist(args: any) { 489 | const schema = z.object({ 490 | command: z.string(), 491 | }); 492 | 493 | const { command } = schema.parse(args); 494 | 495 | this.commandService.removeFromWhitelist(command); 496 | 497 | return { 498 | content: [ 499 | { 500 | type: 'text', 501 | text: `Command '${command}' removed from whitelist`, 502 | }, 503 | ], 504 | }; 505 | } 506 | 507 | /** 508 | * Handle get_platform_info tool 509 | */ 510 | private async handleGetPlatformInfo() { 511 | const { detectPlatform, getDefaultShell, getShellSuggestions, getCommonShellLocations } = await import('./utils/platform-utils.js'); 512 | 513 | const platform = detectPlatform(); 514 | const currentShell = this.commandService.getShell(); 515 | const suggestedShells = getShellSuggestions()[platform]; 516 | const commonLocations = getCommonShellLocations(); 517 | 518 | return { 519 | content: [ 520 | { 521 | type: 'text', 522 | text: JSON.stringify({ 523 | platform, 524 | currentShell, 525 | suggestedShells, 526 | commonLocations, 527 | helpMessage: `Super Shell MCP is running on ${platform} using ${currentShell}` 528 | }, null, 2), 529 | }, 530 | ], 531 | }; 532 | } 533 | 534 | /** 535 | * Handle get_pending_commands tool 536 | */ 537 | private async handleGetPendingCommands() { 538 | const pendingCommands = this.commandService.getPendingCommands(); 539 | 540 | return { 541 | content: [ 542 | { 543 | type: 'text', 544 | text: JSON.stringify(pendingCommands.map(cmd => ({ 545 | id: cmd.id, 546 | command: cmd.command, 547 | args: cmd.args, 548 | requestedAt: cmd.requestedAt, 549 | requestedBy: cmd.requestedBy, 550 | })), null, 2), 551 | }, 552 | ], 553 | }; 554 | } 555 | 556 | /** 557 | * Handle approve_command tool 558 | */ 559 | private async handleApproveCommand(args: any) { 560 | const schema = z.object({ 561 | commandId: z.string(), 562 | }); 563 | 564 | logger.debug(`handleApproveCommand called with args: ${JSON.stringify(args)}`); 565 | 566 | const { commandId } = schema.parse(args); 567 | 568 | // Log the approval attempt 569 | logger.debug(`[Approval Attempt] ID: ${commandId}`); 570 | 571 | // Check if the command exists in our local pending approvals map 572 | const localPending = this.pendingApprovals.has(commandId); 573 | logger.debug(`Command found in local pendingApprovals: ${localPending ? 'yes' : 'no'}`); 574 | 575 | // Check if the command exists in the CommandService's pending queue 576 | const pendingCommands = this.commandService.getPendingCommands(); 577 | logger.debug(`CommandService pending commands: ${pendingCommands.length}`); 578 | const pendingCommand = pendingCommands.find(cmd => cmd.id === commandId); 579 | logger.debug(`Command found in CommandService pending queue: ${pendingCommand ? 'yes' : 'no'}`); 580 | 581 | if (pendingCommand) { 582 | logger.debug(`Pending command details: ${JSON.stringify({ 583 | id: pendingCommand.id, 584 | command: pendingCommand.command, 585 | args: pendingCommand.args, 586 | requestedAt: pendingCommand.requestedAt 587 | })}`); 588 | } 589 | 590 | try { 591 | logger.debug(`Calling CommandService.approveCommand with ID: ${commandId}`); 592 | // Use the CommandService's approveCommand method directly 593 | const result = await this.commandService.approveCommand(commandId); 594 | 595 | logger.debug(`[Command Approved] ID: ${commandId}, Output length: ${result.stdout.length}`); 596 | logger.debug(`Command output: ${result.stdout.substring(0, 100)}${result.stdout.length > 100 ? '...' : ''}`); 597 | 598 | return { 599 | content: [ 600 | { 601 | type: 'text', 602 | text: `Command approved and executed successfully.\nOutput: ${result.stdout}`, 603 | }, 604 | { 605 | type: 'text', 606 | text: result.stderr ? `Error output: ${result.stderr}` : '', 607 | }, 608 | ], 609 | }; 610 | } catch (error) { 611 | logger.error(`[Approval Error] ID: ${commandId}, Error: ${error instanceof Error ? error.message : 'Unknown error'}`); 612 | 613 | if (error instanceof Error) { 614 | return { 615 | content: [ 616 | { 617 | type: 'text', 618 | text: `Command approval failed: ${error.message}`, 619 | }, 620 | ], 621 | isError: true, 622 | }; 623 | } 624 | throw error; 625 | } 626 | } 627 | 628 | /** 629 | * Handle deny_command tool 630 | */ 631 | private async handleDenyCommand(args: any) { 632 | const schema = z.object({ 633 | commandId: z.string(), 634 | reason: z.string().optional(), 635 | }); 636 | 637 | logger.debug(`handleDenyCommand called with args: ${JSON.stringify(args)}`); 638 | 639 | const { commandId, reason } = schema.parse(args); 640 | 641 | logger.debug(`[Denial Attempt] ID: ${commandId}, Reason: ${reason || 'none provided'}`); 642 | 643 | try { 644 | this.commandService.denyCommand(commandId, reason); 645 | logger.info(`Command denied: ID: ${commandId}, Reason: ${reason || 'none provided'}`); 646 | 647 | return { 648 | content: [ 649 | { 650 | type: 'text', 651 | text: `Command denied${reason ? `: ${reason}` : ''}`, 652 | }, 653 | ], 654 | }; 655 | } catch (error) { 656 | logger.error(`[Denial Error] ID: ${commandId}, Error: ${error instanceof Error ? error.message : 'Unknown error'}`); 657 | 658 | if (error instanceof Error) { 659 | return { 660 | content: [ 661 | { 662 | type: 'text', 663 | text: `Command denial failed: ${error.message}`, 664 | }, 665 | ], 666 | isError: true, 667 | }; 668 | } 669 | throw error; 670 | } 671 | } 672 | 673 | /** 674 | * Run the MCP server 675 | */ 676 | async run() { 677 | logger.info('Starting Super Shell MCP server'); 678 | const transport = new StdioServerTransport(); 679 | await this.server.connect(transport); 680 | logger.info('Super Shell MCP server running on stdio'); 681 | console.error('Super Shell MCP server running on stdio'); 682 | console.error(`Log file: ${LOG_FILE}`); 683 | logger.info(`Log file: ${LOG_FILE}`); 684 | } 685 | } 686 | 687 | // Create and run the server 688 | const server = new SuperShellMcpServer(); 689 | server.run().catch(console.error); ```