# Directory Structure ``` ├── .github │ └── workflows │ └── publish.yml ├── .gitignore ├── .npmignore ├── bin │ └── mcp-ssh.js ├── CHANGELOG.md ├── claude_desktop_config.json ├── CLAUDE.md ├── doc │ ├── Claude.png │ └── example.png ├── gist_comment.json ├── github_issue_response.md ├── IMPLEMENTATION_NOTES.md ├── LICENSE ├── manifest.json ├── package-lock.json ├── package.json ├── PUBLISHING.md ├── README.md ├── scripts │ └── build-dxt.sh ├── server-simple.mjs ├── src │ ├── index.ts │ ├── ssh-client.ts │ ├── ssh-config-parser.ts │ └── types.ts ├── start-silent.sh ├── start.sh ├── temp-extract │ └── manifest.json └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` # Development files src/ tsconfig.json .vscode/ IMPLEMENTATION_NOTES.md # Documentation images (keep README.md) doc/ # Development scripts start.sh start-silent.sh # Git and CI/CD .git/ .github/ .gitignore *.log node_modules/ # Generated files *.tgz # Keep these files for npm package !package.json !README.md !server-simple.mjs !LICENSE !CHANGELOG.md ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Coverage directory used by tools like istanbul coverage/ *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories jspm_packages/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test .env.production .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ public # Storybook build outputs .out .storybook-out # Temporary folders tmp/ temp/ # Logs logs *.log # Runtime data pids *.pid *.seed *.pid.lock # OS generated files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # IDE files .vscode/ .idea/ *.swp *.swo *~ # Build outputs dist/ build/ # DXT build artifacts *.dxt releases/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP SSH Agent A Model Context Protocol (MCP) server for managing and controlling SSH connections. This server integrates seamlessly with Claude Desktop and other MCP-compatible clients to provide AI-powered SSH operations. ## Overview This MCP server provides SSH operations through a clean, standardized interface that can be used by MCP-compatible language models like Claude Desktop. The server automatically discovers SSH hosts from your `~/.ssh/config` and `~/.ssh/known_hosts` files and executes commands using native SSH tools for maximum reliability. ## Quick Start ### Desktop Extension Installation (Recommended) The easiest way to install MCP SSH Agent is through the Desktop Extension (.dxt) format: 1. Download the latest `mcp-ssh-*.dxt` file from the [GitHub releases page](https://github.com/aiondadotcom/mcp-ssh/releases) 2. Double-click the `.dxt` file to install it in Claude Desktop 3. The SSH tools will be automatically available in your conversations with Claude ### Alternative Installation Methods #### Installation via npx ```bash npx @aiondadotcom/mcp-ssh ``` #### Manual Claude Desktop Configuration To use this MCP server with Claude Desktop using manual configuration, add the following to your MCP settings file: **On macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` **On Windows**: `%APPDATA%/Claude/claude_desktop_config.json` ```json { "mcpServers": { "mcp-ssh": { "command": "npx", "args": ["@aiondadotcom/mcp-ssh"] } } } ``` After adding this configuration, restart Claude Desktop. The SSH tools will be available for use in your conversations with Claude. #### Global Installation ```bash npm install -g @aiondadotcom/mcp-ssh ``` #### Local Development ```bash git clone https://github.com/aiondadotcom/mcp-ssh.git cd mcp-ssh npm install npm start ``` ## Example Usage  The screenshot above shows the MCP SSH Agent in action, demonstrating how it integrates with MCP-compatible clients to provide seamless SSH operations. ### Integration with Claude  This screenshot demonstrates the MCP SSH Agent integrated with Claude, showing how the AI assistant can directly manage SSH connections and execute remote commands through the MCP protocol. ## Key Features - **Reliable SSH**: Uses native `ssh`/`scp` commands instead of JavaScript SSH libraries - **Automatic Discovery**: Finds hosts from SSH config and known_hosts files - **Full SSH Support**: Works with SSH agents, keys, and all authentication methods - **File Operations**: Upload and download files using `scp` - **Batch Commands**: Execute multiple commands in sequence - **Error Handling**: Comprehensive error reporting with timeouts ## Functions The agent provides the following MCP tools: 1. **listKnownHosts()** - Lists all known SSH hosts, prioritizing entries from ~/.ssh/config first, then additional hosts from ~/.ssh/known_hosts 2. **runRemoteCommand(hostAlias, command)** - Executes a command on a remote host using `ssh` 3. **getHostInfo(hostAlias)** - Returns detailed configuration for a specific host 4. **checkConnectivity(hostAlias)** - Tests SSH connectivity to a host 5. **uploadFile(hostAlias, localPath, remotePath)** - Uploads a file to the remote host using `scp` 6. **downloadFile(hostAlias, remotePath, localPath)** - Downloads a file from the remote host using `scp` 7. **runCommandBatch(hostAlias, commands)** - Executes multiple commands sequentially ## Configuration Examples ### Claude Desktop Integration Here's how your Claude Desktop configuration should look: ```json { "mcpServers": { "mcp-ssh": { "command": "npx", "args": ["@aiondadotcom/mcp-ssh"] } } } ``` ### Manual Server Configuration If you prefer to run the server manually or integrate it with other MCP clients: ```json { "servers": { "mcp-ssh": { "command": "npx", "args": ["@aiondadotcom/mcp-ssh"] } } } ``` ## Requirements - Node.js 18 or higher - SSH client installed (`ssh` and `scp` commands available) - SSH configuration files (`~/.ssh/config` and `~/.ssh/known_hosts`) ## Usage with Claude Desktop Once configured, you can ask Claude to help you with SSH operations like: - "List all my SSH hosts" - "Check connectivity to my production server" - "Run a command on my web server" - "Upload this file to my remote server" - "Download logs from my application server" Claude will use the MCP SSH tools to perform these operations safely and efficiently. ## Usage The agent runs as a Model Context Protocol server over STDIO. When installed via npm, you can use it directly: ```bash # Run via npx (recommended) npx @aiondadotcom/mcp-ssh # Or if installed globally mcp-ssh # For development - run with debug output npm start ``` The server communicates via clean JSON over STDIO, making it perfect for MCP clients like Claude Desktop. ## Advanced Configuration ### Environment Variables - `MCP_SILENT=true` - Disable debug output (automatically set when used as MCP server) ### SSH Configuration The agent reads from standard SSH configuration files: - `~/.ssh/config` - SSH client configuration (supports Include directives) - `~/.ssh/known_hosts` - Known host keys Make sure your SSH keys are properly configured and accessible via SSH agent or key files. #### Include Directive Support The MCP SSH Agent fully supports SSH `Include` directives to organize your configuration across multiple files. However, there's an important SSH bug to be aware of: **⚠️ SSH Include Directive Bug Warning** SSH has a configuration parsing bug where `Include` statements **must be placed at the beginning** of your `~/.ssh/config` file to work correctly. If placed at the end, SSH will read them but won't properly apply the included configurations. **✅ Correct placement (at the beginning):** ```ssh-config # ~/.ssh/config Include ~/.ssh/config.d/* Include ~/.ssh/work-hosts # Global settings ServerAliveInterval 55 # Host definitions Host myserver HostName example.com ``` **❌ Incorrect placement (at the end) - won't work:** ```ssh-config # ~/.ssh/config # Global settings ServerAliveInterval 55 # Host definitions Host myserver HostName example.com # These Include statements won't work properly due to SSH bug: Include ~/.ssh/config.d/* Include ~/.ssh/work-hosts ``` The MCP SSH Agent correctly processes `Include` directives regardless of their placement in the file, so you'll get full host discovery even if SSH itself has issues with your configuration. #### Example ~/.ssh/config Here's an example SSH configuration file that demonstrates various connection scenarios including Include directives: ```ssh-config # Include directives must be at the beginning due to SSH bug Include ~/.ssh/config.d/* Include ~/.ssh/work-servers # Global settings - keep connections alive ServerAliveInterval 55 # Production server with jump host Host prod Hostname 203.0.113.10 Port 22022 User deploy IdentityFile ~/.ssh/id_prod_rsa # Root access to production (separate entry) Host root@prod Hostname 203.0.113.10 Port 22022 User root IdentityFile ~/.ssh/id_prod_rsa # Archive server accessed through production jump host Host archive Hostname 2001:db8:1f0:cafe::1 Port 22077 User archive-user ProxyJump prod # Web servers with specific configurations Host web1.example.com Hostname 198.51.100.15 Port 22022 User root IdentityFile ~/.ssh/id_ed25519 Host web2.example.com Hostname 198.51.100.25 Port 22022 User root IdentityFile ~/.ssh/id_ed25519 # Database server with custom key Host database Hostname 203.0.113.50 Port 22077 User dbadmin IdentityFile ~/.ssh/id_database_rsa IdentitiesOnly yes # Mail servers Host mail1 Hostname 198.51.100.88 Port 22078 User mailuser Host root@mail1 Hostname 198.51.100.88 Port 22078 User root # Monitoring server Host monitor Hostname 203.0.113.100 Port 22077 User monitoring IdentityFile ~/.ssh/id_monitor_ed25519 IdentitiesOnly yes # Load balancers Host lb-a Hostname 198.51.100.200 Port 22077 User root Host lb-b Hostname 198.51.100.201 Port 22077 User root ``` This configuration demonstrates: - **Global settings**: `ServerAliveInterval` to keep connections alive - **Custom ports**: Non-standard SSH ports for security - **Multiple users**: Different user accounts for the same host (e.g., `prod` and `root@prod`) - **Jump hosts**: Using `ProxyJump` to access servers through bastion hosts - **IPv6 addresses**: Modern networking support - **Identity files**: Specific SSH keys for different servers - **Security options**: `IdentitiesOnly yes` to use only specified keys #### How MCP SSH Agent Uses Your Configuration The MCP SSH agent automatically discovers and uses your SSH configuration: 1. **Host Discovery**: All hosts from `~/.ssh/config` are automatically available 2. **Native SSH**: Uses your system's `ssh` command, so all config options work 3. **Authentication**: Respects your SSH agent, key files, and authentication settings 4. **Jump Hosts**: Supports complex proxy chains and bastion host setups 5. **Port Forwarding**: Can work with custom ports and connection options **Example Usage with Claude Desktop:** - "List my SSH hosts" → Shows all configured hosts including `prod`, `archive`, `web1.example.com`, etc. - "Connect to archive server" → Uses the ProxyJump configuration automatically - "Run 'df -h' on web1.example.com" → Connects with the correct user, port, and key - "Upload file to database server" → Uses the specific identity file and port configuration ## Troubleshooting ### Common Issues 1. **Command not found**: Ensure `ssh` and `scp` are installed and in your PATH 2. **Permission denied**: Check SSH key permissions and SSH agent 3. **Host not found**: Verify host exists in `~/.ssh/config` or `~/.ssh/known_hosts` 4. **Connection timeout**: Check network connectivity and firewall settings ### Debug Mode Run with debug output to see detailed operation logs: ```bash # Enable debug mode MCP_SILENT=false npx @aiondadotcom/mcp-ssh ``` ## SSH Key Setup Guide For the MCP SSH Agent to work properly, you need to set up SSH key authentication. Here's a complete guide: ### 1. Creating SSH Keys Generate a new SSH key pair (use Ed25519 for better security): ```bash # Generate Ed25519 key (recommended) ssh-keygen -t ed25519 -C "[email protected]" # Or generate RSA key (if Ed25519 is not supported) ssh-keygen -t rsa -b 4096 -C "[email protected]" ``` **Important**: When prompted for a passphrase, **leave it empty** (press Enter). The MCP SSH Agent cannot handle password-protected keys as it runs non-interactively. ``` Enter passphrase (empty for no passphrase): [Press Enter] Enter same passphrase again: [Press Enter] ``` This creates two files: - `~/.ssh/id_ed25519` (private key) - Keep this secret! - `~/.ssh/id_ed25519.pub` (public key) - This gets copied to servers ### 2. Installing Public Key on Remote Servers Copy your public key to the remote server's authorized_keys file: ```bash # Method 1: Using ssh-copy-id (easiest) ssh-copy-id user@hostname # Method 2: Manual copy cat ~/.ssh/id_ed25519.pub | ssh user@hostname "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" # Method 3: Copy and paste manually cat ~/.ssh/id_ed25519.pub # Then SSH to the server and paste into ~/.ssh/authorized_keys ``` ### 3. Server-Side SSH Configuration To enable secure key-only authentication on your SSH servers, edit `/etc/ssh/sshd_config`: ```bash # Edit SSH daemon configuration sudo nano /etc/ssh/sshd_config ``` Add or modify these settings: ```ssh-config # Enable public key authentication PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys # Disable password authentication (security best practice) PasswordAuthentication no ChallengeResponseAuthentication no UsePAM no # Root login options (choose one): # Option 1: Allow root login with SSH keys only (recommended for admin access) PermitRootLogin prohibit-password # Option 2: Completely disable root login (most secure, but less flexible) # PermitRootLogin no # Optional: Restrict SSH to specific users AllowUsers deploy root admin # Optional: Change default port for security Port 22022 ``` After editing, restart the SSH service: ```bash # On Ubuntu/Debian sudo systemctl restart ssh # On CentOS/RHEL/Fedora sudo systemctl restart sshd # On macOS sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist sudo launchctl load /System/Library/LaunchDaemons/ssh.plist ``` ### 4. Setting Correct Permissions SSH is very strict about file permissions. Set them correctly: **On your local machine:** ```bash chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub chmod 644 ~/.ssh/config chmod 644 ~/.ssh/known_hosts ``` **On the remote server:** ```bash chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys ``` ### 5. Testing SSH Key Authentication Test your connection before using with MCP SSH Agent: ```bash # Test connection ssh -i ~/.ssh/id_ed25519 user@hostname # Test with verbose output for debugging ssh -v -i ~/.ssh/id_ed25519 user@hostname # Test specific configuration ssh -F ~/.ssh/config hostname ``` ### 6. Multiple Keys for Different Servers You can create different keys for different servers: ```bash # Create specific keys ssh-keygen -t ed25519 -f ~/.ssh/id_production -C "production-server" ssh-keygen -t ed25519 -f ~/.ssh/id_staging -C "staging-server" ``` Then configure them in `~/.ssh/config`: ```ssh-config Host production Hostname prod.example.com User deploy IdentityFile ~/.ssh/id_production IdentitiesOnly yes Host staging Hostname staging.example.com User deploy IdentityFile ~/.ssh/id_staging IdentitiesOnly yes ``` ## Security Best Practices ### SSH Key Security - **Never use password-protected keys** with MCP SSH Agent - **Never share private keys** - they should stay on your machine only - **Use Ed25519 keys** when possible (more secure than RSA) - **Create separate keys** for different environments/purposes - **Regularly rotate keys** (every 6-12 months) ### Server Security - **Disable password authentication** completely - **Use non-standard SSH ports** to reduce automated attacks - **Limit SSH access** to specific users with `AllowUsers` - **Choose appropriate root login policy**: - `PermitRootLogin prohibit-password` - Allows root access with SSH keys only (recommended for admin tasks) - `PermitRootLogin no` - Completely disables root login (most secure, but requires sudo access) - **Enable SSH key-only authentication** for all accounts - **Consider using jump hosts** for additional security layers ### Network Security - **Use VPN or bastion hosts** for production servers - **Implement fail2ban** to block brute force attempts - **Monitor SSH logs** regularly - **Use SSH key forwarding carefully** (disable when not needed) ## Building Desktop Extensions For developers who want to build DXT packages locally: ### Prerequisites - Node.js 18 or higher - npm ### Building DXT Package ```bash # Install dependencies npm install # Build the DXT package npm run build:dxt ``` This creates a `.dxt` file in the `build/` directory that can be installed in Claude Desktop. ### Publishing DXT Releases To publish a new DXT release: ```bash # Build the DXT package npm run build:dxt # Create a GitHub release with the DXT file gh release create v1.0.3 build/mcp-ssh-1.0.3.dxt --title "Release v1.0.3" --notes "MCP SSH Agent v1.0.3" ``` The DXT file will be available as a release asset for users to download and install. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License MIT License - see LICENSE file for details. ## Project Structure ``` mcp-ssh/ ├── server-simple.mjs # Main MCP server implementation ├── manifest.json # DXT package manifest ├── package.json # Dependencies and scripts ├── README.md # Documentation ├── LICENSE # MIT License ├── CHANGELOG.md # Release history ├── PUBLISHING.md # Publishing instructions ├── start.sh # Development startup script ├── start-silent.sh # Silent startup script ├── scripts/ │ └── build-dxt.sh # DXT package build script ├── doc/ │ ├── example.png # Usage example screenshot │ └── Claude.png # Claude Desktop integration example ├── src/ # TypeScript source files (development) │ ├── ssh-client.ts # SSH operations implementation │ ├── ssh-config-parser.ts # SSH configuration parsing │ └── types.ts # Type definitions └── tsconfig.json # TypeScript configuration ``` ## About This project is maintained by [aionda.com](https://aionda.com) and provides a reliable bridge between AI assistants and SSH infrastructure through the Model Context Protocol. ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview This is MCP SSH Agent (@aiondadotcom/mcp-ssh) - a Model Context Protocol (MCP) server that provides SSH operations for AI assistants like Claude Desktop. The project uses native SSH commands (`ssh`, `scp`) rather than JavaScript SSH libraries for maximum reliability and compatibility. ## Development Commands ### Basic Operations - `npm start` - Start the MCP server (same as `npm run dev`) - `npm run dev` - Start the MCP server with debug output - `npm run build` - Currently a no-op (echo "Build skipped") - `npm test` - Currently a no-op (echo "No tests specified") ### Development Scripts - `./start.sh` - Start the server with debug output - `./start-silent.sh` - Start the server in silent mode (no debug output) - `node server-simple.mjs` - Direct server execution ### Publishing - `npm version patch|minor|major` - Bump version and create git tag - `npm publish` - Publish to npm (see PUBLISHING.md for details) - `npm pack` - Create tarball for testing ### DXT Package Building - `npm run build:dxt` - Build Desktop Extension (.dxt) package - `./scripts/build-dxt.sh` - Direct build script execution ## Architecture ### Main Entry Point - `server-simple.mjs` - Self-contained MCP server implementation that includes all functionality inline to avoid module resolution issues ### Source Structure (Development) - `src/` - TypeScript source files (currently not compiled/used in production) - `ssh-client.ts` - SSH operations using node-ssh library (development version) - `ssh-config-parser.ts` - SSH config parsing utilities - `types.ts` - TypeScript type definitions - `bin/mcp-ssh.js` - Binary wrapper for npx compatibility ### Key Design Decisions 1. **Native SSH Tools**: Uses system `ssh` and `scp` commands rather than JavaScript SSH libraries for reliability 2. **Self-contained**: `server-simple.mjs` includes all code inline to avoid ESM import issues 3. **Dual Implementation**: TypeScript source in `src/` for development, JavaScript implementation in `server-simple.mjs` for production 4. **Silent Mode**: Controlled by `MCP_SILENT` environment variable to disable debug output when used as MCP server ## SSH Configuration Integration The agent automatically discovers SSH hosts from: - `~/.ssh/config` - Primary source for host configurations - `~/.ssh/known_hosts` - Additional hosts not in config Host discovery prioritizes SSH config entries first, then adds additional hosts from known_hosts. ## MCP Tools Provided 1. **listKnownHosts()** - Lists all discovered SSH hosts 2. **runRemoteCommand(hostAlias, command)** - Execute commands via SSH 3. **getHostInfo(hostAlias)** - Get host configuration details 4. **checkConnectivity(hostAlias)** - Test SSH connectivity 5. **uploadFile(hostAlias, localPath, remotePath)** - Upload files via SCP 6. **downloadFile(hostAlias, remotePath, localPath)** - Download files via SCP 7. **runCommandBatch(hostAlias, commands)** - Execute multiple commands sequentially ## Testing and Debugging ### Manual Testing ```bash # Test as MCP server npx @aiondadotcom/mcp-ssh # Test with debug output MCP_SILENT=false npx @aiondadotcom/mcp-ssh # Test installation npm pack npm install -g ./aiondadotcom-mcp-ssh-*.tgz mcp-ssh ``` ### Integration Testing Configure in Claude Desktop's `claude_desktop_config.json`: ```json { "mcpServers": { "mcp-ssh": { "command": "npx", "args": ["@aiondadotcom/mcp-ssh"] } } } ``` ## Dependencies - `@modelcontextprotocol/sdk` - MCP protocol implementation - `ssh-config` - SSH configuration file parsing - Node.js built-ins: `child_process`, `fs/promises`, `os`, `path` ## Desktop Extension Support The project supports Desktop Extensions (.dxt) for easy installation in Claude Desktop: - `manifest.json` - DXT package manifest with server configuration - `scripts/build-dxt.sh` - Build script that creates .dxt packages in `build/` directory - `.dxt` files are ZIP archives containing the manifest and server files - Built packages are excluded from git via `.gitignore` but can be uploaded to GitHub releases ## Important Notes - The project is ESM-only (`"type": "module"` in package.json) - Production code is in `server-simple.mjs`, not compiled from TypeScript - SSH operations require properly configured SSH keys and host access - The agent runs over STDIO as an MCP server, not as a standalone application - DXT packages provide one-click installation alternative to manual JSON configuration ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript ``` -------------------------------------------------------------------------------- /claude_desktop_config.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "mcp-ssh": { "command": "/absolute/path/to/mcp-ssh/start.sh" } } } ``` -------------------------------------------------------------------------------- /start-silent.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Change to the directory where this script is located cd "$(dirname "$0")" # MCP SSH Agent Silent Startup Script # This script starts the MCP SSH server in silent mode for MCP clients # No debug output will be shown, only clean JSON communication export MCP_SILENT=true exec node server-simple.mjs ``` -------------------------------------------------------------------------------- /bin/mcp-ssh.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node // Simple wrapper to run the main server-simple.mjs file import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Import and run the main module const mainModule = path.join(__dirname, '..', 'server-simple.mjs'); import(mainModule); ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "typeRoots": ["./node_modules/@types"] }, "include": ["src/ssh-client.ts", "src/ssh-config-parser.ts", "src/types.ts"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Change to the directory where this script is located cd "$(dirname "$0")" # MCP SSH Agent Startup Script # This script starts the MCP SSH server using npm # Set MCP_SILENT=true to disable debug output for MCP clients # Check if we should run in silent mode (for MCP clients) if [ "$1" = "--silent" ] || [ "$MCP_SILENT" = "true" ]; then # Silent mode - no startup messages MCP_SILENT=true npm start else # Normal mode with startup messages echo "Starting MCP SSH Agent..." npm start fi ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript // Types for SSH Host information export interface SSHHostInfo { hostname: string; alias?: string; user?: string; port?: number; identityFile?: string; [key: string]: any; // For other configuration options } // Result of a remote command export interface CommandResult { stdout: string; stderr: string; code: number; } // SSH connection status export interface ConnectionStatus { connected: boolean; message: string; } // Batch result of remote commands export interface BatchCommandResult { results: CommandResult[]; success: boolean; } ``` -------------------------------------------------------------------------------- /temp-extract/manifest.json: -------------------------------------------------------------------------------- ```json { "dxt_version": "0.1", "name": "mcp-ssh", "version": "1.0.3", "description": "Connect to SSH hosts, run commands, and transfer files securely through Claude Desktop", "author": { "name": "aionda.com", "url": "https://aionda.com" }, "license": "MIT", "homepage": "https://github.com/aiondadotcom/mcp-ssh", "repository": { "type": "git", "url": "https://github.com/aiondadotcom/mcp-ssh.git" }, "icon": "doc/Claude.png", "keywords": [ "ssh", "remote", "server", "scp", "file-transfer", "automation", "devops" ], "server": { "type": "node", "entry_point": "server-simple.mjs", "mcp_config": { "command": "node", "args": ["server-simple.mjs"], "env": { "MCP_SILENT": "true" } } } } ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json { "dxt_version": "0.1", "name": "mcp-ssh", "version": "1.0.3", "description": "Connect to SSH hosts, run commands, and transfer files securely through Claude Desktop", "author": { "name": "aionda.com", "url": "https://aionda.com" }, "license": "MIT", "homepage": "https://github.com/aiondadotcom/mcp-ssh", "repository": { "type": "git", "url": "https://github.com/aiondadotcom/mcp-ssh.git" }, "icon": "doc/Claude.png", "keywords": [ "ssh", "remote", "server", "scp", "file-transfer", "automation", "devops" ], "server": { "type": "node", "entry_point": "server-simple.mjs", "mcp_config": { "command": "node", "args": ["${__dirname}/server-simple.mjs"], "env": { "MCP_SILENT": "true" } } } } ``` -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- ```yaml name: Publish to NPM on: release: types: [published] workflow_dispatch: inputs: version: description: 'Version to publish (e.g., patch, minor, major)' required: true default: 'patch' type: choice options: - patch - minor - major jobs: publish: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci - name: Run tests (if any) run: npm test --if-present - name: Bump version (if manual trigger) if: github.event_name == 'workflow_dispatch' run: npm version ${{ github.event.inputs.version }} --no-git-tag-version - name: Publish to NPM run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ``` -------------------------------------------------------------------------------- /gist_comment.json: -------------------------------------------------------------------------------- ```json { "body": "**Security Fix Applied**\n\nThank you for reporting this command injection vulnerability. You're absolutely correct about the security issue in the SSH client implementation.\n\n**Issue Confirmed:**\nThe vulnerability existed in `server-simple.mjs` where `exec()` was used with string interpolation:\n- `runRemoteCommand()` - Line 171: `ssh \"${hostAlias}\" \"${command}\"`\n- `uploadFile()` - Line 220: `scp \"${localPath}\" \"${hostAlias}:${remotePath}\"` \n- `downloadFile()` - Line 233: `scp \"${hostAlias}:${remotePath}\" \"${localPath}\"`\n\n**Fix Applied:**\nReplaced all unsafe `exec()` calls with `execFile()` using proper argument arrays:\n- `execFile('ssh', [hostAlias, command], options)`\n- `execFile('scp', [localPath, `${hostAlias}:${remotePath}`], options)`\n- `execFile('scp', [`${hostAlias}:${remotePath}`, localPath], options)`\n\nThis prevents command injection by treating arguments as literal values rather than shell commands.\n\n**Commit:** [5b9b9c5](https://github.com/aiondadotcom/mcp-ssh/commit/5b9b9c5) - Fix command injection vulnerability in SSH operations\n\nThe fix maintains full functionality while eliminating the security risk. Thank you for the responsible disclosure!" } ``` -------------------------------------------------------------------------------- /github_issue_response.md: -------------------------------------------------------------------------------- ```markdown Thank you for reporting this issue! You're absolutely right that the MCP SSH Agent doesn't support `Include` directives, and we'll implement this feature. **Implementation Plan:** We'll add proper `Include` directive support to ensure complete host discovery from all SSH configuration files. **Important SSH Configuration Note:** During our investigation, we discovered that SSH itself has a bug with `Include` directive processing. The `Include` statements **must be placed at the beginning** of your `~/.ssh/config` file to work correctly. **Example of correct placement:** ``` # ~/.ssh/config Include ~/.ssh/config.d/* Include ~/.ssh/work-hosts # Global settings ServerAliveInterval 55 # Host definitions Host myserver HostName example.com ``` **Why this matters:** If `Include` statements are placed at the end of the config file, SSH reads them but doesn't properly apply the included host configurations. This is a bug in OpenSSH's configuration parser. **Our solution:** 1. We'll implement `Include` support in the MCP SSH Agent to read all configuration files 2. We'll document this SSH quirk in our README to help users avoid configuration issues 3. The agent will work correctly regardless of where users place their `Include` statements We'll have this feature implemented soon. Thanks for bringing this to our attention! ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@aiondadotcom/mcp-ssh", "version": "1.1.0", "description": "MCP Agent for managing SSH hosts - A Model Context Protocol server for SSH operations", "main": "server-simple.mjs", "bin": { "mcp-ssh": "bin/mcp-ssh.js" }, "type": "module", "scripts": { "start": "node server-simple.mjs", "dev": "node server-simple.mjs", "build": "echo \"Build skipped\"", "build:dxt": "./scripts/build-dxt.sh", "test": "echo \"No tests specified\" && exit 0", "prepublishOnly": "npm run test", "version": "git add -A", "postversion": "git push && git push --tags" }, "keywords": [ "mcp", "ssh", "agent", "model-context-protocol", "claude", "ai", "remote", "server" ], "author": "aionda.com", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/aiondadotcom/mcp-ssh.git" }, "homepage": "https://github.com/aiondadotcom/mcp-ssh", "bugs": { "url": "https://github.com/aiondadotcom/mcp-ssh/issues" }, "publishConfig": { "access": "public" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "glob": "^11.0.3", "ssh-config": "^5.0.0" }, "devDependencies": { "@anthropic-ai/dxt": "^0.2.5", "@types/node": "^20.11.26", "@types/ssh2": "^1.15.0", "tmp": ">=0.2.4", "ts-node": "^10.9.2", "typescript": "^5.4.3" }, "overrides": { "tmp": ">=0.2.4" } } ``` -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- ```markdown # Publishing Instructions This document contains instructions for publishing the @aiondadotcom/mcp-ssh package to npm. ## Prerequisites 1. You need to be a member of the @aiondadotcom organization on npm 2. You need to be logged in to npm: `npm login` 3. Verify your access: `npm access list packages @aiondadotcom` ## Publishing Process ### Automated Publishing (Recommended) The package is automatically published when you create a GitHub release: 1. Commit all changes 2. Create a new release on GitHub 3. The GitHub Action will automatically publish to npm ### Manual Publishing ```bash # 1. Make sure you're on the main branch and everything is committed git checkout main git pull origin main # 2. Bump the version (patch, minor, or major) npm version patch # or minor/major # 3. Publish to npm npm publish # 4. Push the version commit and tag git push origin main --tags ``` ### Testing Before Publishing ```bash # Test the package locally npm pack npm install -g ./aiondadotcom-mcp-ssh-1.0.0.tgz # Test the binary mcp-ssh --help # Clean up npm uninstall -g @aiondadotcom/mcp-ssh rm *.tgz ``` ## First-Time Setup If this is the first time publishing this package: ```bash # Login to npm npm login # Verify you have access to the @aiondadotcom scope npm access list packages @aiondadotcom # Publish the package npm publish --access public ``` ## Package Configuration The package is configured with: - Scoped name: `@aiondadotcom/mcp-ssh` - Public access - Binary: `mcp-ssh` command - Entry point: `server-simple.mjs` ``` -------------------------------------------------------------------------------- /scripts/build-dxt.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Build script for creating DXT packages for mcp-ssh # This script creates .dxt files for distribution without committing them to the repository set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color echo -e "${GREEN}Building MCP SSH DXT Package${NC}" # Check if dxt CLI is available if ! command -v npx &> /dev/null; then echo -e "${RED}Error: npm/npx not found. Please install Node.js${NC}" exit 1 fi # Check if we have the dxt package if ! npm list @anthropic-ai/dxt &> /dev/null; then echo -e "${RED}Error: @anthropic-ai/dxt not found. Please run 'npm install'${NC}" exit 1 fi # Create build directory (not tracked in git) BUILD_DIR="build" rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" echo -e "${YELLOW}Creating DXT package...${NC}" # Get version from package.json VERSION=$(node -p "require('./package.json').version") DXT_FILE="mcp-ssh-${VERSION}.dxt" # Create the DXT package npx dxt pack . "$BUILD_DIR/$DXT_FILE" if [ $? -eq 0 ]; then echo -e "${GREEN}✓ DXT package created successfully: $BUILD_DIR/$DXT_FILE${NC}" echo -e "${GREEN}✓ Package size: $(ls -lh "$BUILD_DIR/$DXT_FILE" | awk '{print $5}')${NC}" # Display next steps echo -e "\n${YELLOW}Next steps:${NC}" echo "1. Test the DXT package locally" echo "2. Upload to GitHub releases:" echo " gh release create v${VERSION} $BUILD_DIR/$DXT_FILE --title 'Release v${VERSION}' --notes 'MCP SSH Agent v${VERSION}'" echo "3. Or upload manually to GitHub releases page" else echo -e "${RED}✗ Failed to create DXT package${NC}" exit 1 fi ``` -------------------------------------------------------------------------------- /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). ## [1.1.0] - 2025-08-17 ### Added - **NEW FEATURE**: SSH config Include directive support - Added recursive processing of Include directives in SSH configuration files - Support for glob patterns in Include paths (e.g., `Include ~/.ssh/configs/*`) - Enhanced SSH host discovery from included configuration files - Added `glob` dependency for Include path pattern matching ### Enhanced - Improved SSH configuration parsing to handle complex Include hierarchies - Enhanced host discovery to recursively process all included config files - Better error handling for malformed or inaccessible Include files ## [1.0.4] - 2025-08-17 ### Security - **SECURITY FIX**: Fixed command injection vulnerability in SSH operations (commit 5b9b9c5) - **SECURITY FIX**: Upgraded `tmp` dependency to version 0.2.5 to address CVE vulnerability - Fixed arbitrary temporary file/directory write via symbolic link in `tmp` package (GHSA-52f5-9888-hmc6) - Added dependency overrides to ensure all transitive dependencies use secure `tmp` version - Enhanced input validation and sanitization for SSH commands and file paths ### Technical - Added `tmp: ">=0.2.4"` to devDependencies to force secure version - Added npm overrides configuration to enforce secure tmp version across entire dependency tree - Updated package-lock.json to reflect security fixes ## [1.0.3] - 2025-06-06 ### Added - Binary wrapper script (`bin/mcp-ssh.js`) for proper npx compatibility - Fixed npx execution issues by implementing wrapper pattern ### Fixed - NPX executable resolution using wrapper script approach - Package binary configuration now points to proper wrapper ### Technical - Added `bin/mcp-ssh.js` wrapper to handle npx execution - Updated package.json bin configuration to use wrapper script ## [1.0.2] - 2025-06-06 ### Fixed - Build script temporary fix - File permissions for executable ## [1.0.1] - 2025-06-06 ### Fixed - Initial package configuration - File permissions ## [1.0.0] - 2025-06-06 ### Added - Initial release of MCP SSH Agent - Support for all SSH operations via native ssh/scp commands - Automatic SSH host discovery from ~/.ssh/config and ~/.ssh/known_hosts - Functions: listKnownHosts, runRemoteCommand, getHostInfo, checkConnectivity, uploadFile, downloadFile, runCommandBatch - Claude Desktop integration support - NPM package distribution via @aiondadotcom/mcp-ssh - npx compatibility for easy installation and usage ### Features - Native SSH command execution for maximum compatibility - Silent mode for MCP clients (MCP_SILENT=true) - Comprehensive error handling with timeouts - Batch command execution support - File upload/download via scp - SSH connectivity testing ### Documentation - Complete README with Claude Desktop setup instructions - Usage examples and troubleshooting guide - Professional npm package configuration ``` -------------------------------------------------------------------------------- /src/ssh-config-parser.ts: -------------------------------------------------------------------------------- ```typescript import { readFile } from 'fs/promises'; import { homedir } from 'os'; import { join } from 'path'; import * as sshConfig from 'ssh-config'; import { SSHHostInfo } from './types.js'; export class SSHConfigParser { private configPath: string; private knownHostsPath: string; constructor() { const homeDir = homedir(); this.configPath = join(homeDir, '.ssh', 'config'); this.knownHostsPath = join(homeDir, '.ssh', 'known_hosts'); } /** * Reads and parses the SSH config file */ async parseConfig(): Promise<SSHHostInfo[]> { try { const content = await readFile(this.configPath, 'utf-8'); const config = sshConfig.parse(content); return this.extractHostsFromConfig(config); } catch (error) { console.error('Error reading SSH config:', error); return []; } } /** * Extracts host information from SSH Config */ private extractHostsFromConfig(config: any[]): SSHHostInfo[] { const hosts: SSHHostInfo[] = []; for (const section of config) { if (section.param === 'Host' && section.value !== '*') { const hostInfo: SSHHostInfo = { hostname: '', alias: section.value, }; // Search all entries for this host for (const param of section.config) { switch (param.param.toLowerCase()) { case 'hostname': hostInfo.hostname = param.value; break; case 'user': hostInfo.user = param.value; break; case 'port': hostInfo.port = parseInt(param.value, 10); break; case 'identityfile': hostInfo.identityFile = param.value; break; default: // Store other parameters hostInfo[param.param.toLowerCase()] = param.value; } } // Only add hosts with complete information if (hostInfo.hostname) { hosts.push(hostInfo); } } } return hosts; } /** * Reads the known_hosts file and extracts hostnames */ async parseKnownHosts(): Promise<string[]> { try { const content = await readFile(this.knownHostsPath, 'utf-8'); const knownHosts = content .split('\n') .filter(line => line.trim() !== '') .map(line => { // Format: hostname[,hostname2...] key-type public-key const parts = line.split(' ')[0]; return parts.split(',')[0]; }); return knownHosts; } catch (error) { console.error('Error reading known_hosts file:', error); return []; } } /** * Consolidates information from config and known_hosts */ async getAllKnownHosts(): Promise<SSHHostInfo[]> { const configHosts = await this.parseConfig(); const knownHostnames = await this.parseKnownHosts(); // Add hosts from known_hosts that aren't in the config for (const hostname of knownHostnames) { if (!configHosts.some(host => host.hostname === hostname || host.alias === hostname)) { configHosts.push({ hostname: hostname }); } } return configHosts; } } ``` -------------------------------------------------------------------------------- /src/ssh-client.ts: -------------------------------------------------------------------------------- ```typescript import { NodeSSH } from 'node-ssh'; import { SSHHostInfo, CommandResult, ConnectionStatus, BatchCommandResult } from './types.js'; import { SSHConfigParser } from './ssh-config-parser.js'; export class SSHClient { private ssh: NodeSSH; private configParser: SSHConfigParser; constructor() { this.ssh = new NodeSSH(); this.configParser = new SSHConfigParser(); } /** * Lists all known SSH hosts */ async listKnownHosts(): Promise<SSHHostInfo[]> { return await this.configParser.getAllKnownHosts(); } /** * Connects to an SSH host and executes a command */ async runRemoteCommand(hostAlias: string, command: string): Promise<CommandResult> { try { // First connect to the host await this.connectToHost(hostAlias); // Execute the command const result = await this.ssh.execCommand(command); return { stdout: result.stdout, stderr: result.stderr, code: result.code || 0 }; } catch (error) { console.error(`Error executing command on ${hostAlias}:`, error); return { stdout: '', stderr: error instanceof Error ? error.message : String(error), code: 1 }; } finally { this.ssh.dispose(); } } /** * Returns all details about a host */ async getHostInfo(hostAlias: string): Promise<SSHHostInfo | null> { const hosts = await this.configParser.parseConfig(); return hosts.find(host => host.alias === hostAlias || host.hostname === hostAlias) || null; } /** * Checks if a connection to the host is possible */ async checkConnectivity(hostAlias: string): Promise<ConnectionStatus> { try { // Establish connection await this.connectToHost(hostAlias); // Execute ping command const result = await this.ssh.execCommand('echo connected'); const connected = result.stdout.trim() === 'connected'; this.ssh.dispose(); return { connected, message: connected ? 'Connection successful' : 'Echo test failed' }; } catch (error) { console.error(`Connectivity error with ${hostAlias}:`, error); return { connected: false, message: error instanceof Error ? error.message : String(error) }; } } /** * Uploads a file to the remote host */ async uploadFile(hostAlias: string, localPath: string, remotePath: string): Promise<boolean> { try { await this.connectToHost(hostAlias); await this.ssh.putFile(localPath, remotePath); this.ssh.dispose(); return true; } catch (error) { console.error(`Error uploading file to ${hostAlias}:`, error); return false; } } /** * Downloads a file from the remote host */ async downloadFile(hostAlias: string, remotePath: string, localPath: string): Promise<boolean> { try { await this.connectToHost(hostAlias); await this.ssh.getFile(localPath, remotePath); this.ssh.dispose(); return true; } catch (error) { console.error(`Error downloading file from ${hostAlias}:`, error); return false; } } /** * Executes multiple commands in sequence */ async runCommandBatch(hostAlias: string, commands: string[]): Promise<BatchCommandResult> { try { await this.connectToHost(hostAlias); const results: CommandResult[] = []; let success = true; for (const command of commands) { const result = await this.ssh.execCommand(command); const cmdResult: CommandResult = { stdout: result.stdout, stderr: result.stderr, code: result.code || 0 }; results.push(cmdResult); if (cmdResult.code !== 0) { success = false; // We don't abort, execute all commands } } this.ssh.dispose(); return { results, success }; } catch (error) { console.error(`Error during batch execution on ${hostAlias}:`, error); return { results: [{ stdout: '', stderr: error instanceof Error ? error.message : String(error), code: 1 }], success: false }; } } /** * Establishes a connection to a host */ private async connectToHost(hostAlias: string): Promise<void> { // Get host information const hostInfo = await this.getHostInfo(hostAlias); if (!hostInfo) { throw new Error(`Host ${hostAlias} not found`); } // Create connection configuration const connectionConfig = { host: hostInfo.hostname, username: hostInfo.user, port: hostInfo.port || 22, privateKeyPath: hostInfo.identityFile }; try { await this.ssh.connect(connectionConfig); } catch (error) { throw new Error(`Connection to ${hostAlias} failed: ${error instanceof Error ? error.message : String(error)}`); } } } ``` -------------------------------------------------------------------------------- /IMPLEMENTATION_NOTES.md: -------------------------------------------------------------------------------- ```markdown # MCP SSH Agent Implementation Notes ## Final Implementation (v2.0 - Simplified SSH) The MCP SSH Agent has been successfully implemented with a simplified, more reliable SSH approach that replaced the problematic `node-ssh` library. ### Architecture 1. **Main Server File**: `server-simple.mjs` - Pure JavaScript implementation using ES modules with createRequire - Uses `@modelcontextprotocol/sdk` for MCP protocol compliance - Uses native `ssh` and `scp` commands via `child_process.exec()` 2. **Core Components**: - `SSHConfigParser`: Parses `~/.ssh/config` and `~/.ssh/known_hosts` - `SSHClient`: Handles all SSH operations using local SSH commands - MCP Server: Provides standardized tool interface ### Key Changes in v2.0 **Problem Solved**: The original implementation using `node-ssh` library failed with authentication errors ("All configured authentication methods failed") even though manual SSH connections worked perfectly. **Solution**: Replaced `node-ssh` with direct execution of local `ssh` and `scp` commands using Node.js `child_process.exec()`. This approach: - Leverages existing SSH infrastructure (agent, keys, config) - Avoids JavaScript library authentication complexities - Is more reliable and simpler to maintain - Provides better error messages and debugging ### MCP Protocol Implementation - **Server Creation**: Uses `Server` class from MCP SDK - **Transport**: `StdioServerTransport` for STDIO communication - **Request Handlers**: - `ListToolsRequestSchema`: Returns available SSH tools - `CallToolRequestSchema`: Executes SSH operations ### SSH Tools Provided 1. **listKnownHosts**: Returns all configured SSH hosts 2. **runRemoteCommand**: Executes single commands remotely using `ssh "host" "command"` 3. **getHostInfo**: Returns host configuration details 4. **checkConnectivity**: Tests SSH connectivity with simple echo test 5. **uploadFile**: Transfers files to remote hosts using `scp` 6. **downloadFile**: Downloads files from remote hosts using `scp` 7. **runCommandBatch**: Executes multiple commands sequentially ### Technical Implementation Details 1. **SSH Command Execution**: ```javascript const sshCommand = `ssh "${hostAlias}" "${command.replace(/"/g, '\\"')}"`; const { stdout, stderr } = await execAsync(sshCommand, { timeout: 30000 }); ``` 2. **File Transfers**: ```javascript // Upload: scp "localPath" "hostAlias:remotePath" // Download: scp "hostAlias:remotePath" "localPath" ``` 3. **Error Handling**: Proper timeout handling (30s for commands, 60s for transfers) ### Key Technical Decisions 1. **Module Resolution**: Used `createRequire()` to handle MCP SDK CommonJS exports 2. **Schema Compliance**: Used proper MCP request schemas instead of string identifiers 3. **SSH Approach**: Native SSH commands instead of JavaScript SSH libraries 4. **Error Handling**: Comprehensive error handling with meaningful error messages 5. **File Structure**: Simplified to single main file to avoid build complexity ### Dependencies (Simplified) - `@modelcontextprotocol/sdk`: MCP protocol implementation - `ssh-config`: SSH configuration parsing - Native Node.js modules: `child_process`, `util`, `os`, `fs` **Removed**: `node-ssh`, `ssh2` (unreliable authentication) ### Testing The implementation has been thoroughly tested with: 1. **Basic Functionality Tests**: ```bash # Test tools listing echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node server-simple.mjs # Test SSH command execution echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"runRemoteCommand","arguments":{"hostAlias":"prod","command":"echo \"test\""}}}' | node server-simple.mjs ``` 2. **SSH Connectivity**: Successfully tested against prod host (157.90.89.149:42077) 3. **Authentication**: Works with SSH agent and default key resolution 4. **Error Handling**: Proper error reporting for failed connections ### Performance and Reliability - **Startup Time**: Fast server initialization (~1-2 seconds) - **Command Execution**: Efficient direct SSH command execution - **Memory Usage**: Minimal - no persistent SSH connections - **Reliability**: Leverages proven SSH infrastructure ### Usage ```bash npm start # Starts the MCP server on STDIO ``` The server is ready for integration with MCP-compatible clients and language models. ## Troubleshooting History ### Issue: SSH Authentication Failures with node-ssh **Problem**: The original implementation using `node-ssh` consistently failed with: ``` Connection to prod failed: All configured authentication methods failed ``` **Investigation**: - SSH config parsing worked correctly (prod host found) - Manual SSH connection (`ssh prod echo test`) worked perfectly - Multiple attempts to configure SSH agent, keys, and connection options failed - Issue appeared to be with node-ssh library's authentication handling **Solution**: Complete replacement with native SSH command execution - Removed dependencies: `node-ssh`, `ssh2` - Replaced with: `child_process.exec()` + local `ssh`/`scp` commands - Result: Immediate success, much simpler code, better reliability This demonstrates the value of using proven system tools over complex JavaScript libraries for system operations. ## Security Considerations - Uses existing SSH key infrastructure - No password storage or handling - Relies on properly configured SSH authentication - All operations respect SSH configuration restrictions ## Maintenance Notes - Keep MCP SDK dependency updated for protocol compliance - Monitor SSH library updates for security patches - Test with various SSH configurations periodically ``` -------------------------------------------------------------------------------- /server-simple.mjs: -------------------------------------------------------------------------------- ``` #!/usr/bin/env node /** * MCP SSH Agent - A Model Context Protocol server for managing SSH connections * * This is a simplified implementation that directly imports from specific files * to avoid module resolution issues. */ // Import required Node.js modules import { homedir } from 'os'; import { readFile } from 'fs/promises'; import { join } from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; // Use createRequire to work around ESM import issues const require = createRequire(import.meta.url); // Required libraries const { spawn, exec, execFile } = require('child_process'); const { promisify } = require('util'); const sshConfig = require('ssh-config'); const execAsync = promisify(exec); const execFileAsync = promisify(execFile); // Silent mode for MCP clients - disable debug output when used as MCP server const SILENT_MODE = process.env.MCP_SILENT === 'true' || process.argv.includes('--silent'); // Debug logging function - only outputs in non-silent mode function debugLog(message) { if (!SILENT_MODE) { process.stderr.write(message); } } // Import MCP components using proper export paths const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); // SSH Configuration Parser class SSHConfigParser { constructor() { const homeDir = homedir(); this.configPath = join(homeDir, '.ssh', 'config'); this.knownHostsPath = join(homeDir, '.ssh', 'known_hosts'); } async parseConfig() { try { const content = await readFile(this.configPath, 'utf-8'); const config = sshConfig.parse(content); return this.extractHostsFromConfig(config, this.configPath); } catch (error) { debugLog(`Error reading SSH config: ${error.message}\n`); return []; } } async processIncludeDirectives(configPath) { try { const content = await readFile(configPath, 'utf-8'); const config = sshConfig.parse(content); const hosts = []; for (const section of config) { if (section.param === 'Include' && section.value) { const includePaths = this.expandIncludePath(section.value, configPath); for (const includePath of includePaths) { try { const includeHosts = await this.processIncludeDirectives(includePath); hosts.push(...includeHosts); } catch (error) { debugLog(`Error processing include file ${includePath}: ${error.message}\n`); } } } } // Add hosts from the current config file const currentHosts = this.extractHostsFromConfig(config, configPath); hosts.push(...currentHosts); return hosts; } catch (error) { debugLog(`Error processing config file ${configPath}: ${error.message}\n`); return []; } } expandIncludePath(includePath, baseConfigPath) { const { dirname, resolve } = require('path'); const { glob } = require('glob'); const { existsSync } = require('fs'); // Handle tilde expansion if (includePath.startsWith('~/')) { includePath = includePath.replace('~', homedir()); } // Handle relative paths if (!includePath.startsWith('/')) { const baseDir = dirname(baseConfigPath); includePath = resolve(baseDir, includePath); } try { // Handle glob patterns if (includePath.includes('*') || includePath.includes('?')) { return glob.sync(includePath).filter(path => existsSync(path)); } else { return existsSync(includePath) ? [includePath] : []; } } catch (error) { debugLog(`Error expanding include path ${includePath}: ${error.message}\n`); return []; } } extractHostsFromConfig(config, configPath) { const hosts = []; for (const section of config) { // Skip Include directives as they are processed separately if (section.param === 'Include') { continue; } if (section.param === 'Host' && section.value !== '*') { const hostInfo = { hostname: '', alias: section.value, configFile: configPath }; // Search all entries for this host for (const param of section.config) { // Safety check for undefined param if (!param || !param.param) { continue; } switch (param.param.toLowerCase()) { case 'hostname': hostInfo.hostname = param.value; break; case 'user': hostInfo.user = param.value; break; case 'port': hostInfo.port = parseInt(param.value, 10); break; case 'identityfile': hostInfo.identityFile = param.value; break; default: // Store other parameters hostInfo[param.param.toLowerCase()] = param.value; } } // Only add hosts with complete information if (hostInfo.hostname) { hosts.push(hostInfo); } } } return hosts; } async parseKnownHosts() { try { const content = await readFile(this.knownHostsPath, 'utf-8'); const knownHosts = content .split('\n') .filter(line => line.trim() !== '') .map(line => { // Format: hostname[,hostname2...] key-type public-key const parts = line.split(' ')[0]; return parts.split(',')[0]; }); return knownHosts; } catch (error) { debugLog(`Error reading known_hosts file: ${error.message}\n`); return []; } } async getAllKnownHosts() { // First: Get all hosts from ~/.ssh/config including Include directives (these are prioritized) const configHosts = await this.processIncludeDirectives(this.configPath); // Second: Get hostnames from ~/.ssh/known_hosts const knownHostnames = await this.parseKnownHosts(); // Create a comprehensive list starting with config hosts const allHosts = [...configHosts]; // Add hosts from known_hosts that aren't already in the config // These will appear after the config hosts for (const hostname of knownHostnames) { if (!configHosts.some(host => host.hostname === hostname || host.alias === hostname)) { allHosts.push({ hostname: hostname, source: 'known_hosts' }); } } // Mark config hosts for clarity configHosts.forEach(host => { host.source = 'ssh_config'; }); return allHosts; } } // SSH Client Implementation class SSHClient { constructor() { this.configParser = new SSHConfigParser(); } async listKnownHosts() { return await this.configParser.getAllKnownHosts(); } async runRemoteCommand(hostAlias, command) { try { // Use execFile for security - prevents command injection debugLog(`Executing: ssh ${hostAlias} ${command}\n`); const { stdout, stderr } = await execFileAsync('ssh', [hostAlias, command], { timeout: 30000, // 30 second timeout maxBuffer: 1024 * 1024 * 10 // 10MB buffer }); return { stdout: stdout || '', stderr: stderr || '', code: 0 }; } catch (error) { debugLog(`Error executing command on ${hostAlias}: ${error.message}\n`); return { stdout: error.stdout || '', stderr: error.stderr || error.message, code: error.code || 1 }; } } async getHostInfo(hostAlias) { const hosts = await this.configParser.processIncludeDirectives(this.configParser.configPath); return hosts.find(host => host.alias === hostAlias || host.hostname === hostAlias) || null; } async checkConnectivity(hostAlias) { try { // Simple connectivity test using ssh const result = await this.runRemoteCommand(hostAlias, 'echo connected'); const connected = result.code === 0 && result.stdout.trim() === 'connected'; return { connected, message: connected ? 'Connection successful' : 'Connection failed' }; } catch (error) { debugLog(`Connectivity error with ${hostAlias}: ${error.message}\n`); return { connected: false, message: error instanceof Error ? error.message : String(error) }; } } async uploadFile(hostAlias, localPath, remotePath) { try { debugLog(`Executing: scp ${localPath} ${hostAlias}:${remotePath}\n`); await execFileAsync('scp', [localPath, `${hostAlias}:${remotePath}`], { timeout: 60000 // 60 second timeout for file transfer }); return true; } catch (error) { debugLog(`Error uploading file to ${hostAlias}: ${error.message}\n`); return false; } } async downloadFile(hostAlias, remotePath, localPath) { try { debugLog(`Executing: scp ${hostAlias}:${remotePath} ${localPath}\n`); await execFileAsync('scp', [`${hostAlias}:${remotePath}`, localPath], { timeout: 60000 // 60 second timeout for file transfer }); return true; } catch (error) { debugLog(`Error downloading file from ${hostAlias}: ${error.message}\n`); return false; } } async runCommandBatch(hostAlias, commands) { try { const results = []; let success = true; for (const command of commands) { const result = await this.runRemoteCommand(hostAlias, command); results.push(result); if (result.code !== 0) { success = false; // Continue executing remaining commands } } return { results, success }; } catch (error) { debugLog(`Error during batch execution on ${hostAlias}: ${error.message}\n`); return { results: [{ stdout: '', stderr: error instanceof Error ? error.message : String(error), code: 1 }], success: false }; } } } // Main function to start the MCP server async function main() { try { // Create an instance of the SSH client debugLog("Initializing SSH client...\n"); const sshClient = new SSHClient(); debugLog("Creating MCP server...\n"); // Create an MCP server const server = new Server( { name: "mcp-ssh", version: "1.0.0" }, { capabilities: { tools: {} } } ); debugLog("Setting up request handlers...\n"); // Handler for listing available tools server.setRequestHandler(ListToolsRequestSchema, async () => { debugLog("Received listTools request\n"); return { tools: [ { name: "listKnownHosts", description: "Returns a consolidated list of all known SSH hosts, prioritizing ~/.ssh/config entries first, then additional hosts from ~/.ssh/known_hosts", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "runRemoteCommand", description: "Executes a shell command on an SSH host", inputSchema: { type: "object", properties: { hostAlias: { type: "string", description: "Alias or hostname of the SSH host", }, command: { type: "string", description: "The shell command to execute", }, }, required: ["hostAlias", "command"], }, }, { name: "getHostInfo", description: "Returns all configuration details for an SSH host", inputSchema: { type: "object", properties: { hostAlias: { type: "string", description: "Alias or hostname of the SSH host", }, }, required: ["hostAlias"], }, }, { name: "checkConnectivity", description: "Checks if an SSH connection to the host is possible", inputSchema: { type: "object", properties: { hostAlias: { type: "string", description: "Alias or hostname of the SSH host", }, }, required: ["hostAlias"], }, }, { name: "uploadFile", description: "Uploads a local file to an SSH host", inputSchema: { type: "object", properties: { hostAlias: { type: "string", description: "Alias or hostname of the SSH host", }, localPath: { type: "string", description: "Path to the local file", }, remotePath: { type: "string", description: "Path on the remote host", }, }, required: ["hostAlias", "localPath", "remotePath"], }, }, { name: "downloadFile", description: "Downloads a file from an SSH host", inputSchema: { type: "object", properties: { hostAlias: { type: "string", description: "Alias or hostname of the SSH host", }, remotePath: { type: "string", description: "Path on the remote host", }, localPath: { type: "string", description: "Path to the local destination", }, }, required: ["hostAlias", "remotePath", "localPath"], }, }, { name: "runCommandBatch", description: "Executes multiple shell commands sequentially on an SSH host", inputSchema: { type: "object", properties: { hostAlias: { type: "string", description: "Alias or hostname of the SSH host", }, commands: { type: "array", items: { type: "string" }, description: "List of shell commands to execute", }, }, required: ["hostAlias", "commands"], }, }, ], }; }); // Handler for tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; debugLog(`Received callTool request for tool: ${name}\n`); if (!args && name !== "listKnownHosts") { throw new Error(`No arguments provided for tool: ${name}`); } try { switch (name) { case "listKnownHosts": { const hosts = await sshClient.listKnownHosts(); return { content: [{ type: "text", text: JSON.stringify(hosts, null, 2) }], }; } case "runRemoteCommand": { const result = await sshClient.runRemoteCommand( args.hostAlias, args.command ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "getHostInfo": { const hostInfo = await sshClient.getHostInfo(args.hostAlias); return { content: [{ type: "text", text: JSON.stringify(hostInfo, null, 2) }], }; } case "checkConnectivity": { const status = await sshClient.checkConnectivity(args.hostAlias); return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }], }; } case "uploadFile": { const success = await sshClient.uploadFile( args.hostAlias, args.localPath, args.remotePath ); return { content: [{ type: "text", text: JSON.stringify({ success }, null, 2) }], }; } case "downloadFile": { const success = await sshClient.downloadFile( args.hostAlias, args.remotePath, args.localPath ); return { content: [{ type: "text", text: JSON.stringify({ success }, null, 2) }], }; } case "runCommandBatch": { const result = await sshClient.runCommandBatch( args.hostAlias, args.commands ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { debugLog(`Error executing tool ${name}: ${error.message}\n`); return { content: [ { type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error), }), }, ], }; } }); debugLog("Starting MCP SSH Agent on STDIO...\n"); const transport = new StdioServerTransport(); await server.connect(transport); debugLog("MCP SSH Agent connected and ready!\n"); } catch (error) { debugLog(`Error starting MCP SSH Agent: ${error.message}\n`); process.exit(1); } } // Start the server main().catch(error => { debugLog(`Unhandled error: ${error.message}\n`); process.exit(1); }); ```