# 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: -------------------------------------------------------------------------------- ``` # This file ensures the logs directory is tracked by Git # Log files themselves are ignored via .gitignore ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* yarn.lock # Build output build/ dist/ *.tsbuildinfo # Environment variables .env .env.local .env.development.local .env.test.local .env.production.local # IDE and editor files .idea/ .vscode/ *.swp *.swo .DS_Store # Logs logs/*.log ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown [](https://mseep.ai/app/cfdude-super-shell-mcp) # Super Shell MCP Server [](https://smithery.ai/package/@cfdude/super-shell-mcp) 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. > 🎉 **Now available as a Claude Desktop Extension!** Install with one click using the `.dxt` package - no developer tools or configuration required. ## Features - Execute shell commands through MCP on Windows, macOS, and Linux - Automatic platform detection and shell selection - Support for multiple shells: - **Windows**: cmd.exe, PowerShell - **macOS**: zsh, bash, sh - **Linux**: bash, sh, zsh - Command whitelisting with security levels: - **Safe**: Commands that can be executed without approval - **Requires Approval**: Commands that need explicit approval before execution - **Forbidden**: Commands that are explicitly blocked - Platform-specific command whitelists - Non-blocking approval workflow for potentially dangerous commands - Comprehensive logging system with file-based logs - Comprehensive command management tools - Platform information tool for diagnostics ## Installation ### Option 1: Claude Desktop Extension (.dxt) - Recommended **One-Click Installation for Claude Desktop:** 1. **Download** the `super-shell-mcp.dxt` file from the [latest release](https://github.com/cfdude/super-shell-mcp/releases) 2. **Quick Install**: Double-click the `.dxt` file while Claude Desktop is open **OR** **Manual Install**: - Open Claude Desktop - Go to **Settings** > **Extensions** - Click **"Add Extension"** - Select the downloaded `super-shell-mcp.dxt` file 3. **Configure** (optional): Set custom shell path if needed 4. **Start using** - The extension is ready to use immediately! ✅ **Benefits of DXT Installation:** - No developer tools required (Node.js, Python, etc.) - No manual configuration files - Automatic dependency management - One-click installation and updates - Secure credential storage in OS keychain ### Option 2: Installing via Smithery To install Super Shell MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/package/@cfdude/super-shell-mcp): ```bash npx -y @smithery/cli install @cfdude/super-shell-mcp --client claude ``` ### Option 3: Installing Manually ```bash # Clone the repository git clone https://github.com/cfdude/super-shell-mcp.git cd super-shell-mcp # Install dependencies npm install # Build the project npm run build ``` ## Usage ### For Claude Desktop Extension Users (.dxt) If you installed using the `.dxt` extension (Option 1), **you're ready to go!** No additional configuration needed. The extension handles everything automatically: - ✅ **Automatic startup** when Claude Desktop launches - ✅ **Platform detection** and appropriate shell selection - ✅ **Built-in security** with command whitelisting and approval workflows - ✅ **Optional configuration** via Claude Desktop's extension settings ### For Manual Installation Users If you installed manually (Option 2 or 3), you'll need to configure Claude Desktop or your MCP client: #### Starting the Server Manually ```bash npm start ``` Or directly: ```bash node build/index.js ``` #### Manual Configuration for MCP Clients For manual installations, both Roo Code and Claude Desktop use a similar configuration format for MCP servers: ##### Using NPX (Recommended for Manual Setup) 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). ##### Roo Code Configuration with NPX ```json "super-shell": { "command": "npx", "args": [ "-y", "super-shell-mcp" ], "alwaysAllow": [], "disabled": false } ``` ##### Claude Desktop Configuration with NPX ```json "super-shell": { "command": "npx", "args": [ "-y", "super-shell-mcp" ], "alwaysAllow": false, "disabled": false } ``` #### Option 2: Using Local Installation 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`): ```json "super-shell": { "command": "node", "args": [ "/path/to/super-shell-mcp/build/index.js" ], "alwaysAllow": [], "disabled": false } ``` You can optionally specify a custom shell by adding a shell parameter: ```json "super-shell": { "command": "node", "args": [ "/path/to/super-shell-mcp/build/index.js", "--shell=/usr/bin/bash" ], "alwaysAllow": [], "disabled": false } ``` Windows 11 example ```json "super-shell": { "command": "C:\\Program Files\\nodejs\\node.exe", "args": [ "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js", "-y", "super-shell-mcp", "C:\\Users\\username" ], "alwaysAllow": [], "disabled": false } ``` #### Claude Desktop Configuration Add the following to your Claude Desktop configuration file (located at `~/Library/Application Support/Claude/claude_desktop_config.json`): ```json "super-shell": { "command": "node", "args": [ "/path/to/super-shell-mcp/build/index.js" ], "alwaysAllow": false, "disabled": false } ``` For Windows users, the configuration file is typically located at `%APPDATA%\Claude\claude_desktop_config.json`. ### Platform-Specific Configuration #### Windows - Default shell: cmd.exe (or PowerShell if available) - Configuration paths: - Roo Code: `%APPDATA%\Code\User\globalStorage\rooveterinaryinc.roo-cline\settings\cline_mcp_settings.json` - Claude Desktop: `%APPDATA%\Claude\claude_desktop_config.json` - Shell path examples: - cmd.exe: `C:\\Windows\\System32\\cmd.exe` - PowerShell: `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` - PowerShell Core: `C:\\Program Files\\PowerShell\\7\\pwsh.exe` #### macOS - Default shell: /bin/zsh - Configuration paths: - Roo Code: `~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` - Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` - Shell path examples: - zsh: `/bin/zsh` - bash: `/bin/bash` - sh: `/bin/sh` #### Linux - Default shell: /bin/bash (or $SHELL environment variable) - Configuration paths: - Roo Code: `~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` - Claude Desktop: `~/.config/Claude/claude_desktop_config.json` - Shell path examples: - bash: `/bin/bash` - sh: `/bin/sh` - zsh: `/usr/bin/zsh` You can optionally specify a custom shell: ```json "super-shell": { "command": "node", "args": [ "/path/to/super-shell-mcp/build/index.js", "--shell=C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" ], "alwaysAllow": false, "disabled": false } ``` Replace `/path/to/super-shell-mcp` with the actual path where you cloned the repository. > **Note**: > - 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"]`. > - 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. > > **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. ### Available Tools The server exposes the following MCP tools: #### `get_platform_info` Get information about the current platform and shell. ```json {} ``` #### `execute_command` Execute a shell command on the current platform. ```json { "command": "ls", "args": ["-la"] } ``` #### `get_whitelist` Get the list of whitelisted commands. ```json {} ``` #### `add_to_whitelist` Add a command to the whitelist. ```json { "command": "python3", "securityLevel": "safe", "description": "Run Python 3 scripts" } ``` #### `update_security_level` Update the security level of a whitelisted command. ```json { "command": "python3", "securityLevel": "requires_approval" } ``` #### `remove_from_whitelist` Remove a command from the whitelist. ```json { "command": "python3" } ``` #### `get_pending_commands` Get the list of commands pending approval. ```json {} ``` #### `approve_command` Approve a pending command. ```json { "commandId": "command-uuid-here" } ``` #### `deny_command` Deny a pending command. ```json { "commandId": "command-uuid-here", "reason": "This command is potentially dangerous" } ``` ## Default Whitelisted Commands The server includes platform-specific command whitelists that are automatically selected based on the detected platform. ### Common Safe Commands (All Platforms) - `echo` - Print text to standard output ### Unix-like Safe Commands (macOS/Linux) - `ls` - List directory contents - `pwd` - Print working directory - `echo` - Print text to standard output - `cat` - Concatenate and print files - `grep` - Search for patterns in files - `find` - Find files in a directory hierarchy - `cd` - Change directory - `head` - Output the first part of files - `tail` - Output the last part of files - `wc` - Print newline, word, and byte counts ### Windows-specific Safe Commands - `dir` - List directory contents - `type` - Display the contents of a text file - `findstr` - Search for strings in files - `where` - Locate programs - `whoami` - Display current user - `hostname` - Display computer name - `ver` - Display operating system version ### Commands Requiring Approval #### Windows Commands Requiring Approval - `copy` - Copy files - `move` - Move files - `mkdir` - Create directories - `rmdir` - Remove directories - `rename` - Rename files - `attrib` - Change file attributes #### Unix Commands Requiring Approval - `mv` - Move (rename) files - `cp` - Copy files and directories - `mkdir` - Create directories - `touch` - Change file timestamps or create empty files - `chmod` - Change file mode bits - `chown` - Change file owner and group ### Forbidden Commands #### Windows Forbidden Commands - `del` - Delete files - `erase` - Delete files - `format` - Format a disk - `runas` - Execute a program as another user #### Unix Forbidden Commands - `rm` - Remove files or directories - `sudo` - Execute a command as another user ## Security Considerations - All commands are executed with the permissions of the user running the MCP server - Commands requiring approval are held in a queue until explicitly approved - Forbidden commands are never executed - The server uses Node.js's `execFile` instead of `exec` to prevent shell injection - Arguments are validated against allowed patterns when specified ## Extending the Whitelist You can extend the whitelist by using the `add_to_whitelist` tool. For example: ```json { "command": "npm", "securityLevel": "requires_approval", "description": "Node.js package manager" } ``` ## NPM Package Information 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). ### Benefits of Using NPX Using the NPX method (as shown in Option 1 of the Configuration section) offers several advantages: 1. **No Manual Setup**: No need to clone the repository, install dependencies, or build the project 2. **Automatic Updates**: Always uses the latest published version 3. **Cross-Platform Compatibility**: Works the same way on Windows, macOS, and Linux 4. **Simplified Configuration**: Shorter configuration with no absolute paths 5. **Reduced Maintenance**: No local files to manage or update ### Using from GitHub If you prefer to use the latest development version directly from GitHub: ```json "super-shell": { "command": "npx", "args": [ "-y", "github:cfdude/super-shell-mcp" ], "alwaysAllow": [], // For Roo Code "disabled": false } ``` ### Publishing Your Own Version If you want to publish your own modified version to npm: 1. Update the package.json with your details 2. Ensure the "bin" field is properly configured: ```json "bin": { "super-shell-mcp": "./build/index.js" } ``` 3. Publish to npm: ```bash npm publish ``` ## NPX Best Practices For optimal integration with MCP clients using NPX, this project follows these best practices: 1. **Executable Entry Point**: The main file includes a shebang line (`#!/usr/bin/env node`) and is made executable during build. 2. **Package Configuration**: - `"type": "module"` - Ensures ES Modules are used - `"bin"` field - Maps the command name to the entry point - `"files"` field - Specifies which files to include when publishing - `"prepare"` script - Ensures compilation happens on install 3. **TypeScript Configuration**: - `"module": "NodeNext"` - Proper ES Modules support - `"moduleResolution": "NodeNext"` - Consistent with ES Modules 4. **Automatic Installation and Execution**: - The MCP client configuration uses `npx -y` to automatically install and run the package - No terminal window is tied up as the process runs in the background 5. **Publishing Process**: ```bash # Update version in package.json npm version patch # or minor/major as appropriate # Build and publish npm publish ``` 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. ## Troubleshooting ### Cross-Platform Issues #### Windows-Specific Issues 1. **PowerShell Script Execution Policy** - **Issue**: PowerShell may block script execution with the error "Execution of scripts is disabled on this system" - **Solution**: Run PowerShell as Administrator and execute `Set-ExecutionPolicy RemoteSigned` or use the `-ExecutionPolicy Bypass` parameter when configuring the shell 2. **Path Separators** - **Issue**: Windows uses backslashes (`\`) in paths, which need to be escaped in JSON - **Solution**: Use double backslashes (`\\`) in JSON configuration files, e.g., `C:\\Windows\\System32\\cmd.exe` 3. **Command Not Found** - **Issue**: Windows doesn't have Unix commands like `ls`, `grep`, etc. - **Solution**: Use Windows equivalents (`dir` instead of `ls`, `findstr` instead of `grep`) #### macOS/Linux-Specific Issues 1. **Shell Permissions** - **Issue**: Permission denied when executing commands - **Solution**: Ensure the shell has appropriate permissions with `chmod +x /path/to/shell` 2. **Environment Variables** - **Issue**: Environment variables not available in MCP server - **Solution**: Set environment variables in the shell's profile file (`.zshrc`, `.bashrc`, etc.) ### General Troubleshooting 1. **Shell Detection Issues** - **Issue**: Server fails to detect the correct shell - **Solution**: Explicitly specify the shell path in the configuration 2. **Command Execution Timeout** - **Issue**: Commands taking too long and timing out - **Solution**: Increase the timeout value in the command service constructor ### Logging System The server includes a comprehensive logging system that writes logs to a file for easier debugging and monitoring: 1. **Log File Location** - Default: `logs/super-shell-mcp.log` in the server's directory - The logs directory is created automatically and tracked by Git (with a .gitkeep file) - Log files themselves are excluded from Git via .gitignore - Contains detailed information about server operations, command execution, and approval workflow 2. **Log Levels** - **INFO**: General operational information - **DEBUG**: Detailed debugging information - **ERROR**: Error conditions and exceptions 3. **Viewing Logs** - Use standard file viewing commands to check logs: ```bash # View the entire log cat logs/super-shell-mcp.log # Follow log updates in real-time tail -f logs/super-shell-mcp.log ``` 4. **Log Content** - Server startup and configuration - Command execution requests and results - Approval workflow events (pending, approved, denied) - Error conditions and troubleshooting information 3. **Whitelist Management** - **Issue**: Need to add custom commands to whitelist - **Solution**: Use the `add_to_whitelist` tool to add commands specific to your environment ## License 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. ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown # Contributing to This Project 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. ## 📦 Getting Started 1. **Fork the Repository** Click the **Fork** button at the top of the repository page to create your own copy of the project. 2. **Clone Your Fork** ```bash git clone https://github.com/your-username/your-fork.git cd your-fork ``` 3. Create a Feature Branch Create a new branch based on main for your changes: ```bash git checkout -b feature/your-feature-name ``` 🚀 Submitting a Pull Request Once you’re ready to share your changes: 1. Ensure Code Quality • Run all linting checks. • Use Prettier to format your code. • Confirm all tests pass (if applicable). 2. Document Your Work • Add meaningful comments to your code. • Update or add documentation where necessary (e.g., README, inline comments, or docs folder). 3. Submit Your PR • Push your branch to your fork: ```bash git push origin feature/your-feature-name ``` * Open a Pull Request against the main branch of the original repository. * Include a clear summary of your changes and why they are beneficial. ✅ Code Standards * Follow the existing code style and formatting conventions. * All code must pass linting and Prettier formatting before submission. * Keep your changes focused and limited to the scope of your feature or fix. ❤️ Be Kind * Be respectful and constructive in code reviews and discussions. * Ask questions if you’re unsure—collaboration is key. We appreciate your help in improving the project for everyone. Thank you for contributing! ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ``` -------------------------------------------------------------------------------- /mcp-settings-example.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "mac-shell": { "command": "node", "args": ["/path/to/mac-shell-mcp/build/index.js"], "disabled": false, "alwaysAllow": [] } } } ``` -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Build the project if needed if [ ! -d "./build" ] || [ ! -f "./build/index.js" ]; then echo "Building project..." npm run build fi # Run the MCP server echo "Starting Mac Shell MCP Server..." node build/index.js ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: push: pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm test ``` -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- ```json { "permissions": { "allow": [ "Bash(gh pr create:*)", "Bash(gh browse:*)", "Bash(docker build:*)", "Bash(docker run:*)", "mcp__super-shell__execute_command", "mcp__git__git_status", "mcp__git__git_add", "mcp__git__git_commit" ], "deny": [] } } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "types": ["node"], "esModuleInterop": true, "strict": true, "outDir": "./build", "rootDir": "./src", "declaration": true, "sourceMap": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "build"] } ``` -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- ``` module.exports = { transform: {}, testEnvironment: 'node', testMatch: ['**/tests/**/*.test.js'], verbose: true, testRunner: 'jest-circus/runner', transformIgnorePatterns: [ 'node_modules/(?!(.*\\.mjs$))' ], setupFiles: ['./jest.setup.cjs'], moduleNameMapper: { '^../build/services/command-service.js$': '<rootDir>/jest.setup.cjs', '^../build/utils/platform-utils.js$': '<rootDir>/jest.setup.cjs' } }; ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object properties: {} default: {} commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'npm', args: ['start'] }) exampleConfig: {} ``` -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- ```yaml name: Checks on: push: branches: [ main ] pull_request: branches: [ main ] jobs: lint-and-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: npm ci - name: Run Linter run: npm run lint - name: Build Verification run: npm run build ``` -------------------------------------------------------------------------------- /scripts/make-executable.js: -------------------------------------------------------------------------------- ```javascript import fs from 'fs'; import { platform } from 'os'; const isWindows = platform() === 'win32'; const indexPath = './build/index.js'; if (isWindows) { console.log('Windows detected, skipping chmod operation'); } else { try { // On Unix-like systems, make the file executable fs.chmodSync(indexPath, '755'); console.log(`Made ${indexPath} executable`); } catch (error) { console.error(`Error making ${indexPath} executable:`, error); process.exit(1); } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- ```yaml # These are supported funding model platforms github: [cfdude] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: cfdude thanks_dev: # Replace with a single thanks.dev username custom: ['paypal.me/cfdude', 'https://account.venmo.com/u/cfdude', 'https://cash.app/$cfdude'] ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Use Node.js 18 Alpine for smaller image size FROM node:18-alpine # Set working directory WORKDIR /app # Create logs directory RUN mkdir -p logs # Copy package files first for better caching COPY package*.json ./ # Copy TypeScript configuration and source code COPY tsconfig.json ./ COPY src/ ./src/ # Install all dependencies (including dev dependencies for build) RUN npm ci # Build the project (compiles TypeScript and sets executable permissions) RUN npm run build # Remove dev dependencies to reduce image size RUN npm prune --omit=dev && npm cache clean --force # Ensure the built file is executable RUN chmod +x build/index.js # Expose stdio for MCP communication # Note: MCP servers typically communicate via stdio, not network ports # Set the entrypoint to the built application ENTRYPOINT ["node", "build/index.js"] ``` -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- ```yaml name: "CodeQL Analysis" on: push: branches: [ main ] pull_request: branches: [ main ] schedule: - cron: '0 0 * * 0' # Run once a week at midnight on Sunday jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'javascript' ] # Add other languages as needed: python, java, go, cpp, etc. steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # Autobuild attempts to build any compiled languages - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ``` -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- ```yaml # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages name: Publish Package to npmjs on: release: types: [created] jobs: build: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm test publish-npm: needs: build runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm publish --provenance env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "super-shell-mcp", "version": "2.0.13", "description": "MCP server for executing shell commands across multiple platforms", "type": "module", "main": "build/index.js", "bin": { "super-shell-mcp": "./build/index.js" }, "repository": { "type": "git", "url": "https://github.com/cfdude/super-shell-mcp.git" }, "files": [ "build" ], "scripts": { "build": "tsc && chmod +x build/index.js", "prepare": "npm run build", "start": "node build/index.js", "dev": "ts-node --esm src/index.ts", "lint": "eslint .", "test": "jest --config=jest.config.cjs", "test:quiet": "jest --config=jest.config.cjs --silent" }, "keywords": [ "mcp", "shell", "macos", "windows", "linux", "zsh", "bash", "powershell", "cmd", "terminal", "cross-platform" ], "author": "", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "zod": "^3.22.4" }, "devDependencies": { "@eslint/js": "^9.30.1", "@types/jest": "^29.5.11", "@types/node": "^20.17.24", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^9.30.1", "jest": "^29.7.0", "jest-circus": "^29.7.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json { "dxt_version": "0.1", "name": "super-shell-mcp", "version": "2.0.13", "description": "Execute shell commands across multiple platforms with built-in security controls", "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.", "author": { "name": "cfdude", "url": "https://github.com/cfdude/super-shell-mcp" }, "repository": { "type": "git", "url": "https://github.com/cfdude/super-shell-mcp" }, "license": "MIT", "keywords": ["shell", "commands", "security", "cross-platform", "terminal"], "server": { "type": "node", "entry_point": "server/index.js", "mcp_config": { "command": "node", "args": ["${__dirname}/server/index.js"], "env": { "CUSTOM_SHELL": "${user_config.shell_path}" } } }, "tools": [ { "name": "execute_command", "description": "Execute shell commands with security controls" }, { "name": "get_whitelist", "description": "Get list of whitelisted commands" }, { "name": "add_to_whitelist", "description": "Add commands to the security whitelist" }, { "name": "get_platform_info", "description": "Get current platform and shell information" } ], "user_config": { "shell_path": { "type": "string", "title": "Custom Shell Path", "description": "Optional: Specify a custom shell path (e.g., /bin/zsh, C:\\Windows\\System32\\cmd.exe)", "required": false } } } ``` -------------------------------------------------------------------------------- /docs/adr/001-command-security-levels.md: -------------------------------------------------------------------------------- ```markdown # ADR 001: Command Security Levels ## Status Accepted ## Context 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: 1. Some commands are relatively safe (e.g., `ls`, `pwd`, `echo`) 2. Some commands can modify the system but in limited ways (e.g., `mkdir`, `cp`, `mv`) 3. Some commands can cause significant damage (e.g., `rm -rf`, `sudo`) We need a mechanism to categorize commands based on their potential risk and handle them accordingly. ## Decision We will implement a three-tier security level system for commands: 1. **Safe Commands**: These commands can be executed immediately without approval. They are read-only or have minimal impact on the system. 2. **Commands Requiring Approval**: These commands can modify the system but are not inherently dangerous. They will be queued for explicit approval before execution. 3. **Forbidden Commands**: These commands are considered too dangerous and will be rejected outright. Each command will be categorized in a whitelist, and the security level will determine how the command is handled when execution is requested. ## Consequences ### Positive - Provides a clear security model for command execution - Allows safe commands to be executed without friction - Creates an approval workflow for potentially dangerous commands - Completely blocks high-risk commands - Makes the security policy explicit and configurable ### Negative - Requires maintaining a whitelist of commands - May introduce friction for legitimate use cases of commands requiring approval - Initial categorization may not be perfect and could require adjustment ## Implementation The security levels will be implemented as an enum in the `CommandService` class: ```typescript export enum CommandSecurityLevel { SAFE = 'safe', REQUIRES_APPROVAL = 'requires_approval', FORBIDDEN = 'forbidden' } ``` Commands will be stored in a whitelist with their security level: ```typescript export interface CommandWhitelistEntry { command: string; securityLevel: CommandSecurityLevel; allowedArgs?: Array<string | RegExp>; description?: string; } ``` When a command is executed, its security level will determine the behavior: - Safe commands are executed immediately - Commands requiring approval are queued for explicit approval - Forbidden commands are rejected with an error ``` -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript import * as fs from 'fs'; import * as path from 'path'; /** * Simple logging utility that writes to a file */ export class Logger { private logFile: string; private enabled: boolean; private fileStream: fs.WriteStream | null = null; /** * Create a new logger * @param logFile Path to the log file * @param enabled Whether logging is enabled */ constructor(logFile: string, enabled = true) { this.logFile = logFile; this.enabled = enabled; if (this.enabled) { // Create the directory if it doesn't exist const logDir = path.dirname(this.logFile); console.error(`Creating log directory: ${logDir}`); try { if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } } catch (error) { console.error(`Error creating log directory: ${error}`); // Fall back to a directory we know exists this.logFile = './super-shell-mcp.log'; console.error(`Falling back to log file: ${this.logFile}`); } // Create or truncate the log file this.fileStream = fs.createWriteStream(this.logFile, { flags: 'w' }); // Write a header to the log file this.log('INFO', `Logging started at ${new Date().toISOString()}`); } } /** * Log a message * @param level Log level (INFO, DEBUG, ERROR, etc.) * @param message Message to log */ public log(level: string, message: string): void { if (!this.enabled || !this.fileStream) { return; } const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] [${level}] ${message}\n`; this.fileStream.write(logMessage); } /** * Log an info message * @param message Message to log */ public info(message: string): void { this.log('INFO', message); } /** * Log a debug message * @param message Message to log */ public debug(message: string): void { this.log('DEBUG', message); } /** * Log an error message * @param message Message to log */ public error(message: string): void { this.log('ERROR', message); } /** * Close the logger */ public close(): void { if (this.fileStream) { this.fileStream.end(); this.fileStream = null; } } } // Create a singleton logger instance let loggerInstance: Logger | null = null; /** * Get the logger instance * @param logFile Path to the log file * @param enabled Whether logging is enabled * @returns Logger instance */ export function getLogger(logFile?: string, enabled?: boolean): Logger { if (!loggerInstance && logFile) { loggerInstance = new Logger(logFile, enabled); } if (!loggerInstance) { throw new Error('Logger not initialized'); } return loggerInstance; } ``` -------------------------------------------------------------------------------- /docs/adr/002-mcp-for-shell-commands.md: -------------------------------------------------------------------------------- ```markdown # ADR 002: Using MCP for Shell Command Execution ## Status Accepted ## Context There are several ways to provide shell command execution capabilities to AI assistants: 1. Custom API endpoints 2. Direct integration with specific AI platforms 3. Standardized protocols like MCP (Model Context Protocol) 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. ## Decision We will implement shell command execution as an MCP server for the following reasons: 1. **Standardization**: MCP is an emerging standard for AI tool integration, supported by major AI platforms like Anthropic's Claude. 2. **Discoverability**: MCP provides built-in tool discovery, allowing AI assistants to automatically learn about available commands and their parameters. 3. **Security**: MCP's structured approach allows for clear security boundaries and validation of inputs. 4. **Flexibility**: MCP servers can be used with any MCP-compatible client, not just specific AI platforms. 5. **Future-proofing**: As more AI platforms adopt MCP, our implementation will be compatible without changes. ## Consequences ### Positive - Works with any MCP-compatible client (Claude Desktop, etc.) - Provides structured tool definitions with clear parameter schemas - Enables dynamic tool discovery - Follows an emerging industry standard - Separates concerns between command execution and AI integration ### Negative - MCP is still an evolving standard - Requires understanding of MCP concepts and implementation details - May have more overhead than a direct, custom integration ## Implementation We will implement the MCP server using the official TypeScript SDK: ```typescript import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; ``` The server will expose tools for command execution and whitelist management: ```typescript this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'execute_command', description: 'Execute a shell command on macOS', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to execute', }, args: { type: 'array', items: { type: 'string', }, description: 'Command arguments', }, }, required: ['command'], }, }, // Additional tools... ], })); ``` The server will use stdio transport for compatibility with Claude Desktop and other MCP clients: ```typescript const transport = new StdioServerTransport(); await this.server.connect(transport); ``` -------------------------------------------------------------------------------- /docs/adr/003-command-approval-workflow.md: -------------------------------------------------------------------------------- ```markdown # ADR 003: Command Approval Workflow ## Status Accepted ## Context 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. Options considered: 1. Reject all potentially dangerous commands 2. Allow all commands with appropriate warnings 3. Implement an approval workflow for commands that require additional verification ## Decision We will implement an approval workflow for commands that are potentially dangerous but still necessary. This workflow will: 1. Queue commands marked as requiring approval 2. Provide tools to list pending commands 3. Allow explicit approval or denial of pending commands 4. Execute approved commands and reject denied commands This approach balances security with usability by allowing potentially dangerous commands to be executed after explicit approval. ## Consequences ### Positive - Provides a middle ground between allowing and forbidding commands - Creates an audit trail of command approvals - Allows for human judgment in borderline cases - Enables safe use of necessary system-modifying commands - Prevents accidental execution of dangerous commands ### Negative - Introduces asynchronous workflow for command execution - Requires additional user interaction for approval - May create confusion if approvals are delayed or forgotten ## Implementation The approval workflow will be implemented using a queue of pending commands: ```typescript interface PendingCommand { id: string; command: string; args: string[]; requestedAt: Date; requestedBy?: string; resolve: (value: CommandResult) => void; reject: (reason: Error) => void; } ``` When a command requiring approval is executed, it will be added to the queue: ```typescript private queueCommandForApproval( command: string, args: string[] = [], requestedBy?: string ): Promise<CommandResult> { return new Promise((resolve, reject) => { const id = randomUUID(); const pendingCommand: PendingCommand = { id, command, args, requestedAt: new Date(), requestedBy, resolve, reject }; this.pendingCommands.set(id, pendingCommand); this.emit('command:pending', pendingCommand); }); } ``` The MCP server will expose tools to list, approve, and deny pending commands: ```typescript // Get pending commands const pendingCommands = await client.callTool('get_pending_commands', {}); // Approve a command await client.callTool('approve_command', { commandId }); // Deny a command await client.callTool('deny_command', { commandId, reason: 'Not allowed' }); ``` This workflow ensures that potentially dangerous commands are only executed after explicit approval, providing an additional layer of security. ``` -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- ```javascript import js from '@eslint/js'; import tsEslint from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; export default [ js.configs.recommended, { files: ['**/*.js'], languageOptions: { ecmaVersion: 2022, sourceType: 'module', globals: { console: 'readonly', process: 'readonly', Buffer: 'readonly', __dirname: 'readonly', __filename: 'readonly', global: 'readonly', module: 'readonly', require: 'readonly', exports: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', setImmediate: 'readonly', clearImmediate: 'readonly' } }, rules: { 'no-unused-vars': 'warn', 'no-console': 'off', 'prefer-const': 'error', 'no-var': 'error' } }, { files: ['**/*.ts'], languageOptions: { parser: tsParser, ecmaVersion: 2022, sourceType: 'module', globals: { console: 'readonly', process: 'readonly', Buffer: 'readonly', __dirname: 'readonly', __filename: 'readonly', global: 'readonly', module: 'readonly', require: 'readonly', exports: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', setImmediate: 'readonly', clearImmediate: 'readonly' } }, plugins: { '@typescript-eslint': tsEslint }, rules: { 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 'no-console': 'off', 'prefer-const': 'error', 'no-var': 'error' } }, { files: ['**/*.cjs'], languageOptions: { ecmaVersion: 2022, sourceType: 'commonjs', globals: { console: 'readonly', process: 'readonly', Buffer: 'readonly', __dirname: 'readonly', __filename: 'readonly', global: 'readonly', module: 'readonly', require: 'readonly', exports: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', setImmediate: 'readonly', clearImmediate: 'readonly' } }, rules: { 'no-unused-vars': 'warn', 'no-console': 'off', 'prefer-const': 'error', 'no-var': 'error' } }, { files: ['**/*.test.js', '**/*.test.ts', '**/tests/**/*.js', '**/tests/**/*.ts'], languageOptions: { globals: { describe: 'readonly', test: 'readonly', it: 'readonly', expect: 'readonly', beforeAll: 'readonly', afterAll: 'readonly', beforeEach: 'readonly', afterEach: 'readonly', jest: 'readonly' } } }, { ignores: ['build/**', 'node_modules/**', '*.config.js', '*.config.cjs'] } ]; ``` -------------------------------------------------------------------------------- /src/utils/platform-utils.ts: -------------------------------------------------------------------------------- ```typescript import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; /** * Supported platform types */ export enum PlatformType { WINDOWS = 'windows', MACOS = 'macos', LINUX = 'linux', UNKNOWN = 'unknown' } /** * Detect the current platform * @returns The detected platform type */ export function detectPlatform(): PlatformType { const platform = process.platform; if (platform === 'win32') return PlatformType.WINDOWS; if (platform === 'darwin') return PlatformType.MACOS; if (platform === 'linux') return PlatformType.LINUX; return PlatformType.UNKNOWN; } /** * Get the default shell for the current platform * @returns Path to the default shell */ export function getDefaultShell(): string { const platform = detectPlatform(); switch (platform) { case PlatformType.WINDOWS: return process.env.COMSPEC || 'cmd.exe'; case PlatformType.MACOS: return '/bin/zsh'; case PlatformType.LINUX: return process.env.SHELL || '/bin/bash'; default: return process.env.SHELL || '/bin/sh'; } } /** * Validate if a shell path exists and is executable * @param shellPath Path to the shell * @returns True if the shell is valid */ export function validateShellPath(shellPath: string): boolean { try { return fs.existsSync(shellPath) && fs.statSync(shellPath).isFile(); } catch (error) { return false; } } /** * Get shell suggestions for each platform * @returns Record of platform types to array of suggested shells */ export function getShellSuggestions(): Record<PlatformType, string[]> { return { [PlatformType.WINDOWS]: ['cmd.exe', 'powershell.exe', 'pwsh.exe'], [PlatformType.MACOS]: ['/bin/zsh', '/bin/bash', '/bin/sh'], [PlatformType.LINUX]: ['/bin/bash', '/bin/sh', '/bin/zsh'], [PlatformType.UNKNOWN]: ['/bin/sh'] }; } /** * Get common locations for shells on the current platform * @returns Array of common shell locations */ export function getCommonShellLocations(): string[] { const platform = detectPlatform(); switch (platform) { case PlatformType.WINDOWS: return [ process.env.COMSPEC || 'C:\\Windows\\System32\\cmd.exe', 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' ]; case PlatformType.MACOS: return ['/bin/zsh', '/bin/bash', '/bin/sh']; case PlatformType.LINUX: return ['/bin/bash', '/bin/sh', '/usr/bin/bash', '/usr/bin/zsh']; default: return ['/bin/sh']; } } /** * Get helpful message for shell configuration * @returns A helpful message with shell configuration guidance */ export function getShellConfigurationHelp(): string { const platform = detectPlatform(); const suggestions = getShellSuggestions()[platform]; const locations = getCommonShellLocations(); let message = 'Shell Configuration Help:\n\n'; message += `Detected platform: ${platform}\n\n`; message += 'Suggested shells for this platform:\n'; suggestions.forEach(shell => { message += `- ${shell}\n`; }); message += '\nCommon shell locations on this platform:\n'; locations.forEach(location => { message += `- ${location}\n`; }); message += '\nTo configure a custom shell, provide the full path to the shell executable.'; return message; } ``` -------------------------------------------------------------------------------- /examples/client-example.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node import { McpClient } from '@modelcontextprotocol/sdk/client/index.js'; import { ChildProcessClientTransport } from '@modelcontextprotocol/sdk/client/child-process.js'; /** * Example client for the Mac Shell MCP Server * This demonstrates how to connect to the server and use its tools */ async function main() { // Create a client transport that connects to the server const transport = new ChildProcessClientTransport({ command: 'node', args: ['../build/index.js'], }); // Create the MCP client const client = new McpClient(); try { // Connect to the server await client.connect(transport); console.log('Connected to Mac Shell MCP Server'); // List available tools const tools = await client.listTools(); console.log('Available tools:'); tools.forEach(tool => { console.log(`- ${tool.name}: ${tool.description}`); }); // Get the whitelist console.log('\nWhitelisted commands:'); const whitelist = await client.callTool('get_whitelist', {}); console.log(whitelist.content[0].text); // Execute a safe command console.log('\nExecuting a safe command (ls -la):'); const lsResult = await client.callTool('execute_command', { command: 'ls', args: ['-la'], }); console.log(lsResult.content[0].text); // Add a command to the whitelist console.log('\nAdding a command to the whitelist:'); const addResult = await client.callTool('add_to_whitelist', { command: 'node', securityLevel: 'safe', description: 'Execute Node.js scripts', }); console.log(addResult.content[0].text); // Try executing a command that requires approval console.log('\nExecuting a command that requires approval (mkdir test-dir):'); try { const mkdirResult = await client.callTool('execute_command', { command: 'mkdir', args: ['test-dir'], }); console.log(mkdirResult.content[0].text); } catch (error) { console.log('Command requires approval. Getting pending commands...'); // Get pending commands const pendingCommands = await client.callTool('get_pending_commands', {}); const pendingCommandsObj = JSON.parse(pendingCommands.content[0].text); if (pendingCommandsObj.length > 0) { const commandId = pendingCommandsObj[0].id; console.log(`Approving command with ID: ${commandId}`); // Approve the command const approveResult = await client.callTool('approve_command', { commandId, }); console.log(approveResult.content[0].text); } } // Clean up - remove the test directory console.log('\nCleaning up:'); try { // This will fail because 'rm' is forbidden const rmResult = await client.callTool('execute_command', { command: 'rm', args: ['-rf', 'test-dir'], }); console.log(rmResult.content[0].text); } catch (error) { console.log(`Clean-up failed: ${error.message}`); console.log('Note: This is expected because "rm" is a forbidden command'); } } catch (error) { console.error('Error:', error); } finally { // Disconnect from the server await client.disconnect(); console.log('\nDisconnected from Mac Shell MCP Server'); } } main().catch(console.error); ``` -------------------------------------------------------------------------------- /docs/adr/004-cross-platform-support.md: -------------------------------------------------------------------------------- ```markdown # ADR 004: Cross-Platform Shell Support ## Status Accepted ## Context 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. Key limitations in the original implementation: 1. **Shell Path Hardcoding**: The server was hardcoded to use `/bin/zsh` as the default shell 2. **Command Set Assumptions**: The default whitelist included macOS/Unix commands that don't exist natively on Windows 3. **Path Handling**: Command validation extracted the base command by splitting on '/' which doesn't work for Windows backslash paths 4. **Naming and Documentation**: The server was explicitly named "mac-shell-mcp" and documented for macOS ## Decision We will refactor the server to be platform-agnostic with the following changes: 1. **Platform Detection**: Implement platform detection using `process.platform` to identify the current operating system 2. **Shell Selection**: Select appropriate default shell based on platform and allow shell path to be configurable 3. **Path Normalization**: Use Node.js `path` module for cross-platform path handling 4. **Platform-Specific Command Whitelists**: Implement separate command whitelists for each supported platform 5. **Rename and Rebrand**: Rename to "super-shell-mcp" and update documentation to reflect cross-platform support ## Consequences ### Positive - Works across Windows, macOS, and Linux - Supports various shells based on user preference - Maintains the same security model across platforms - Provides consistent experience regardless of platform - Increases the potential user base by supporting multiple platforms ### Negative - Increased complexity in command handling - Need to maintain separate command whitelists for each platform - Some commands may behave differently across platforms - Testing becomes more complex, requiring validation on multiple platforms ## Implementation The implementation uses a platform detection utility: ```typescript export function detectPlatform(): PlatformType { const platform = process.platform; if (platform === 'win32') return PlatformType.WINDOWS; if (platform === 'darwin') return PlatformType.MACOS; if (platform === 'linux') return PlatformType.LINUX; return PlatformType.UNKNOWN; } ``` Platform-specific shell detection: ```typescript export function getDefaultShell(): string { const platform = detectPlatform(); switch (platform) { case PlatformType.WINDOWS: return process.env.COMSPEC || 'cmd.exe'; case PlatformType.MACOS: return '/bin/zsh'; case PlatformType.LINUX: return process.env.SHELL || '/bin/bash'; default: return process.env.SHELL || '/bin/sh'; } } ``` Platform-specific command whitelists: ```typescript private initializeDefaultWhitelist(): void { const platformCommands = getPlatformSpecificCommands(); platformCommands.forEach(entry => { this.whitelist.set(entry.command, entry); }); } ``` Cross-platform path handling: ```typescript private validateCommand(command: string, args: string[]): CommandSecurityLevel | null { // Extract the base command (without path) using path.basename const baseCommand = path.basename(command); // Check if the command is in the whitelist const entry = this.whitelist.get(baseCommand); if (!entry) { return null; } // Rest of validation... } ``` -------------------------------------------------------------------------------- /tests/command-service.platform.test.js: -------------------------------------------------------------------------------- ```javascript const { CommandService, CommandSecurityLevel } = require('../build/services/command-service.js'); const { detectPlatform, PlatformType } = require('../build/utils/platform-utils.js'); describe('CommandService Platform Tests', () => { let commandService; const currentPlatform = detectPlatform(); beforeEach(() => { // Create a new CommandService instance for each test with auto-detected shell commandService = new CommandService(); }); test('should initialize with platform-specific whitelist', () => { const whitelist = commandService.getWhitelist(); expect(whitelist).toBeDefined(); expect(whitelist.length).toBeGreaterThan(0); // Check for common command across all platforms const echoCommand = whitelist.find(entry => entry.command === 'echo'); expect(echoCommand).toBeDefined(); expect(echoCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); // Platform-specific command checks if (currentPlatform === PlatformType.WINDOWS) { // Windows-specific commands const dirCommand = whitelist.find(entry => entry.command === 'dir'); expect(dirCommand).toBeDefined(); expect(dirCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); const delCommand = whitelist.find(entry => entry.command === 'del'); expect(delCommand).toBeDefined(); expect(delCommand.securityLevel).toBe(CommandSecurityLevel.FORBIDDEN); } else { // Unix-like platforms (macOS, Linux) const lsCommand = whitelist.find(entry => entry.command === 'ls'); expect(lsCommand).toBeDefined(); expect(lsCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); const rmCommand = whitelist.find(entry => entry.command === 'rm'); expect(rmCommand).toBeDefined(); expect(rmCommand.securityLevel).toBe(CommandSecurityLevel.FORBIDDEN); } }); test('should execute platform-specific safe command', async () => { // Choose a command based on platform const command = currentPlatform === PlatformType.WINDOWS ? 'echo' : 'echo'; const args = ['test']; const result = await commandService.executeCommand(command, args); expect(result).toBeDefined(); expect(result.stdout.trim()).toBe('test'); }); test('should reject platform-specific forbidden command', async () => { // Choose a forbidden command based on platform const command = currentPlatform === PlatformType.WINDOWS ? 'del' : 'rm'; const args = currentPlatform === PlatformType.WINDOWS ? ['test.txt'] : ['-rf', 'test']; await expect(commandService.executeCommand(command, args)).rejects.toThrow(); }); test('should queue platform-specific command requiring approval', async () => { // Set up event listener to capture pending command let pendingCommandId = null; commandService.on('command:pending', (pendingCommand) => { pendingCommandId = pendingCommand.id; }); // Choose a command requiring approval based on platform // Use a command that doesn't actually create anything to avoid test failures const command = currentPlatform === PlatformType.WINDOWS ? 'copy' : 'cp'; const args = ['nonexistent-file', 'nonexistent-copy']; // Execute a command that requires approval const executePromise = commandService.executeCommand(command, args); // Wait a bit for the event to fire await new Promise(resolve => setTimeout(resolve, 100)); // Check if we got a pending command expect(pendingCommandId).not.toBeNull(); // Get pending commands const pendingCommands = commandService.getPendingCommands(); expect(pendingCommands.length).toBe(1); expect(pendingCommands[0].id).toBe(pendingCommandId); // Approve the command const approvePromise = commandService.approveCommand(pendingCommandId); try { // Wait for both promises to resolve await Promise.all([executePromise, approvePromise]); } catch (error) { // Expect an error since we're trying to copy a non-existent file // This is expected and we can ignore it } // Check that there are no more pending commands expect(commandService.getPendingCommands().length).toBe(0); }); test('should deny platform-specific command requiring approval', async () => { // Set up event listener to capture pending command let pendingCommandId = null; commandService.on('command:pending', (pendingCommand) => { pendingCommandId = pendingCommand.id; }); // Choose a command requiring approval based on platform const command = currentPlatform === PlatformType.WINDOWS ? 'mkdir' : 'mkdir'; const args = ['test-dir']; // Execute a command that requires approval const executePromise = commandService.executeCommand(command, args); // Wait a bit for the event to fire await new Promise(resolve => setTimeout(resolve, 100)); // Check if we got a pending command expect(pendingCommandId).not.toBeNull(); // Deny the command commandService.denyCommand(pendingCommandId, 'Test denial'); // The execute promise should be rejected await expect(executePromise).rejects.toThrow('Test denial'); // Check that there are no more pending commands expect(commandService.getPendingCommands().length).toBe(0); }); }); ``` -------------------------------------------------------------------------------- /tests/command-service.test.js: -------------------------------------------------------------------------------- ```javascript const { CommandService, CommandSecurityLevel } = require('../build/services/command-service.js'); const { getDefaultShell } = require('../build/utils/platform-utils.js'); describe('CommandService', () => { let commandService; beforeEach(() => { // Create a new CommandService instance for each test with auto-detected shell commandService = new CommandService(); }); test('should initialize with default whitelist', () => { const whitelist = commandService.getWhitelist(); expect(whitelist).toBeDefined(); expect(whitelist.length).toBeGreaterThan(0); // Check if common commands are in the whitelist const lsCommand = whitelist.find(entry => entry.command === 'ls'); expect(lsCommand).toBeDefined(); expect(lsCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); const rmCommand = whitelist.find(entry => entry.command === 'rm'); expect(rmCommand).toBeDefined(); expect(rmCommand.securityLevel).toBe(CommandSecurityLevel.FORBIDDEN); }); test('should add command to whitelist', () => { const testCommand = { command: 'test-command', securityLevel: CommandSecurityLevel.SAFE, description: 'Test command' }; commandService.addToWhitelist(testCommand); const whitelist = commandService.getWhitelist(); const addedCommand = whitelist.find(entry => entry.command === 'test-command'); expect(addedCommand).toBeDefined(); expect(addedCommand.securityLevel).toBe(CommandSecurityLevel.SAFE); expect(addedCommand.description).toBe('Test command'); }); test('should update command security level', () => { // First add a command const testCommand = { command: 'test-command', securityLevel: CommandSecurityLevel.SAFE, description: 'Test command' }; commandService.addToWhitelist(testCommand); // Then update its security level commandService.updateSecurityLevel('test-command', CommandSecurityLevel.REQUIRES_APPROVAL); const whitelist = commandService.getWhitelist(); const updatedCommand = whitelist.find(entry => entry.command === 'test-command'); expect(updatedCommand).toBeDefined(); expect(updatedCommand.securityLevel).toBe(CommandSecurityLevel.REQUIRES_APPROVAL); }); test('should remove command from whitelist', () => { // First add a command const testCommand = { command: 'test-command', securityLevel: CommandSecurityLevel.SAFE, description: 'Test command' }; commandService.addToWhitelist(testCommand); // Then remove it commandService.removeFromWhitelist('test-command'); const whitelist = commandService.getWhitelist(); const removedCommand = whitelist.find(entry => entry.command === 'test-command'); expect(removedCommand).toBeUndefined(); }); test('should execute safe command', async () => { // Execute a safe command (echo) const result = await commandService.executeCommand('echo', ['test']); expect(result).toBeDefined(); expect(result.stdout.trim()).toBe('test'); }); test('should reject forbidden command', async () => { // Try to execute a forbidden command (rm) await expect(commandService.executeCommand('rm', ['-rf', 'test'])).rejects.toThrow(); }); test('should queue command requiring approval', async () => { // Set up event listener to capture pending command let pendingCommandId = null; commandService.on('command:pending', (pendingCommand) => { pendingCommandId = pendingCommand.id; }); // Execute a command that requires approval // Use a command that doesn't actually create anything to avoid test failures const executePromise = commandService.executeCommand('cp', ['nonexistent-file', 'nonexistent-copy']); // Wait a bit for the event to fire await new Promise(resolve => setTimeout(resolve, 100)); // Check if we got a pending command expect(pendingCommandId).not.toBeNull(); // Get pending commands const pendingCommands = commandService.getPendingCommands(); expect(pendingCommands.length).toBe(1); expect(pendingCommands[0].id).toBe(pendingCommandId); // Approve the command const approvePromise = commandService.approveCommand(pendingCommandId); try { // Wait for both promises to resolve await Promise.all([executePromise, approvePromise]); } catch (error) { // Expect an error since we're trying to copy a non-existent file // This is expected and we can ignore it } // Check that there are no more pending commands expect(commandService.getPendingCommands().length).toBe(0); // Clean up try { await commandService.executeCommand('rmdir', ['test-dir']); } catch (error) { // Ignore cleanup errors } }); test('should deny command requiring approval', async () => { // Set up event listener to capture pending command let pendingCommandId = null; commandService.on('command:pending', (pendingCommand) => { pendingCommandId = pendingCommand.id; }); // Execute a command that requires approval (mkdir) const executePromise = commandService.executeCommand('mkdir', ['test-dir']); // Wait a bit for the event to fire await new Promise(resolve => setTimeout(resolve, 100)); // Check if we got a pending command expect(pendingCommandId).not.toBeNull(); // Deny the command commandService.denyCommand(pendingCommandId, 'Test denial'); // The execute promise should be rejected await expect(executePromise).rejects.toThrow('Test denial'); // Check that there are no more pending commands expect(commandService.getPendingCommands().length).toBe(0); }); }); ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.0.13] - 2025-03-14 ### Fixed - Updated the package.json file to include the proper github repo information ## [2.0.12] - 2025-03-14 ### Changed - Converted project from CommonJS to ESM module system - Updated TypeScript configuration to use NodeNext module system - Added ESM-compatible workaround for `__dirname` in ESM context ### Fixed - Fixed Jest test suite to work with ESM modules - Added CommonJS-compatible mock modules for testing - Updated module imports to use ESM syntax with .js extensions ## [2.0.11] - 2025-03-14 ### Added - Published package to npm at https://www.npmjs.com/package/super-shell-mcp - Updated README.md to highlight NPX installation method as the recommended approach - Added benefits of using NPX in documentation - Enhanced configuration examples for easier setup ### Changed - Reorganized documentation to prioritize NPX installation method - Simplified GitHub installation instructions - Improved package publishing documentation ## [2.0.10] - 2025-03-14 ### Added - Added logs directory with .gitkeep file to ensure logs directory is tracked by Git - Enhanced error handling for command approval process - Improved logging for command execution and approval workflow ### Fixed - Fixed version inconsistencies across package.json, package-lock.json, and src/index.ts - Improved documentation for logging system - Enhanced cross-platform compatibility for log file paths ## [2.0.9] - 2025-03-14 ### Added - Added additional logging for command approval workflow - Improved error handling for command execution - Enhanced debugging capabilities with more detailed logs ### Fixed - Fixed minor issues with command approval workflow - Improved reliability of command execution across platforms - Enhanced error messages for better troubleshooting ## [2.0.8] - 2025-03-13 ### Added - Added comprehensive logging system with file-based logs - Implemented non-blocking command approval workflow - Added new `queueCommandForApprovalNonBlocking` method to CommandService ### Fixed - Fixed timeout issue with commands requiring approval by implementing non-blocking approval workflow - Improved user experience by providing immediate feedback for commands requiring approval - Enhanced error handling for commands requiring approval - Fixed issue where pending commands would cause client timeouts ## [2.0.7] - 2025-03-13 ### Fixed - Fixed timeout issue when using the "Approve" button in Roo Code client - Improved error handling in `handleApproveCommand` method to bypass Promise resolution mechanism - Added detailed logging for command approval process to aid debugging - Enhanced direct command execution in approval workflow to prevent timeouts - Fixed TypeScript errors related to error handling in command execution ## [2.0.6] - 2025-03-13 ### Fixed - Fixed command approval workflow to prevent timeout errors - Added immediate detection of commands requiring approval - Improved error messages for commands requiring approval with clear instructions - Added direct guidance to use get_pending_commands and approve_command functions ## [2.0.5] - 2025-03-13 ### Added - Improved command approval workflow with timeout detection - Added guidance for AI assistants when command approval times out - Enhanced error messages for commands requiring approval ### Fixed - Module compatibility issues between ES Modules and CommonJS in tests - Updated TypeScript configuration to use CommonJS module system for better test compatibility - Removed unnecessary CommonJS compatibility code from source files - Changed package.json "type" from "module" to "commonjs" for consistent module system ## [2.0.4] - 2025-03-13 ### Fixed - Module compatibility issues between ES Modules and CommonJS in tests - Updated TypeScript configuration to use CommonJS module system for better test compatibility - Removed unnecessary CommonJS compatibility code from source files - Changed package.json "type" from "module" to "commonjs" for consistent module system ## [2.0.3] - 2025-03-12 ### Added - NPX best practices documentation in README.md - Improved package.json configuration for NPX compatibility ### Changed - Updated TypeScript configuration to use NodeNext module system for better ES Modules support - Added prepare script to ensure compilation happens on install - Added files field to package.json to specify which files to include when publishing - Simplified build script to use chmod directly instead of custom script ## [2.0.2] - 2025-03-12 ### Fixed - Test suite compatibility with cross-platform environments - Module system compatibility between ESM and CommonJS - Fixed platform-specific test cases to work on all operating systems - Updated TypeScript configuration for better CommonJS compatibility - Improved test reliability by using non-filesystem-modifying commands ## [2.0.1] - 2025-03-12 ### Added - Platform-aware test suite that adapts to the current operating system - Cross-platform build script that works on Windows, macOS, and Linux - Enhanced platform-specific documentation with configuration examples - Troubleshooting guide for common cross-platform issues - Detailed shell path examples for each supported platform ### Fixed - Build script compatibility with Windows (removed Unix-specific chmod) - Test suite compatibility with Windows command sets - Path handling in shell validation for Windows paths ## [2.0.0] - 2025-03-12 ### Added - Cross-platform support for Windows, macOS, and Linux - Platform detection using `process.platform` - Auto-detection of appropriate shell based on platform - Platform-specific command whitelists - New `get_platform_info` tool to retrieve platform and shell information - Support for Windows shells (cmd.exe, PowerShell) - Support for Linux shells (bash, sh) - New ADR for cross-platform support ### Changed - Renamed from "mac-shell-mcp" to "super-shell-mcp" - Updated path handling to use Node.js path module for cross-platform compatibility - Modified command validation to work with Windows paths - Updated documentation to reflect cross-platform support - Refactored code to be platform-agnostic - Made shell configurable with auto-detection as fallback ## [1.0.3] - 2025-03-12 ### Fixed - Improved documentation for Claude Desktop configuration which uses boolean value for `alwaysAllow` - Added separate configuration examples for Roo Code and Claude Desktop - Clarified that Roo Code uses array format while Claude Desktop uses boolean format - Added explicit note that the `alwaysAllow` parameter is processed by the MCP client, not the server ## [1.0.2] - 2025-03-12 ### Fixed - Fixed MCP configuration format to use an empty array `[]` for `alwaysAllow` instead of `false` - Updated all configuration examples in README.md to use the correct format - Fixed error "Invalid config: missing or invalid parameters" when adding to MCP settings ## [1.0.1] - 2025-03-12 ### Added - Support for using the server as an npm package with npx - Added bin field to package.json for CLI usage - Improved MCP configuration instructions for Roo Code and Claude Desktop - Added examples for using with npx directly from GitHub ## [1.0.0] - 2025-03-12 ### Added - Initial release of the Mac Shell MCP Server - Command execution service with ZSH shell support - Command whitelisting system with three security levels: - Safe commands (no approval required) - Commands requiring approval - Forbidden commands - Pre-configured whitelist with common safe commands - Approval workflow for potentially dangerous commands - MCP tools for command execution and whitelist management: - `execute_command`: Execute shell commands - `get_whitelist`: Get the list of whitelisted commands - `add_to_whitelist`: Add a command to the whitelist - `update_security_level`: Update a command's security level - `remove_from_whitelist`: Remove a command from the whitelist - `get_pending_commands`: Get commands pending approval - `approve_command`: Approve a pending command - `deny_command`: Deny a pending command - Comprehensive test suite for the command service - Example client implementation - Documentation and configuration examples ``` -------------------------------------------------------------------------------- /src/utils/command-whitelist-utils.ts: -------------------------------------------------------------------------------- ```typescript import { CommandSecurityLevel, CommandWhitelistEntry } from '../services/command-service.js'; import { PlatformType, detectPlatform } from './platform-utils.js'; /** * Get common safe commands that work across all platforms * @returns Array of common safe command whitelist entries */ export function getCommonSafeCommands(): CommandWhitelistEntry[] { return [ { command: 'echo', securityLevel: CommandSecurityLevel.SAFE, description: 'Print text to standard output' } ]; } /** * Get Windows-specific safe commands * @returns Array of Windows safe command whitelist entries */ export function getWindowsSafeCommands(): CommandWhitelistEntry[] { return [ { command: 'dir', securityLevel: CommandSecurityLevel.SAFE, description: 'List directory contents' }, { command: 'type', securityLevel: CommandSecurityLevel.SAFE, description: 'Display the contents of a text file' }, { command: 'cd', securityLevel: CommandSecurityLevel.SAFE, description: 'Change directory' }, { command: 'findstr', securityLevel: CommandSecurityLevel.SAFE, description: 'Search for strings in files' }, { command: 'where', securityLevel: CommandSecurityLevel.SAFE, description: 'Locate programs' }, { command: 'whoami', securityLevel: CommandSecurityLevel.SAFE, description: 'Display current user' }, { command: 'hostname', securityLevel: CommandSecurityLevel.SAFE, description: 'Display computer name' }, { command: 'ver', securityLevel: CommandSecurityLevel.SAFE, description: 'Display operating system version' } ]; } /** * Get macOS-specific safe commands * @returns Array of macOS safe command whitelist entries */ export function getMacOSSafeCommands(): CommandWhitelistEntry[] { return [ { command: 'ls', securityLevel: CommandSecurityLevel.SAFE, description: 'List directory contents' }, { command: 'pwd', securityLevel: CommandSecurityLevel.SAFE, description: 'Print working directory' }, { command: 'cat', securityLevel: CommandSecurityLevel.SAFE, description: 'Concatenate and print files' }, { command: 'grep', securityLevel: CommandSecurityLevel.SAFE, description: 'Search for patterns in files' }, { command: 'find', securityLevel: CommandSecurityLevel.SAFE, description: 'Find files in a directory hierarchy' }, { command: 'cd', securityLevel: CommandSecurityLevel.SAFE, description: 'Change directory' }, { command: 'head', securityLevel: CommandSecurityLevel.SAFE, description: 'Output the first part of files' }, { command: 'tail', securityLevel: CommandSecurityLevel.SAFE, description: 'Output the last part of files' }, { command: 'wc', securityLevel: CommandSecurityLevel.SAFE, description: 'Print newline, word, and byte counts' } ]; } /** * Get Linux-specific safe commands * @returns Array of Linux safe command whitelist entries */ export function getLinuxSafeCommands(): CommandWhitelistEntry[] { // Linux safe commands are similar to macOS return getMacOSSafeCommands(); } /** * Get Windows-specific commands requiring approval * @returns Array of Windows command whitelist entries requiring approval */ export function getWindowsApprovalCommands(): CommandWhitelistEntry[] { return [ { command: 'copy', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Copy files' }, { command: 'move', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Move files' }, { command: 'mkdir', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Create directories' }, { command: 'rmdir', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Remove directories' }, { command: 'rename', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Rename files' }, { command: 'attrib', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file attributes' } ]; } /** * Get macOS-specific commands requiring approval * @returns Array of macOS command whitelist entries requiring approval */ export function getMacOSApprovalCommands(): CommandWhitelistEntry[] { return [ { command: 'mv', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Move (rename) files' }, { command: 'cp', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Copy files and directories' }, { command: 'mkdir', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Create directories' }, { command: 'touch', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file timestamps or create empty files' }, { command: 'chmod', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file mode bits' }, { command: 'chown', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file owner and group' } ]; } /** * Get Linux-specific commands requiring approval * @returns Array of Linux command whitelist entries requiring approval */ export function getLinuxApprovalCommands(): CommandWhitelistEntry[] { // Linux approval commands are similar to macOS return getMacOSApprovalCommands(); } /** * Get Windows-specific forbidden commands * @returns Array of Windows forbidden command whitelist entries */ export function getWindowsForbiddenCommands(): CommandWhitelistEntry[] { return [ { command: 'del', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Delete files' }, { command: 'erase', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Delete files' }, { command: 'format', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Format a disk' }, { command: 'runas', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Execute a program as another user' } ]; } /** * Get macOS-specific forbidden commands * @returns Array of macOS forbidden command whitelist entries */ export function getMacOSForbiddenCommands(): CommandWhitelistEntry[] { return [ { command: 'rm', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Remove files or directories' }, { command: 'sudo', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Execute a command as another user' } ]; } /** * Get Linux-specific forbidden commands * @returns Array of Linux forbidden command whitelist entries */ export function getLinuxForbiddenCommands(): CommandWhitelistEntry[] { // Linux forbidden commands are similar to macOS return getMacOSForbiddenCommands(); } /** * Get platform-specific command whitelist entries * @returns Array of command whitelist entries for the current platform */ export function getPlatformSpecificCommands(): CommandWhitelistEntry[] { const platform = detectPlatform(); let safeCommands: CommandWhitelistEntry[] = []; let approvalCommands: CommandWhitelistEntry[] = []; let forbiddenCommands: CommandWhitelistEntry[] = []; // Add common safe commands that work across all platforms const commonSafeCommands = getCommonSafeCommands(); // Add platform-specific commands switch (platform) { case PlatformType.WINDOWS: safeCommands = getWindowsSafeCommands(); approvalCommands = getWindowsApprovalCommands(); forbiddenCommands = getWindowsForbiddenCommands(); break; case PlatformType.MACOS: safeCommands = getMacOSSafeCommands(); approvalCommands = getMacOSApprovalCommands(); forbiddenCommands = getMacOSForbiddenCommands(); break; case PlatformType.LINUX: safeCommands = getLinuxSafeCommands(); approvalCommands = getLinuxApprovalCommands(); forbiddenCommands = getLinuxForbiddenCommands(); break; default: // Use Unix-like defaults for unknown platforms safeCommands = getLinuxSafeCommands(); approvalCommands = getLinuxApprovalCommands(); forbiddenCommands = getLinuxForbiddenCommands(); } // Combine all commands return [...commonSafeCommands, ...safeCommands, ...approvalCommands, ...forbiddenCommands]; } ``` -------------------------------------------------------------------------------- /jest.setup.cjs: -------------------------------------------------------------------------------- ``` // Mock the ESM modules with CommonJS equivalents for Jest const fs = require('fs'); const path = require('path'); const { EventEmitter } = require('events'); const { execFile } = require('child_process'); const { promisify } = require('util'); const { randomUUID } = require('crypto'); // Define the CommandSecurityLevel enum const CommandSecurityLevel = { SAFE: 'safe', REQUIRES_APPROVAL: 'requires_approval', FORBIDDEN: 'forbidden' }; // Define the PlatformType enum const PlatformType = { WINDOWS: 'windows', MACOS: 'macos', LINUX: 'linux', UNKNOWN: 'unknown' }; // Mock the platform-utils module const detectPlatform = () => { const platform = process.platform; if (platform === 'win32') return PlatformType.WINDOWS; if (platform === 'darwin') return PlatformType.MACOS; if (platform === 'linux') return PlatformType.LINUX; return PlatformType.UNKNOWN; }; const getDefaultShell = () => { const platform = detectPlatform(); switch (platform) { case PlatformType.WINDOWS: return process.env.COMSPEC || 'cmd.exe'; case PlatformType.MACOS: return '/bin/zsh'; case PlatformType.LINUX: return process.env.SHELL || '/bin/bash'; default: return process.env.SHELL || '/bin/sh'; } }; const validateShellPath = (shellPath) => { try { return fs.existsSync(shellPath) && fs.statSync(shellPath).isFile(); } catch (error) { return false; } }; const getShellSuggestions = () => ({ [PlatformType.WINDOWS]: ['cmd.exe', 'powershell.exe', 'pwsh.exe'], [PlatformType.MACOS]: ['/bin/zsh', '/bin/bash', '/bin/sh'], [PlatformType.LINUX]: ['/bin/bash', '/bin/sh', '/bin/zsh'], [PlatformType.UNKNOWN]: ['/bin/sh'] }); const getCommonShellLocations = () => { const platform = detectPlatform(); switch (platform) { case PlatformType.WINDOWS: return [ process.env.COMSPEC || 'C:\\Windows\\System32\\cmd.exe', 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' ]; case PlatformType.MACOS: return ['/bin/zsh', '/bin/bash', '/bin/sh']; case PlatformType.LINUX: return ['/bin/bash', '/bin/sh', '/usr/bin/bash', '/usr/bin/zsh']; default: return ['/bin/sh']; } }; const getShellConfigurationHelp = () => { const platform = detectPlatform(); const suggestions = getShellSuggestions()[platform]; const locations = getCommonShellLocations(); let message = 'Shell Configuration Help:\n\n'; message += `Detected platform: ${platform}\n\n`; message += 'Suggested shells for this platform:\n'; suggestions.forEach(shell => { message += `- ${shell}\n`; }); message += '\nCommon shell locations on this platform:\n'; locations.forEach(location => { message += `- ${location}\n`; }); message += '\nTo configure a custom shell, provide the full path to the shell executable.'; return message; }; // Mock the CommandService class class CommandService extends EventEmitter { constructor(shell, defaultTimeout = 30000) { super(); this.shell = shell || getDefaultShell(); this.whitelist = new Map(); this.pendingCommands = new Map(); this.defaultTimeout = defaultTimeout; this.initializeDefaultWhitelist(); } getShell() { return this.shell; } initializeDefaultWhitelist() { const platform = detectPlatform(); const commands = []; // Common commands for all platforms commands.push({ command: 'echo', securityLevel: CommandSecurityLevel.SAFE, description: 'Print text to standard output' }); // Platform-specific commands if (platform === PlatformType.WINDOWS) { commands.push({ command: 'dir', securityLevel: CommandSecurityLevel.SAFE, description: 'List directory contents' }); commands.push({ command: 'copy', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Copy files' }); commands.push({ command: 'mkdir', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Create directories' }); commands.push({ command: 'del', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Delete files' }); } else { commands.push({ command: 'ls', securityLevel: CommandSecurityLevel.SAFE, description: 'List directory contents' }); commands.push({ command: 'cat', securityLevel: CommandSecurityLevel.SAFE, description: 'Concatenate and print files' }); commands.push({ command: 'grep', securityLevel: CommandSecurityLevel.SAFE, description: 'Search for patterns in files' }); commands.push({ command: 'find', securityLevel: CommandSecurityLevel.SAFE, description: 'Find files in a directory hierarchy' }); commands.push({ command: 'cd', securityLevel: CommandSecurityLevel.SAFE, description: 'Change directory' }); commands.push({ command: 'head', securityLevel: CommandSecurityLevel.SAFE, description: 'Output the first part of files' }); commands.push({ command: 'tail', securityLevel: CommandSecurityLevel.SAFE, description: 'Output the last part of files' }); commands.push({ command: 'wc', securityLevel: CommandSecurityLevel.SAFE, description: 'Print newline, word, and byte counts' }); commands.push({ command: 'mv', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Move (rename) files' }); commands.push({ command: 'cp', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Copy files and directories' }); commands.push({ command: 'mkdir', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Create directories' }); commands.push({ command: 'touch', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file timestamps or create empty files' }); commands.push({ command: 'chmod', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file mode bits' }); commands.push({ command: 'chown', securityLevel: CommandSecurityLevel.REQUIRES_APPROVAL, description: 'Change file owner and group' }); commands.push({ command: 'rm', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Remove files or directories' }); commands.push({ command: 'sudo', securityLevel: CommandSecurityLevel.FORBIDDEN, description: 'Execute a command as another user' }); } commands.forEach(entry => { this.whitelist.set(entry.command, entry); }); } addToWhitelist(entry) { this.whitelist.set(entry.command, entry); } removeFromWhitelist(command) { this.whitelist.delete(command); } updateSecurityLevel(command, securityLevel) { const entry = this.whitelist.get(command); if (entry) { entry.securityLevel = securityLevel; this.whitelist.set(command, entry); } } getWhitelist() { return Array.from(this.whitelist.values()); } getPendingCommands() { return Array.from(this.pendingCommands.values()); } validateCommand(command, args) { const baseCommand = path.basename(command); const entry = this.whitelist.get(baseCommand); if (!entry) { return null; } if (entry.securityLevel === CommandSecurityLevel.FORBIDDEN) { return CommandSecurityLevel.FORBIDDEN; } if (entry.allowedArgs && entry.allowedArgs.length > 0) { const allArgsValid = args.every((arg, index) => { if (index >= (entry.allowedArgs?.length || 0)) { return false; } const pattern = entry.allowedArgs?.[index]; if (!pattern) { return false; } if (typeof pattern === 'string') { return arg === pattern; } else { return pattern.test(arg); } }); if (!allArgsValid) { return CommandSecurityLevel.REQUIRES_APPROVAL; } } return entry.securityLevel; } async executeCommand(command, args = [], options = {}) { const securityLevel = this.validateCommand(command, args); if (securityLevel === null) { throw new Error(`Command not whitelisted: ${command}`); } if (securityLevel === CommandSecurityLevel.FORBIDDEN) { throw new Error(`Command is forbidden: ${command}`); } if (securityLevel === CommandSecurityLevel.REQUIRES_APPROVAL) { return this.queueCommandForApproval(command, args, options.requestedBy); } try { const timeout = options.timeout || this.defaultTimeout; const execFileAsync = promisify(execFile); const { stdout, stderr } = await execFileAsync(command, args, { timeout, shell: this.shell }); return { stdout, stderr }; } catch (error) { if (error instanceof Error) { throw new Error(`Command execution failed: ${error.message}`); } throw error; } } queueCommandForApproval(command, args = [], requestedBy) { return new Promise((resolve, reject) => { const id = randomUUID(); const pendingCommand = { id, command, args, requestedAt: new Date(), requestedBy, resolve: (result) => resolve(result), reject: (error) => reject(error) }; this.pendingCommands.set(id, pendingCommand); this.emit('command:pending', pendingCommand); setTimeout(() => { if (this.pendingCommands.has(id)) { this.emit('command:approval_timeout', { commandId: id, 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.' }); } }, 5000); }); } queueCommandForApprovalNonBlocking(command, args = [], requestedBy) { const id = randomUUID(); const pendingCommand = { id, command, args, requestedAt: new Date(), requestedBy, resolve: () => {}, reject: () => {} }; this.pendingCommands.set(id, pendingCommand); this.emit('command:pending', pendingCommand); setTimeout(() => { if (this.pendingCommands.has(id)) { this.emit('command:approval_timeout', { commandId: id, 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.' }); } }, 5000); return id; } async approveCommand(commandId) { const pendingCommand = this.pendingCommands.get(commandId); if (!pendingCommand) { throw new Error(`No pending command with ID: ${commandId}`); } try { const execFileAsync = promisify(execFile); const { stdout, stderr } = await execFileAsync( pendingCommand.command, pendingCommand.args, { shell: this.shell } ); this.pendingCommands.delete(commandId); this.emit('command:approved', { commandId, stdout, stderr }); pendingCommand.resolve({ stdout, stderr }); return { stdout, stderr }; } catch (error) { this.pendingCommands.delete(commandId); this.emit('command:failed', { commandId, error }); if (error instanceof Error) { pendingCommand.reject(error); throw error; } const genericError = new Error('Command execution failed'); pendingCommand.reject(genericError); throw genericError; } } denyCommand(commandId, reason = 'Command denied') { const pendingCommand = this.pendingCommands.get(commandId); if (!pendingCommand) { throw new Error(`No pending command with ID: ${commandId}`); } this.pendingCommands.delete(commandId); this.emit('command:denied', { commandId, reason }); pendingCommand.reject(new Error(reason)); } } // Export the mocked modules module.exports = { CommandService, CommandSecurityLevel, detectPlatform, PlatformType, getDefaultShell, validateShellPath, getShellSuggestions, getCommonShellLocations, getShellConfigurationHelp }; ``` -------------------------------------------------------------------------------- /src/services/command-service.ts: -------------------------------------------------------------------------------- ```typescript import { execFile } from 'child_process'; import { promisify } from 'util'; import { randomUUID } from 'crypto'; import { EventEmitter } from 'events'; import * as path from 'path'; import { getDefaultShell, validateShellPath, getShellConfigurationHelp } from '../utils/platform-utils.js'; import { getPlatformSpecificCommands } from '../utils/command-whitelist-utils.js'; const execFileAsync = promisify(execFile); /** * Command security level classification */ export enum CommandSecurityLevel { /** Safe commands that can be executed without approval */ SAFE = 'safe', /** Commands that require approval before execution */ REQUIRES_APPROVAL = 'requires_approval', /** Commands that are explicitly forbidden */ FORBIDDEN = 'forbidden' } /** * Command whitelist entry */ export interface CommandWhitelistEntry { /** The command path or name */ command: string; /** Security level of the command */ securityLevel: CommandSecurityLevel; /** Allowed arguments (string for exact match, RegExp for pattern match) */ allowedArgs?: Array<string | RegExp>; /** Description of the command for documentation */ description?: string; } /** * Pending command awaiting approval */ export interface PendingCommand { /** Unique ID for the command */ id: string; /** The command to execute */ command: string; /** Arguments for the command */ args: string[]; /** When the command was requested */ requestedAt: Date; /** Who requested the command */ requestedBy?: string; /** Resolve function to call when approved */ resolve: (value: { stdout: string; stderr: string }) => void; /** Reject function to call when denied */ reject: (reason: Error) => void; } /** * Result of command execution */ export interface CommandResult { /** Standard output from the command */ stdout: string; /** Standard error from the command */ stderr: string; } /** * Service for securely executing shell commands */ export class CommandService extends EventEmitter { /** Shell to use for commands */ private shell: string; /** Command whitelist */ private whitelist: Map<string, CommandWhitelistEntry>; /** Pending commands awaiting approval */ private pendingCommands: Map<string, PendingCommand>; /** Default timeout for command execution in milliseconds */ private defaultTimeout: number; /** * Create a new CommandService * @param shell The shell to use for commands (default: auto-detected based on platform) * @param defaultTimeout Default timeout for command execution in milliseconds (default: 30000) */ constructor(shell?: string, defaultTimeout = 30000) { super(); this.shell = shell || getDefaultShell(); this.whitelist = new Map(); this.pendingCommands = new Map(); this.defaultTimeout = defaultTimeout; // Initialize with platform-specific commands this.initializeDefaultWhitelist(); } /** * Get the current shell being used * @returns The shell path */ public getShell(): string { return this.shell; } /** * Initialize the default command whitelist based on the current platform */ private initializeDefaultWhitelist(): void { // Get platform-specific commands const platformCommands = getPlatformSpecificCommands(); // Add all commands to the whitelist platformCommands.forEach(entry => { this.whitelist.set(entry.command, entry); }); } /** * Add a command to the whitelist * @param entry The command whitelist entry */ public addToWhitelist(entry: CommandWhitelistEntry): void { this.whitelist.set(entry.command, entry); } /** * Remove a command from the whitelist * @param command The command to remove */ public removeFromWhitelist(command: string): void { this.whitelist.delete(command); } /** * Update a command's security level * @param command The command to update * @param securityLevel The new security level */ public updateSecurityLevel(command: string, securityLevel: CommandSecurityLevel): void { const entry = this.whitelist.get(command); if (entry) { entry.securityLevel = securityLevel; this.whitelist.set(command, entry); } } /** * Get all whitelisted commands * @returns Array of command whitelist entries */ public getWhitelist(): CommandWhitelistEntry[] { return Array.from(this.whitelist.values()); } /** * Get all pending commands awaiting approval * @returns Array of pending commands */ public getPendingCommands(): PendingCommand[] { return Array.from(this.pendingCommands.values()); } /** * Validate if a command and its arguments are allowed * @param command The command to validate * @param args The command arguments * @returns The security level of the command or null if not whitelisted */ private validateCommand(command: string, args: string[]): CommandSecurityLevel | null { // Extract the base command (without path) using path.basename const baseCommand = path.basename(command); // Check if the command is in the whitelist const entry = this.whitelist.get(baseCommand); if (!entry) { return null; } // If the command is forbidden, return immediately if (entry.securityLevel === CommandSecurityLevel.FORBIDDEN) { return CommandSecurityLevel.FORBIDDEN; } // If there are allowed arguments defined, validate them if (entry.allowedArgs && entry.allowedArgs.length > 0) { // Check if all arguments are allowed const allArgsValid = args.every((arg, index) => { // If we have more args than allowed patterns, reject if (index >= (entry.allowedArgs?.length || 0)) { return false; } const pattern = entry.allowedArgs?.[index]; if (!pattern) { return false; } // Check if the argument matches the pattern if (typeof pattern === 'string') { return arg === pattern; } else { return pattern.test(arg); } }); if (!allArgsValid) { return CommandSecurityLevel.REQUIRES_APPROVAL; } } return entry.securityLevel; } /** * Execute a shell command * @param command The command to execute * @param args Command arguments * @param options Additional options * @returns Promise resolving to command output */ public async executeCommand( command: string, args: string[] = [], options: { timeout?: number; requestedBy?: string; } = {} ): Promise<CommandResult> { const securityLevel = this.validateCommand(command, args); // If command is not whitelisted, reject if (securityLevel === null) { throw new Error(`Command not whitelisted: ${command}`); } // If command is forbidden, reject if (securityLevel === CommandSecurityLevel.FORBIDDEN) { throw new Error(`Command is forbidden: ${command}`); } // If command requires approval, add to pending queue if (securityLevel === CommandSecurityLevel.REQUIRES_APPROVAL) { return this.queueCommandForApproval(command, args, options.requestedBy); } // For safe commands, execute immediately try { const timeout = options.timeout || this.defaultTimeout; const { stdout, stderr } = await execFileAsync(command, args, { timeout, shell: this.shell }); return { stdout, stderr }; } catch (error) { if (error instanceof Error) { throw new Error(`Command execution failed: ${error.message}`); } throw error; } } /** * Queue a command for approval * @param command The command to queue * @param args Command arguments * @param requestedBy Who requested the command * @returns Promise resolving when command is approved and executed */ private queueCommandForApproval( command: string, args: string[] = [], requestedBy?: string ): Promise<CommandResult> { return new Promise((resolve, reject) => { const id = randomUUID(); const pendingCommand: PendingCommand = { id, command, args, requestedAt: new Date(), requestedBy, resolve: (result: CommandResult) => resolve(result), reject: (error: Error) => reject(error) }; this.pendingCommands.set(id, pendingCommand); // Emit event for pending command this.emit('command:pending', pendingCommand); // Set a timeout to check if the command is still pending after a while // This helps detect if the UI approval didn't properly trigger the approveCommand method setTimeout(() => { // If the command is still pending after the timeout if (this.pendingCommands.has(id)) { // Emit a warning event that can be handled by the client this.emit('command:approval_timeout', { commandId: id, 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.' }); } }, 5000); // 5 second timeout to detect UI approval issues }); } /** * Queue a command for approval without waiting for the Promise to resolve * @param command The command to queue * @param args Command arguments * @param requestedBy Who requested the command * @returns The ID of the queued command */ public queueCommandForApprovalNonBlocking( command: string, args: string[] = [], requestedBy?: string ): string { const id = randomUUID(); const pendingCommand: PendingCommand = { id, command, args, requestedAt: new Date(), requestedBy, resolve: () => {}, // No-op resolve function reject: () => {} // No-op reject function }; this.pendingCommands.set(id, pendingCommand); // Emit event for pending command this.emit('command:pending', pendingCommand); // Set a timeout to check if the command is still pending after a while setTimeout(() => { // If the command is still pending after the timeout if (this.pendingCommands.has(id)) { // Emit a warning event that can be handled by the client this.emit('command:approval_timeout', { commandId: id, 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.' }); } }, 5000); // 5 second timeout to detect UI approval issues return id; } /** * Approve a pending command * @param commandId ID of the command to approve * @returns Promise resolving to command output */ public async approveCommand(commandId: string): Promise<CommandResult> { const pendingCommand = this.pendingCommands.get(commandId); if (!pendingCommand) { throw new Error(`No pending command with ID: ${commandId}`); } try { const { stdout, stderr } = await execFileAsync( pendingCommand.command, pendingCommand.args, { shell: this.shell } ); // Remove from pending queue this.pendingCommands.delete(commandId); // Emit event for approved command this.emit('command:approved', { commandId, stdout, stderr }); // Resolve the original promise pendingCommand.resolve({ stdout, stderr }); return { stdout, stderr }; } catch (error) { // Remove from pending queue this.pendingCommands.delete(commandId); // Emit event for failed command this.emit('command:failed', { commandId, error }); if (error instanceof Error) { // Reject the original promise pendingCommand.reject(error); throw error; } const genericError = new Error('Command execution failed'); pendingCommand.reject(genericError); throw genericError; } } /** * Deny a pending command * @param commandId ID of the command to deny * @param reason Reason for denial */ public denyCommand(commandId: string, reason: string = 'Command denied'): void { const pendingCommand = this.pendingCommands.get(commandId); if (!pendingCommand) { throw new Error(`No pending command with ID: ${commandId}`); } // Remove from pending queue this.pendingCommands.delete(commandId); // Emit event for denied command this.emit('command:denied', { commandId, reason }); // Reject the original promise pendingCommand.reject(new Error(reason)); } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import * as path from 'path'; import * as fs from 'fs'; import { execFile } from 'child_process'; import { promisify } from 'util'; import { randomUUID } from 'crypto'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { CommandService, CommandSecurityLevel } from './services/command-service.js'; import { getLogger, Logger } from './utils/logger.js'; // In ESM, __dirname is not available directly, so we create it const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const execFileAsync = promisify(execFile); // Initialize the logger // Use __dirname to get the directory of the current file const LOG_FILE = path.join(__dirname, '../logs/super-shell-mcp.log'); console.error(`Log file path: ${LOG_FILE}`); const logger = getLogger(LOG_FILE, true); /** * SuperShellMcpServer - MCP server for executing shell commands across multiple platforms */ class SuperShellMcpServer { private server: Server; private commandService: CommandService; private pendingApprovals: Map<string, { command: string; args: string[] }>; constructor(options?: { shell?: string }) { // Initialize the command service with auto-detected or specified shell this.commandService = new CommandService(options?.shell); this.pendingApprovals = new Map(); // Initialize the MCP server this.server = new Server( { name: 'super-shell-mcp', version: '2.0.13', }, { capabilities: { tools: {}, }, } ); // Set up event handlers for command service this.setupCommandServiceEvents(); // Set up MCP request handlers this.setupRequestHandlers(); // Error handling this.server.onerror = (error) => { logger.error(`[MCP Error] ${error}`); console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { logger.info('Received SIGINT signal, shutting down'); await this.server.close(); logger.info('Server closed, exiting process'); logger.close(); process.exit(0); }); } /** * Set up event handlers for the command service */ private setupCommandServiceEvents(): void { this.commandService.on('command:pending', (pendingCommand) => { logger.info(`[Pending Command] ID: ${pendingCommand.id}, Command: ${pendingCommand.command} ${pendingCommand.args.join(' ')}`); console.error(`[Pending Command] ID: ${pendingCommand.id}, Command: ${pendingCommand.command} ${pendingCommand.args.join(' ')}`); this.pendingApprovals.set(pendingCommand.id, { command: pendingCommand.command, args: pendingCommand.args, }); }); this.commandService.on('command:approved', (data) => { logger.info(`[Approved Command] ID: ${data.commandId}`); console.error(`[Approved Command] ID: ${data.commandId}`); this.pendingApprovals.delete(data.commandId); }); this.commandService.on('command:denied', (data) => { logger.info(`[Denied Command] ID: ${data.commandId}, Reason: ${data.reason}`); console.error(`[Denied Command] ID: ${data.commandId}, Reason: ${data.reason}`); this.pendingApprovals.delete(data.commandId); }); this.commandService.on('command:failed', (data) => { logger.error(`[Failed Command] ID: ${data.commandId}, Error: ${data.error.message}`); console.error(`[Failed Command] ID: ${data.commandId}, Error: ${data.error.message}`); this.pendingApprovals.delete(data.commandId); }); // Handle approval timeout events this.commandService.on('command:approval_timeout', (data) => { logger.error(`[Approval Timeout] ID: ${data.commandId}, Message: ${data.message}`); console.error(`[Approval Timeout] ID: ${data.commandId}, Message: ${data.message}`); // Log the timeout but keep the command in the pending queue // The AI assistant will need to use get_pending_commands and approve_command to proceed }); } /** * Set up MCP request handlers */ private setupRequestHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get_platform_info', description: 'Get information about the current platform and shell', inputSchema: { type: 'object', properties: {}, }, }, { name: 'execute_command', description: 'Execute a shell command on the current platform', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to execute', }, args: { type: 'array', items: { type: 'string', }, description: 'Command arguments', }, }, required: ['command'], }, }, { name: 'get_whitelist', description: 'Get the list of whitelisted commands', inputSchema: { type: 'object', properties: {}, }, }, { name: 'add_to_whitelist', description: 'Add a command to the whitelist', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to whitelist', }, securityLevel: { type: 'string', enum: ['safe', 'requires_approval', 'forbidden'], description: 'Security level for the command', }, description: { type: 'string', description: 'Description of the command', }, }, required: ['command', 'securityLevel'], }, }, { name: 'update_security_level', description: 'Update the security level of a whitelisted command', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to update', }, securityLevel: { type: 'string', enum: ['safe', 'requires_approval', 'forbidden'], description: 'New security level for the command', }, }, required: ['command', 'securityLevel'], }, }, { name: 'remove_from_whitelist', description: 'Remove a command from the whitelist', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to remove from whitelist', }, }, required: ['command'], }, }, { name: 'get_pending_commands', description: 'Get the list of commands pending approval', inputSchema: { type: 'object', properties: {}, }, }, { name: 'approve_command', description: 'Approve a pending command', inputSchema: { type: 'object', properties: { commandId: { type: 'string', description: 'ID of the command to approve', }, }, required: ['commandId'], }, }, { name: 'deny_command', description: 'Deny a pending command', inputSchema: { type: 'object', properties: { commandId: { type: 'string', description: 'ID of the command to deny', }, reason: { type: 'string', description: 'Reason for denial', }, }, required: ['commandId'], }, }, ], })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_platform_info': return await this.handleGetPlatformInfo(); case 'execute_command': return await this.handleExecuteCommand(args); case 'get_whitelist': return await this.handleGetWhitelist(); case 'add_to_whitelist': return await this.handleAddToWhitelist(args); case 'update_security_level': return await this.handleUpdateSecurityLevel(args); case 'remove_from_whitelist': return await this.handleRemoveFromWhitelist(args); case 'get_pending_commands': return await this.handleGetPendingCommands(); case 'approve_command': return await this.handleApproveCommand(args); case 'deny_command': return await this.handleDenyCommand(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } if (error instanceof Error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], isError: true, }; } throw new McpError( ErrorCode.InternalError, 'An unexpected error occurred' ); } }); } /** * Handle execute_command tool */ private async handleExecuteCommand(args: any) { const schema = z.object({ command: z.string(), args: z.array(z.string()).optional(), }); // Log the start of command execution logger.debug(`handleExecuteCommand called with args: ${JSON.stringify(args)}`); const { command, args: commandArgs = [] } = schema.parse(args); // Extract the base command (without path) const baseCommand = path.basename(command); logger.debug(`[Executing Command] Command: ${command} ${commandArgs.join(' ')}`); logger.debug(`Base command: ${baseCommand}`); // Check if the command requires approval before attempting execution const whitelist = this.commandService.getWhitelist(); logger.debug(`Whitelist entries: ${whitelist.length}`); const whitelistEntry = whitelist.find(entry => entry.command === baseCommand); logger.debug(`Whitelist entry found: ${whitelistEntry ? 'yes' : 'no'}`); if (whitelistEntry) { logger.debug(`Security level: ${whitelistEntry.securityLevel}`); } if (whitelistEntry && whitelistEntry.securityLevel === CommandSecurityLevel.REQUIRES_APPROVAL) { logger.debug(`[Command Requires Approval] Command: ${command} ${commandArgs.join(' ')}`); // Use the non-blocking method to queue the command for approval const commandId = this.commandService.queueCommandForApprovalNonBlocking(command, commandArgs); logger.debug(`Command queued for approval with ID: ${commandId}`); // Return immediately with instructions for approval logger.debug(`Returning response to client`); return { content: [ { type: 'text', 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.`, }, ], isError: false, // Not an error, just needs approval }; } // For safe commands or forbidden commands, use the normal execution path try { // Use the CommandService's executeCommand method const result = await this.commandService.executeCommand(command, commandArgs); return { content: [ { type: 'text', text: result.stdout, }, { type: 'text', text: result.stderr ? `Error output: ${result.stderr}` : '', }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; console.error(`[Command Execution Failed] Error: ${errorMessage}`); return { content: [ { type: 'text', text: `Command execution failed: ${errorMessage}`, }, ], isError: true, }; } } /** * Handle get_whitelist tool */ private async handleGetWhitelist() { const whitelist = this.commandService.getWhitelist(); return { content: [ { type: 'text', text: JSON.stringify(whitelist, null, 2), }, ], }; } /** * Handle add_to_whitelist tool */ private async handleAddToWhitelist(args: any) { const schema = z.object({ command: z.string(), securityLevel: z.enum(['safe', 'requires_approval', 'forbidden']), description: z.string().optional(), }); const { command, securityLevel, description } = schema.parse(args); // Map string security level to enum const securityLevelEnum = securityLevel === 'safe' ? CommandSecurityLevel.SAFE : securityLevel === 'requires_approval' ? CommandSecurityLevel.REQUIRES_APPROVAL : CommandSecurityLevel.FORBIDDEN; this.commandService.addToWhitelist({ command, securityLevel: securityLevelEnum, description, }); return { content: [ { type: 'text', text: `Command '${command}' added to whitelist with security level '${securityLevel}'`, }, ], }; } /** * Handle update_security_level tool */ private async handleUpdateSecurityLevel(args: any) { const schema = z.object({ command: z.string(), securityLevel: z.enum(['safe', 'requires_approval', 'forbidden']), }); const { command, securityLevel } = schema.parse(args); // Map string security level to enum const securityLevelEnum = securityLevel === 'safe' ? CommandSecurityLevel.SAFE : securityLevel === 'requires_approval' ? CommandSecurityLevel.REQUIRES_APPROVAL : CommandSecurityLevel.FORBIDDEN; this.commandService.updateSecurityLevel(command, securityLevelEnum); return { content: [ { type: 'text', text: `Security level for command '${command}' updated to '${securityLevel}'`, }, ], }; } /** * Handle remove_from_whitelist tool */ private async handleRemoveFromWhitelist(args: any) { const schema = z.object({ command: z.string(), }); const { command } = schema.parse(args); this.commandService.removeFromWhitelist(command); return { content: [ { type: 'text', text: `Command '${command}' removed from whitelist`, }, ], }; } /** * Handle get_platform_info tool */ private async handleGetPlatformInfo() { const { detectPlatform, getDefaultShell, getShellSuggestions, getCommonShellLocations } = await import('./utils/platform-utils.js'); const platform = detectPlatform(); const currentShell = this.commandService.getShell(); const suggestedShells = getShellSuggestions()[platform]; const commonLocations = getCommonShellLocations(); return { content: [ { type: 'text', text: JSON.stringify({ platform, currentShell, suggestedShells, commonLocations, helpMessage: `Super Shell MCP is running on ${platform} using ${currentShell}` }, null, 2), }, ], }; } /** * Handle get_pending_commands tool */ private async handleGetPendingCommands() { const pendingCommands = this.commandService.getPendingCommands(); return { content: [ { type: 'text', text: JSON.stringify(pendingCommands.map(cmd => ({ id: cmd.id, command: cmd.command, args: cmd.args, requestedAt: cmd.requestedAt, requestedBy: cmd.requestedBy, })), null, 2), }, ], }; } /** * Handle approve_command tool */ private async handleApproveCommand(args: any) { const schema = z.object({ commandId: z.string(), }); logger.debug(`handleApproveCommand called with args: ${JSON.stringify(args)}`); const { commandId } = schema.parse(args); // Log the approval attempt logger.debug(`[Approval Attempt] ID: ${commandId}`); // Check if the command exists in our local pending approvals map const localPending = this.pendingApprovals.has(commandId); logger.debug(`Command found in local pendingApprovals: ${localPending ? 'yes' : 'no'}`); // Check if the command exists in the CommandService's pending queue const pendingCommands = this.commandService.getPendingCommands(); logger.debug(`CommandService pending commands: ${pendingCommands.length}`); const pendingCommand = pendingCommands.find(cmd => cmd.id === commandId); logger.debug(`Command found in CommandService pending queue: ${pendingCommand ? 'yes' : 'no'}`); if (pendingCommand) { logger.debug(`Pending command details: ${JSON.stringify({ id: pendingCommand.id, command: pendingCommand.command, args: pendingCommand.args, requestedAt: pendingCommand.requestedAt })}`); } try { logger.debug(`Calling CommandService.approveCommand with ID: ${commandId}`); // Use the CommandService's approveCommand method directly const result = await this.commandService.approveCommand(commandId); logger.debug(`[Command Approved] ID: ${commandId}, Output length: ${result.stdout.length}`); logger.debug(`Command output: ${result.stdout.substring(0, 100)}${result.stdout.length > 100 ? '...' : ''}`); return { content: [ { type: 'text', text: `Command approved and executed successfully.\nOutput: ${result.stdout}`, }, { type: 'text', text: result.stderr ? `Error output: ${result.stderr}` : '', }, ], }; } catch (error) { logger.error(`[Approval Error] ID: ${commandId}, Error: ${error instanceof Error ? error.message : 'Unknown error'}`); if (error instanceof Error) { return { content: [ { type: 'text', text: `Command approval failed: ${error.message}`, }, ], isError: true, }; } throw error; } } /** * Handle deny_command tool */ private async handleDenyCommand(args: any) { const schema = z.object({ commandId: z.string(), reason: z.string().optional(), }); logger.debug(`handleDenyCommand called with args: ${JSON.stringify(args)}`); const { commandId, reason } = schema.parse(args); logger.debug(`[Denial Attempt] ID: ${commandId}, Reason: ${reason || 'none provided'}`); try { this.commandService.denyCommand(commandId, reason); logger.info(`Command denied: ID: ${commandId}, Reason: ${reason || 'none provided'}`); return { content: [ { type: 'text', text: `Command denied${reason ? `: ${reason}` : ''}`, }, ], }; } catch (error) { logger.error(`[Denial Error] ID: ${commandId}, Error: ${error instanceof Error ? error.message : 'Unknown error'}`); if (error instanceof Error) { return { content: [ { type: 'text', text: `Command denial failed: ${error.message}`, }, ], isError: true, }; } throw error; } } /** * Run the MCP server */ async run() { logger.info('Starting Super Shell MCP server'); const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('Super Shell MCP server running on stdio'); console.error('Super Shell MCP server running on stdio'); console.error(`Log file: ${LOG_FILE}`); logger.info(`Log file: ${LOG_FILE}`); } } // Create and run the server const server = new SuperShellMcpServer(); server.run().catch(console.error); ```