#
tokens: 23605/50000 24/24 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | # Development files
 2 | src/
 3 | tsconfig.json
 4 | .vscode/
 5 | IMPLEMENTATION_NOTES.md
 6 | 
 7 | # Documentation images (keep README.md)
 8 | doc/
 9 | 
10 | # Development scripts
11 | start.sh
12 | start-silent.sh
13 | 
14 | # Git and CI/CD
15 | .git/
16 | .github/
17 | .gitignore
18 | *.log
19 | node_modules/
20 | 
21 | # Generated files
22 | *.tgz
23 | 
24 | # Keep these files for npm package
25 | !package.json
26 | !README.md
27 | !server-simple.mjs
28 | !LICENSE
29 | !CHANGELOG.md
30 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Dependencies
  2 | node_modules/
  3 | npm-debug.log*
  4 | yarn-debug.log*
  5 | yarn-error.log*
  6 | 
  7 | # Runtime data
  8 | pids
  9 | *.pid
 10 | *.seed
 11 | *.pid.lock
 12 | 
 13 | # Coverage directory used by tools like istanbul
 14 | coverage/
 15 | *.lcov
 16 | 
 17 | # nyc test coverage
 18 | .nyc_output
 19 | 
 20 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 21 | .grunt
 22 | 
 23 | # Bower dependency directory (https://bower.io/)
 24 | bower_components
 25 | 
 26 | # node-waf configuration
 27 | .lock-wscript
 28 | 
 29 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 30 | build/Release
 31 | 
 32 | # Dependency directories
 33 | jspm_packages/
 34 | 
 35 | # TypeScript cache
 36 | *.tsbuildinfo
 37 | 
 38 | # Optional npm cache directory
 39 | .npm
 40 | 
 41 | # Optional eslint cache
 42 | .eslintcache
 43 | 
 44 | # Microbundle cache
 45 | .rpt2_cache/
 46 | .rts2_cache_cjs/
 47 | .rts2_cache_es/
 48 | .rts2_cache_umd/
 49 | 
 50 | # Optional REPL history
 51 | .node_repl_history
 52 | 
 53 | # Output of 'npm pack'
 54 | *.tgz
 55 | 
 56 | # Yarn Integrity file
 57 | .yarn-integrity
 58 | 
 59 | # dotenv environment variables file
 60 | .env
 61 | .env.test
 62 | .env.production
 63 | .env.local
 64 | 
 65 | # parcel-bundler cache (https://parceljs.org/)
 66 | .cache
 67 | .parcel-cache
 68 | 
 69 | # Next.js build output
 70 | .next
 71 | 
 72 | # Nuxt.js build / generate output
 73 | .nuxt
 74 | dist
 75 | 
 76 | # Gatsby files
 77 | .cache/
 78 | public
 79 | 
 80 | # Storybook build outputs
 81 | .out
 82 | .storybook-out
 83 | 
 84 | # Temporary folders
 85 | tmp/
 86 | temp/
 87 | 
 88 | # Logs
 89 | logs
 90 | *.log
 91 | 
 92 | # Runtime data
 93 | pids
 94 | *.pid
 95 | *.seed
 96 | *.pid.lock
 97 | 
 98 | # OS generated files
 99 | .DS_Store
100 | .DS_Store?
101 | ._*
102 | .Spotlight-V100
103 | .Trashes
104 | ehthumbs.db
105 | Thumbs.db
106 | 
107 | # IDE files
108 | .vscode/
109 | .idea/
110 | *.swp
111 | *.swo
112 | *~
113 | 
114 | # Build outputs
115 | dist/
116 | build/
117 | 
118 | # DXT build artifacts
119 | *.dxt
120 | releases/
121 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP SSH Agent
  2 | 
  3 | 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.
  4 | 
  5 | ## Overview
  6 | 
  7 | 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.
  8 | 
  9 | ## Quick Start
 10 | 
 11 | ### Desktop Extension Installation (Recommended)
 12 | 
 13 | The easiest way to install MCP SSH Agent is through the Desktop Extension (.dxt) format:
 14 | 
 15 | 1. Download the latest `mcp-ssh-*.dxt` file from the [GitHub releases page](https://github.com/aiondadotcom/mcp-ssh/releases)
 16 | 2. Double-click the `.dxt` file to install it in Claude Desktop
 17 | 3. The SSH tools will be automatically available in your conversations with Claude
 18 | 
 19 | ### Alternative Installation Methods
 20 | 
 21 | #### Installation via npx
 22 | 
 23 | ```bash
 24 | npx @aiondadotcom/mcp-ssh
 25 | ```
 26 | 
 27 | #### Manual Claude Desktop Configuration
 28 | 
 29 | To use this MCP server with Claude Desktop using manual configuration, add the following to your MCP settings file:
 30 | 
 31 | **On macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
 32 | **On Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
 33 | 
 34 | ```json
 35 | {
 36 |   "mcpServers": {
 37 |     "mcp-ssh": {
 38 |       "command": "npx",
 39 |       "args": ["@aiondadotcom/mcp-ssh"]
 40 |     }
 41 |   }
 42 | }
 43 | ```
 44 | 
 45 | After adding this configuration, restart Claude Desktop. The SSH tools will be available for use in your conversations with Claude.
 46 | 
 47 | #### Global Installation
 48 | ```bash
 49 | npm install -g @aiondadotcom/mcp-ssh
 50 | ```
 51 | 
 52 | #### Local Development
 53 | ```bash
 54 | git clone https://github.com/aiondadotcom/mcp-ssh.git
 55 | cd mcp-ssh
 56 | npm install
 57 | npm start
 58 | ```
 59 | 
 60 | ## Example Usage
 61 | 
 62 | ![MCP SSH Agent Example](doc/example.png)
 63 | 
 64 | The screenshot above shows the MCP SSH Agent in action, demonstrating how it integrates with MCP-compatible clients to provide seamless SSH operations.
 65 | 
 66 | ### Integration with Claude
 67 | 
 68 | ![Claude MCP Integration](doc/Claude.png)
 69 | 
 70 | 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.
 71 | 
 72 | ## Key Features
 73 | 
 74 | - **Reliable SSH**: Uses native `ssh`/`scp` commands instead of JavaScript SSH libraries
 75 | - **Automatic Discovery**: Finds hosts from SSH config and known_hosts files
 76 | - **Full SSH Support**: Works with SSH agents, keys, and all authentication methods
 77 | - **File Operations**: Upload and download files using `scp`
 78 | - **Batch Commands**: Execute multiple commands in sequence
 79 | - **Error Handling**: Comprehensive error reporting with timeouts
 80 | 
 81 | ## Functions
 82 | 
 83 | The agent provides the following MCP tools:
 84 | 
 85 | 1. **listKnownHosts()** - Lists all known SSH hosts, prioritizing entries from ~/.ssh/config first, then additional hosts from ~/.ssh/known_hosts
 86 | 2. **runRemoteCommand(hostAlias, command)** - Executes a command on a remote host using `ssh`
 87 | 3. **getHostInfo(hostAlias)** - Returns detailed configuration for a specific host
 88 | 4. **checkConnectivity(hostAlias)** - Tests SSH connectivity to a host
 89 | 5. **uploadFile(hostAlias, localPath, remotePath)** - Uploads a file to the remote host using `scp`
 90 | 6. **downloadFile(hostAlias, remotePath, localPath)** - Downloads a file from the remote host using `scp`
 91 | 7. **runCommandBatch(hostAlias, commands)** - Executes multiple commands sequentially
 92 | 
 93 | ## Configuration Examples
 94 | 
 95 | ### Claude Desktop Integration
 96 | 
 97 | Here's how your Claude Desktop configuration should look:
 98 | 
 99 | ```json
100 | {
101 |   "mcpServers": {
102 |     "mcp-ssh": {
103 |       "command": "npx",
104 |       "args": ["@aiondadotcom/mcp-ssh"]
105 |     }
106 |   }
107 | }
108 | ```
109 | 
110 | ### Manual Server Configuration
111 | 
112 | If you prefer to run the server manually or integrate it with other MCP clients:
113 | 
114 | ```json
115 | {
116 |   "servers": {
117 |     "mcp-ssh": {
118 |       "command": "npx",
119 |       "args": ["@aiondadotcom/mcp-ssh"]
120 |     }
121 |   }
122 | }
123 | ```
124 | 
125 | ## Requirements
126 | 
127 | - Node.js 18 or higher
128 | - SSH client installed (`ssh` and `scp` commands available)
129 | - SSH configuration files (`~/.ssh/config` and `~/.ssh/known_hosts`)
130 | 
131 | ## Usage with Claude Desktop
132 | 
133 | Once configured, you can ask Claude to help you with SSH operations like:
134 | 
135 | - "List all my SSH hosts"
136 | - "Check connectivity to my production server" 
137 | - "Run a command on my web server"
138 | - "Upload this file to my remote server"
139 | - "Download logs from my application server"
140 | 
141 | Claude will use the MCP SSH tools to perform these operations safely and efficiently.
142 | 
143 | ## Usage
144 | 
145 | The agent runs as a Model Context Protocol server over STDIO. When installed via npm, you can use it directly:
146 | 
147 | ```bash
148 | # Run via npx (recommended)
149 | npx @aiondadotcom/mcp-ssh
150 | 
151 | # Or if installed globally
152 | mcp-ssh
153 | 
154 | # For development - run with debug output
155 | npm start
156 | ```
157 | 
158 | The server communicates via clean JSON over STDIO, making it perfect for MCP clients like Claude Desktop.
159 | 
160 | ## Advanced Configuration
161 | 
162 | ### Environment Variables
163 | 
164 | - `MCP_SILENT=true` - Disable debug output (automatically set when used as MCP server)
165 | 
166 | ### SSH Configuration
167 | 
168 | The agent reads from standard SSH configuration files:
169 | - `~/.ssh/config` - SSH client configuration (supports Include directives)
170 | - `~/.ssh/known_hosts` - Known host keys
171 | 
172 | Make sure your SSH keys are properly configured and accessible via SSH agent or key files.
173 | 
174 | #### Include Directive Support
175 | 
176 | 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:
177 | 
178 | **⚠️ SSH Include Directive Bug Warning**
179 | 
180 | 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.
181 | 
182 | **✅ Correct placement (at the beginning):**
183 | ```ssh-config
184 | # ~/.ssh/config
185 | Include ~/.ssh/config.d/*
186 | Include ~/.ssh/work-hosts
187 | 
188 | # Global settings
189 | ServerAliveInterval 55
190 | 
191 | # Host definitions
192 | Host myserver
193 |     HostName example.com
194 | ```
195 | 
196 | **❌ Incorrect placement (at the end) - won't work:**
197 | ```ssh-config
198 | # ~/.ssh/config
199 | # Global settings
200 | ServerAliveInterval 55
201 | 
202 | # Host definitions
203 | Host myserver
204 |     HostName example.com
205 | 
206 | # These Include statements won't work properly due to SSH bug:
207 | Include ~/.ssh/config.d/*
208 | Include ~/.ssh/work-hosts
209 | ```
210 | 
211 | 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.
212 | 
213 | #### Example ~/.ssh/config
214 | 
215 | Here's an example SSH configuration file that demonstrates various connection scenarios including Include directives:
216 | 
217 | ```ssh-config
218 | # Include directives must be at the beginning due to SSH bug
219 | Include ~/.ssh/config.d/*
220 | Include ~/.ssh/work-servers
221 | 
222 | # Global settings - keep connections alive
223 | ServerAliveInterval 55
224 | 
225 | # Production server with jump host
226 | Host prod
227 |     Hostname 203.0.113.10
228 |     Port 22022
229 |     User deploy
230 |     IdentityFile ~/.ssh/id_prod_rsa
231 | 
232 | # Root access to production (separate entry)
233 | Host root@prod
234 |     Hostname 203.0.113.10
235 |     Port 22022
236 |     User root
237 |     IdentityFile ~/.ssh/id_prod_rsa
238 | 
239 | # Archive server accessed through production jump host
240 | Host archive
241 |     Hostname 2001:db8:1f0:cafe::1
242 |     Port 22077
243 |     User archive-user
244 |     ProxyJump prod
245 | 
246 | # Web servers with specific configurations
247 | Host web1.example.com
248 |     Hostname 198.51.100.15
249 |     Port 22022
250 |     User root
251 |     IdentityFile ~/.ssh/id_ed25519
252 | 
253 | Host web2.example.com
254 |     Hostname 198.51.100.25
255 |     Port 22022
256 |     User root
257 |     IdentityFile ~/.ssh/id_ed25519
258 | 
259 | # Database server with custom key
260 | Host database
261 |     Hostname 203.0.113.50
262 |     Port 22077
263 |     User dbadmin
264 |     IdentityFile ~/.ssh/id_database_rsa
265 |     IdentitiesOnly yes
266 | 
267 | # Mail servers
268 | Host mail1
269 |     Hostname 198.51.100.88
270 |     Port 22078
271 |     User mailuser
272 | 
273 | Host root@mail1
274 |     Hostname 198.51.100.88
275 |     Port 22078
276 |     User root
277 | 
278 | # Monitoring server
279 | Host monitor
280 |     Hostname 203.0.113.100
281 |     Port 22077
282 |     User monitoring
283 |     IdentityFile ~/.ssh/id_monitor_ed25519
284 |     IdentitiesOnly yes
285 | 
286 | # Load balancers
287 | Host lb-a
288 |     Hostname 198.51.100.200
289 |     Port 22077
290 |     User root
291 | 
292 | Host lb-b
293 |     Hostname 198.51.100.201
294 |     Port 22077
295 |     User root
296 | ```
297 | 
298 | This configuration demonstrates:
299 | - **Global settings**: `ServerAliveInterval` to keep connections alive
300 | - **Custom ports**: Non-standard SSH ports for security
301 | - **Multiple users**: Different user accounts for the same host (e.g., `prod` and `root@prod`)
302 | - **Jump hosts**: Using `ProxyJump` to access servers through bastion hosts
303 | - **IPv6 addresses**: Modern networking support
304 | - **Identity files**: Specific SSH keys for different servers
305 | - **Security options**: `IdentitiesOnly yes` to use only specified keys
306 | 
307 | #### How MCP SSH Agent Uses Your Configuration
308 | 
309 | The MCP SSH agent automatically discovers and uses your SSH configuration:
310 | 
311 | 1. **Host Discovery**: All hosts from `~/.ssh/config` are automatically available
312 | 2. **Native SSH**: Uses your system's `ssh` command, so all config options work
313 | 3. **Authentication**: Respects your SSH agent, key files, and authentication settings
314 | 4. **Jump Hosts**: Supports complex proxy chains and bastion host setups
315 | 5. **Port Forwarding**: Can work with custom ports and connection options
316 | 
317 | **Example Usage with Claude Desktop:**
318 | - "List my SSH hosts" → Shows all configured hosts including `prod`, `archive`, `web1.example.com`, etc.
319 | - "Connect to archive server" → Uses the ProxyJump configuration automatically
320 | - "Run 'df -h' on web1.example.com" → Connects with the correct user, port, and key
321 | - "Upload file to database server" → Uses the specific identity file and port configuration
322 | 
323 | ## Troubleshooting
324 | 
325 | ### Common Issues
326 | 
327 | 1. **Command not found**: Ensure `ssh` and `scp` are installed and in your PATH
328 | 2. **Permission denied**: Check SSH key permissions and SSH agent
329 | 3. **Host not found**: Verify host exists in `~/.ssh/config` or `~/.ssh/known_hosts`
330 | 4. **Connection timeout**: Check network connectivity and firewall settings
331 | 
332 | ### Debug Mode
333 | 
334 | Run with debug output to see detailed operation logs:
335 | 
336 | ```bash
337 | # Enable debug mode
338 | MCP_SILENT=false npx @aiondadotcom/mcp-ssh
339 | ```
340 | 
341 | ## SSH Key Setup Guide
342 | 
343 | For the MCP SSH Agent to work properly, you need to set up SSH key authentication. Here's a complete guide:
344 | 
345 | ### 1. Creating SSH Keys
346 | 
347 | Generate a new SSH key pair (use Ed25519 for better security):
348 | 
349 | ```bash
350 | # Generate Ed25519 key (recommended)
351 | ssh-keygen -t ed25519 -C "[email protected]"
352 | 
353 | # Or generate RSA key (if Ed25519 is not supported)
354 | ssh-keygen -t rsa -b 4096 -C "[email protected]"
355 | ```
356 | 
357 | **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.
358 | 
359 | ```
360 | Enter passphrase (empty for no passphrase): [Press Enter]
361 | Enter same passphrase again: [Press Enter]
362 | ```
363 | 
364 | This creates two files:
365 | - `~/.ssh/id_ed25519` (private key) - Keep this secret!
366 | - `~/.ssh/id_ed25519.pub` (public key) - This gets copied to servers
367 | 
368 | ### 2. Installing Public Key on Remote Servers
369 | 
370 | Copy your public key to the remote server's authorized_keys file:
371 | 
372 | ```bash
373 | # Method 1: Using ssh-copy-id (easiest)
374 | ssh-copy-id user@hostname
375 | 
376 | # Method 2: Manual copy
377 | cat ~/.ssh/id_ed25519.pub | ssh user@hostname "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
378 | 
379 | # Method 3: Copy and paste manually
380 | cat ~/.ssh/id_ed25519.pub
381 | # Then SSH to the server and paste into ~/.ssh/authorized_keys
382 | ```
383 | 
384 | ### 3. Server-Side SSH Configuration
385 | 
386 | To enable secure key-only authentication on your SSH servers, edit `/etc/ssh/sshd_config`:
387 | 
388 | ```bash
389 | # Edit SSH daemon configuration
390 | sudo nano /etc/ssh/sshd_config
391 | ```
392 | 
393 | Add or modify these settings:
394 | 
395 | ```ssh-config
396 | # Enable public key authentication
397 | PubkeyAuthentication yes
398 | AuthorizedKeysFile .ssh/authorized_keys
399 | 
400 | # Disable password authentication (security best practice)
401 | PasswordAuthentication no
402 | ChallengeResponseAuthentication no
403 | UsePAM no
404 | 
405 | # Root login options (choose one):
406 | # Option 1: Allow root login with SSH keys only (recommended for admin access)
407 | PermitRootLogin prohibit-password
408 | 
409 | # Option 2: Completely disable root login (most secure, but less flexible)
410 | # PermitRootLogin no
411 | 
412 | # Optional: Restrict SSH to specific users
413 | AllowUsers deploy root admin
414 | 
415 | # Optional: Change default port for security
416 | Port 22022
417 | ```
418 | 
419 | After editing, restart the SSH service:
420 | 
421 | ```bash
422 | # On Ubuntu/Debian
423 | sudo systemctl restart ssh
424 | 
425 | # On CentOS/RHEL/Fedora
426 | sudo systemctl restart sshd
427 | 
428 | # On macOS
429 | sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist
430 | sudo launchctl load /System/Library/LaunchDaemons/ssh.plist
431 | ```
432 | 
433 | ### 4. Setting Correct Permissions
434 | 
435 | SSH is very strict about file permissions. Set them correctly:
436 | 
437 | **On your local machine:**
438 | ```bash
439 | chmod 700 ~/.ssh
440 | chmod 600 ~/.ssh/id_ed25519
441 | chmod 644 ~/.ssh/id_ed25519.pub
442 | chmod 644 ~/.ssh/config
443 | chmod 644 ~/.ssh/known_hosts
444 | ```
445 | 
446 | **On the remote server:**
447 | ```bash
448 | chmod 700 ~/.ssh
449 | chmod 600 ~/.ssh/authorized_keys
450 | ```
451 | 
452 | ### 5. Testing SSH Key Authentication
453 | 
454 | Test your connection before using with MCP SSH Agent:
455 | 
456 | ```bash
457 | # Test connection
458 | ssh -i ~/.ssh/id_ed25519 user@hostname
459 | 
460 | # Test with verbose output for debugging
461 | ssh -v -i ~/.ssh/id_ed25519 user@hostname
462 | 
463 | # Test specific configuration
464 | ssh -F ~/.ssh/config hostname
465 | ```
466 | 
467 | ### 6. Multiple Keys for Different Servers
468 | 
469 | You can create different keys for different servers:
470 | 
471 | ```bash
472 | # Create specific keys
473 | ssh-keygen -t ed25519 -f ~/.ssh/id_production -C "production-server"
474 | ssh-keygen -t ed25519 -f ~/.ssh/id_staging -C "staging-server"
475 | ```
476 | 
477 | Then configure them in `~/.ssh/config`:
478 | 
479 | ```ssh-config
480 | Host production
481 |     Hostname prod.example.com
482 |     User deploy
483 |     IdentityFile ~/.ssh/id_production
484 |     IdentitiesOnly yes
485 | 
486 | Host staging
487 |     Hostname staging.example.com
488 |     User deploy
489 |     IdentityFile ~/.ssh/id_staging
490 |     IdentitiesOnly yes
491 | ```
492 | 
493 | ## Security Best Practices
494 | 
495 | ### SSH Key Security
496 | - **Never use password-protected keys** with MCP SSH Agent
497 | - **Never share private keys** - they should stay on your machine only
498 | - **Use Ed25519 keys** when possible (more secure than RSA)
499 | - **Create separate keys** for different environments/purposes
500 | - **Regularly rotate keys** (every 6-12 months)
501 | 
502 | ### Server Security
503 | - **Disable password authentication** completely
504 | - **Use non-standard SSH ports** to reduce automated attacks
505 | - **Limit SSH access** to specific users with `AllowUsers`
506 | - **Choose appropriate root login policy**:
507 |   - `PermitRootLogin prohibit-password` - Allows root access with SSH keys only (recommended for admin tasks)
508 |   - `PermitRootLogin no` - Completely disables root login (most secure, but requires sudo access)
509 | - **Enable SSH key-only authentication** for all accounts
510 | - **Consider using jump hosts** for additional security layers
511 | 
512 | ### Network Security
513 | - **Use VPN or bastion hosts** for production servers
514 | - **Implement fail2ban** to block brute force attempts
515 | - **Monitor SSH logs** regularly
516 | - **Use SSH key forwarding carefully** (disable when not needed)
517 | 
518 | ## Building Desktop Extensions
519 | 
520 | For developers who want to build DXT packages locally:
521 | 
522 | ### Prerequisites
523 | 
524 | - Node.js 18 or higher
525 | - npm
526 | 
527 | ### Building DXT Package
528 | 
529 | ```bash
530 | # Install dependencies
531 | npm install
532 | 
533 | # Build the DXT package
534 | npm run build:dxt
535 | ```
536 | 
537 | This creates a `.dxt` file in the `build/` directory that can be installed in Claude Desktop.
538 | 
539 | ### Publishing DXT Releases
540 | 
541 | To publish a new DXT release:
542 | 
543 | ```bash
544 | # Build the DXT package
545 | npm run build:dxt
546 | 
547 | # Create a GitHub release with the DXT file
548 | 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"
549 | ```
550 | 
551 | The DXT file will be available as a release asset for users to download and install.
552 | 
553 | ## Contributing
554 | 
555 | Contributions are welcome! Please feel free to submit a Pull Request.
556 | 
557 | ## License
558 | 
559 | MIT License - see LICENSE file for details.
560 | 
561 | ## Project Structure
562 | 
563 | ```
564 | mcp-ssh/
565 | ├── server-simple.mjs          # Main MCP server implementation
566 | ├── manifest.json              # DXT package manifest
567 | ├── package.json               # Dependencies and scripts
568 | ├── README.md                  # Documentation
569 | ├── LICENSE                    # MIT License
570 | ├── CHANGELOG.md               # Release history
571 | ├── PUBLISHING.md              # Publishing instructions
572 | ├── start.sh                   # Development startup script
573 | ├── start-silent.sh            # Silent startup script
574 | ├── scripts/
575 | │   └── build-dxt.sh           # DXT package build script
576 | ├── doc/
577 | │   ├── example.png            # Usage example screenshot
578 | │   └── Claude.png             # Claude Desktop integration example
579 | ├── src/                       # TypeScript source files (development)
580 | │   ├── ssh-client.ts          # SSH operations implementation
581 | │   ├── ssh-config-parser.ts   # SSH configuration parsing
582 | │   └── types.ts               # Type definitions
583 | └── tsconfig.json              # TypeScript configuration
584 | ```
585 | 
586 | ## About
587 | 
588 | 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.
589 | 
```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # CLAUDE.md
  2 | 
  3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
  4 | 
  5 | ## Project Overview
  6 | 
  7 | 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.
  8 | 
  9 | ## Development Commands
 10 | 
 11 | ### Basic Operations
 12 | - `npm start` - Start the MCP server (same as `npm run dev`)
 13 | - `npm run dev` - Start the MCP server with debug output
 14 | - `npm run build` - Currently a no-op (echo "Build skipped")
 15 | - `npm test` - Currently a no-op (echo "No tests specified")
 16 | 
 17 | ### Development Scripts
 18 | - `./start.sh` - Start the server with debug output
 19 | - `./start-silent.sh` - Start the server in silent mode (no debug output)
 20 | - `node server-simple.mjs` - Direct server execution
 21 | 
 22 | ### Publishing
 23 | - `npm version patch|minor|major` - Bump version and create git tag
 24 | - `npm publish` - Publish to npm (see PUBLISHING.md for details)
 25 | - `npm pack` - Create tarball for testing
 26 | 
 27 | ### DXT Package Building
 28 | - `npm run build:dxt` - Build Desktop Extension (.dxt) package
 29 | - `./scripts/build-dxt.sh` - Direct build script execution
 30 | 
 31 | ## Architecture
 32 | 
 33 | ### Main Entry Point
 34 | - `server-simple.mjs` - Self-contained MCP server implementation that includes all functionality inline to avoid module resolution issues
 35 | 
 36 | ### Source Structure (Development)
 37 | - `src/` - TypeScript source files (currently not compiled/used in production)
 38 |   - `ssh-client.ts` - SSH operations using node-ssh library (development version)
 39 |   - `ssh-config-parser.ts` - SSH config parsing utilities
 40 |   - `types.ts` - TypeScript type definitions
 41 | - `bin/mcp-ssh.js` - Binary wrapper for npx compatibility
 42 | 
 43 | ### Key Design Decisions
 44 | 1. **Native SSH Tools**: Uses system `ssh` and `scp` commands rather than JavaScript SSH libraries for reliability
 45 | 2. **Self-contained**: `server-simple.mjs` includes all code inline to avoid ESM import issues
 46 | 3. **Dual Implementation**: TypeScript source in `src/` for development, JavaScript implementation in `server-simple.mjs` for production
 47 | 4. **Silent Mode**: Controlled by `MCP_SILENT` environment variable to disable debug output when used as MCP server
 48 | 
 49 | ## SSH Configuration Integration
 50 | 
 51 | The agent automatically discovers SSH hosts from:
 52 | - `~/.ssh/config` - Primary source for host configurations
 53 | - `~/.ssh/known_hosts` - Additional hosts not in config
 54 | 
 55 | Host discovery prioritizes SSH config entries first, then adds additional hosts from known_hosts.
 56 | 
 57 | ## MCP Tools Provided
 58 | 
 59 | 1. **listKnownHosts()** - Lists all discovered SSH hosts
 60 | 2. **runRemoteCommand(hostAlias, command)** - Execute commands via SSH
 61 | 3. **getHostInfo(hostAlias)** - Get host configuration details
 62 | 4. **checkConnectivity(hostAlias)** - Test SSH connectivity
 63 | 5. **uploadFile(hostAlias, localPath, remotePath)** - Upload files via SCP
 64 | 6. **downloadFile(hostAlias, remotePath, localPath)** - Download files via SCP
 65 | 7. **runCommandBatch(hostAlias, commands)** - Execute multiple commands sequentially
 66 | 
 67 | ## Testing and Debugging
 68 | 
 69 | ### Manual Testing
 70 | ```bash
 71 | # Test as MCP server
 72 | npx @aiondadotcom/mcp-ssh
 73 | 
 74 | # Test with debug output
 75 | MCP_SILENT=false npx @aiondadotcom/mcp-ssh
 76 | 
 77 | # Test installation
 78 | npm pack
 79 | npm install -g ./aiondadotcom-mcp-ssh-*.tgz
 80 | mcp-ssh
 81 | ```
 82 | 
 83 | ### Integration Testing
 84 | Configure in Claude Desktop's `claude_desktop_config.json`:
 85 | ```json
 86 | {
 87 |   "mcpServers": {
 88 |     "mcp-ssh": {
 89 |       "command": "npx",
 90 |       "args": ["@aiondadotcom/mcp-ssh"]
 91 |     }
 92 |   }
 93 | }
 94 | ```
 95 | 
 96 | ## Dependencies
 97 | 
 98 | - `@modelcontextprotocol/sdk` - MCP protocol implementation
 99 | - `ssh-config` - SSH configuration file parsing
100 | - Node.js built-ins: `child_process`, `fs/promises`, `os`, `path`
101 | 
102 | ## Desktop Extension Support
103 | 
104 | The project supports Desktop Extensions (.dxt) for easy installation in Claude Desktop:
105 | 
106 | - `manifest.json` - DXT package manifest with server configuration
107 | - `scripts/build-dxt.sh` - Build script that creates .dxt packages in `build/` directory
108 | - `.dxt` files are ZIP archives containing the manifest and server files
109 | - Built packages are excluded from git via `.gitignore` but can be uploaded to GitHub releases
110 | 
111 | ## Important Notes
112 | 
113 | - The project is ESM-only (`"type": "module"` in package.json)
114 | - Production code is in `server-simple.mjs`, not compiled from TypeScript
115 | - SSH operations require properly configured SSH keys and host access
116 | - The agent runs over STDIO as an MCP server, not as a standalone application
117 | - DXT packages provide one-click installation alternative to manual JSON configuration
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | 
```

--------------------------------------------------------------------------------
/claude_desktop_config.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "mcpServers": {
3 |     "mcp-ssh": {
4 |       "command": "/absolute/path/to/mcp-ssh/start.sh"
5 |     }
6 |   }
7 | }
```

--------------------------------------------------------------------------------
/start-silent.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Change to the directory where this script is located
 4 | cd "$(dirname "$0")"
 5 | 
 6 | # MCP SSH Agent Silent Startup Script
 7 | # This script starts the MCP SSH server in silent mode for MCP clients
 8 | # No debug output will be shown, only clean JSON communication
 9 | 
10 | export MCP_SILENT=true
11 | exec node server-simple.mjs
12 | 
```

--------------------------------------------------------------------------------
/bin/mcp-ssh.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | 
 3 | // Simple wrapper to run the main server-simple.mjs file
 4 | import path from 'path';
 5 | import { fileURLToPath } from 'url';
 6 | 
 7 | const __filename = fileURLToPath(import.meta.url);
 8 | const __dirname = path.dirname(__filename);
 9 | 
10 | // Import and run the main module
11 | const mainModule = path.join(__dirname, '..', 'server-simple.mjs');
12 | import(mainModule);
13 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "outDir": "./dist",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "resolveJsonModule": true,
13 |     "allowSyntheticDefaultImports": true,
14 |     "typeRoots": ["./node_modules/@types"]
15 |   },
16 |   "include": ["src/ssh-client.ts", "src/ssh-config-parser.ts", "src/types.ts"],
17 |   "exclude": ["node_modules"]
18 | }
19 | 
```

--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Change to the directory where this script is located
 4 | cd "$(dirname "$0")"
 5 | 
 6 | # MCP SSH Agent Startup Script
 7 | # This script starts the MCP SSH server using npm
 8 | # Set MCP_SILENT=true to disable debug output for MCP clients
 9 | 
10 | # Check if we should run in silent mode (for MCP clients)
11 | if [ "$1" = "--silent" ] || [ "$MCP_SILENT" = "true" ]; then
12 |     # Silent mode - no startup messages
13 |     MCP_SILENT=true npm start
14 | else
15 |     # Normal mode with startup messages
16 |     echo "Starting MCP SSH Agent..."
17 |     npm start
18 | fi
19 | 
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // Types for SSH Host information
 2 | export interface SSHHostInfo {
 3 |   hostname: string;
 4 |   alias?: string;
 5 |   user?: string;
 6 |   port?: number;
 7 |   identityFile?: string;
 8 |   [key: string]: any; // For other configuration options
 9 | }
10 | 
11 | // Result of a remote command
12 | export interface CommandResult {
13 |   stdout: string;
14 |   stderr: string;
15 |   code: number;
16 | }
17 | 
18 | // SSH connection status
19 | export interface ConnectionStatus {
20 |   connected: boolean;
21 |   message: string;
22 | }
23 | 
24 | // Batch result of remote commands
25 | export interface BatchCommandResult {
26 |   results: CommandResult[];
27 |   success: boolean;
28 | }
29 | 
```

--------------------------------------------------------------------------------
/temp-extract/manifest.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "dxt_version": "0.1",
 3 |   "name": "mcp-ssh",
 4 |   "version": "1.0.3",
 5 |   "description": "Connect to SSH hosts, run commands, and transfer files securely through Claude Desktop",
 6 |   "author": {
 7 |     "name": "aionda.com",
 8 |     "url": "https://aionda.com"
 9 |   },
10 |   "license": "MIT",
11 |   "homepage": "https://github.com/aiondadotcom/mcp-ssh",
12 |   "repository": {
13 |     "type": "git",
14 |     "url": "https://github.com/aiondadotcom/mcp-ssh.git"
15 |   },
16 |   "icon": "doc/Claude.png",
17 |   "keywords": [
18 |     "ssh",
19 |     "remote",
20 |     "server",
21 |     "scp",
22 |     "file-transfer",
23 |     "automation",
24 |     "devops"
25 |   ],
26 |   "server": {
27 |     "type": "node",
28 |     "entry_point": "server-simple.mjs",
29 |     "mcp_config": {
30 |       "command": "node",
31 |       "args": ["server-simple.mjs"],
32 |       "env": {
33 |         "MCP_SILENT": "true"
34 |       }
35 |     }
36 |   }
37 | }
```

--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "dxt_version": "0.1",
 3 |   "name": "mcp-ssh",
 4 |   "version": "1.0.3",
 5 |   "description": "Connect to SSH hosts, run commands, and transfer files securely through Claude Desktop",
 6 |   "author": {
 7 |     "name": "aionda.com",
 8 |     "url": "https://aionda.com"
 9 |   },
10 |   "license": "MIT",
11 |   "homepage": "https://github.com/aiondadotcom/mcp-ssh",
12 |   "repository": {
13 |     "type": "git",
14 |     "url": "https://github.com/aiondadotcom/mcp-ssh.git"
15 |   },
16 |   "icon": "doc/Claude.png",
17 |   "keywords": [
18 |     "ssh",
19 |     "remote",
20 |     "server",
21 |     "scp",
22 |     "file-transfer",
23 |     "automation",
24 |     "devops"
25 |   ],
26 |   "server": {
27 |     "type": "node",
28 |     "entry_point": "server-simple.mjs",
29 |     "mcp_config": {
30 |       "command": "node",
31 |       "args": ["${__dirname}/server-simple.mjs"],
32 |       "env": {
33 |         "MCP_SILENT": "true"
34 |       }
35 |     }
36 |   }
37 | }
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish to NPM
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [published]
 6 |   workflow_dispatch:
 7 |     inputs:
 8 |       version:
 9 |         description: 'Version to publish (e.g., patch, minor, major)'
10 |         required: true
11 |         default: 'patch'
12 |         type: choice
13 |         options:
14 |           - patch
15 |           - minor
16 |           - major
17 | 
18 | jobs:
19 |   publish:
20 |     runs-on: ubuntu-latest
21 |     
22 |     steps:
23 |       - name: Checkout code
24 |         uses: actions/checkout@v4
25 |         
26 |       - name: Setup Node.js
27 |         uses: actions/setup-node@v4
28 |         with:
29 |           node-version: '18'
30 |           registry-url: 'https://registry.npmjs.org'
31 |           
32 |       - name: Install dependencies
33 |         run: npm ci
34 |         
35 |       - name: Run tests (if any)
36 |         run: npm test --if-present
37 |         
38 |       - name: Bump version (if manual trigger)
39 |         if: github.event_name == 'workflow_dispatch'
40 |         run: npm version ${{ github.event.inputs.version }} --no-git-tag-version
41 |         
42 |       - name: Publish to NPM
43 |         run: npm publish
44 |         env:
45 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
46 | 
```

--------------------------------------------------------------------------------
/gist_comment.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "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!"
3 | }
```

--------------------------------------------------------------------------------
/github_issue_response.md:
--------------------------------------------------------------------------------

```markdown
 1 | 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.
 2 | 
 3 | **Implementation Plan:**
 4 | We'll add proper `Include` directive support to ensure complete host discovery from all SSH configuration files.
 5 | 
 6 | **Important SSH Configuration Note:**
 7 | 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. 
 8 | 
 9 | **Example of correct placement:**
10 | ```
11 | # ~/.ssh/config
12 | Include ~/.ssh/config.d/*
13 | Include ~/.ssh/work-hosts
14 | 
15 | # Global settings
16 | ServerAliveInterval 55
17 | 
18 | # Host definitions
19 | Host myserver
20 |     HostName example.com
21 | ```
22 | 
23 | **Why this matters:**
24 | 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.
25 | 
26 | **Our solution:**
27 | 1. We'll implement `Include` support in the MCP SSH Agent to read all configuration files
28 | 2. We'll document this SSH quirk in our README to help users avoid configuration issues
29 | 3. The agent will work correctly regardless of where users place their `Include` statements
30 | 
31 | We'll have this feature implemented soon. Thanks for bringing this to our attention!
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@aiondadotcom/mcp-ssh",
 3 |   "version": "1.1.0",
 4 |   "description": "MCP Agent for managing SSH hosts - A Model Context Protocol server for SSH operations",
 5 |   "main": "server-simple.mjs",
 6 |   "bin": {
 7 |     "mcp-ssh": "bin/mcp-ssh.js"
 8 |   },
 9 |   "type": "module",
10 |   "scripts": {
11 |     "start": "node server-simple.mjs",
12 |     "dev": "node server-simple.mjs",
13 |     "build": "echo \"Build skipped\"",
14 |     "build:dxt": "./scripts/build-dxt.sh",
15 |     "test": "echo \"No tests specified\" && exit 0",
16 |     "prepublishOnly": "npm run test",
17 |     "version": "git add -A",
18 |     "postversion": "git push && git push --tags"
19 |   },
20 |   "keywords": [
21 |     "mcp",
22 |     "ssh",
23 |     "agent",
24 |     "model-context-protocol",
25 |     "claude",
26 |     "ai",
27 |     "remote",
28 |     "server"
29 |   ],
30 |   "author": "aionda.com",
31 |   "license": "MIT",
32 |   "repository": {
33 |     "type": "git",
34 |     "url": "git+https://github.com/aiondadotcom/mcp-ssh.git"
35 |   },
36 |   "homepage": "https://github.com/aiondadotcom/mcp-ssh",
37 |   "bugs": {
38 |     "url": "https://github.com/aiondadotcom/mcp-ssh/issues"
39 |   },
40 |   "publishConfig": {
41 |     "access": "public"
42 |   },
43 |   "dependencies": {
44 |     "@modelcontextprotocol/sdk": "^1.12.0",
45 |     "glob": "^11.0.3",
46 |     "ssh-config": "^5.0.0"
47 |   },
48 |   "devDependencies": {
49 |     "@anthropic-ai/dxt": "^0.2.5",
50 |     "@types/node": "^20.11.26",
51 |     "@types/ssh2": "^1.15.0",
52 |     "tmp": ">=0.2.4",
53 |     "ts-node": "^10.9.2",
54 |     "typescript": "^5.4.3"
55 |   },
56 |   "overrides": {
57 |     "tmp": ">=0.2.4"
58 |   }
59 | }
60 | 
```

--------------------------------------------------------------------------------
/PUBLISHING.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Publishing Instructions
 2 | 
 3 | This document contains instructions for publishing the @aiondadotcom/mcp-ssh package to npm.
 4 | 
 5 | ## Prerequisites
 6 | 
 7 | 1. You need to be a member of the @aiondadotcom organization on npm
 8 | 2. You need to be logged in to npm: `npm login`
 9 | 3. Verify your access: `npm access list packages @aiondadotcom`
10 | 
11 | ## Publishing Process
12 | 
13 | ### Automated Publishing (Recommended)
14 | 
15 | The package is automatically published when you create a GitHub release:
16 | 
17 | 1. Commit all changes
18 | 2. Create a new release on GitHub
19 | 3. The GitHub Action will automatically publish to npm
20 | 
21 | ### Manual Publishing
22 | 
23 | ```bash
24 | # 1. Make sure you're on the main branch and everything is committed
25 | git checkout main
26 | git pull origin main
27 | 
28 | # 2. Bump the version (patch, minor, or major)
29 | npm version patch  # or minor/major
30 | 
31 | # 3. Publish to npm
32 | npm publish
33 | 
34 | # 4. Push the version commit and tag
35 | git push origin main --tags
36 | ```
37 | 
38 | ### Testing Before Publishing
39 | 
40 | ```bash
41 | # Test the package locally
42 | npm pack
43 | npm install -g ./aiondadotcom-mcp-ssh-1.0.0.tgz
44 | 
45 | # Test the binary
46 | mcp-ssh --help
47 | 
48 | # Clean up
49 | npm uninstall -g @aiondadotcom/mcp-ssh
50 | rm *.tgz
51 | ```
52 | 
53 | ## First-Time Setup
54 | 
55 | If this is the first time publishing this package:
56 | 
57 | ```bash
58 | # Login to npm
59 | npm login
60 | 
61 | # Verify you have access to the @aiondadotcom scope
62 | npm access list packages @aiondadotcom
63 | 
64 | # Publish the package
65 | npm publish --access public
66 | ```
67 | 
68 | ## Package Configuration
69 | 
70 | The package is configured with:
71 | - Scoped name: `@aiondadotcom/mcp-ssh`
72 | - Public access
73 | - Binary: `mcp-ssh` command
74 | - Entry point: `server-simple.mjs`
75 | 
```

--------------------------------------------------------------------------------
/scripts/build-dxt.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Build script for creating DXT packages for mcp-ssh
 4 | # This script creates .dxt files for distribution without committing them to the repository
 5 | 
 6 | set -e
 7 | 
 8 | # Colors for output
 9 | RED='\033[0;31m'
10 | GREEN='\033[0;32m'
11 | YELLOW='\033[1;33m'
12 | NC='\033[0m' # No Color
13 | 
14 | echo -e "${GREEN}Building MCP SSH DXT Package${NC}"
15 | 
16 | # Check if dxt CLI is available
17 | if ! command -v npx &> /dev/null; then
18 |     echo -e "${RED}Error: npm/npx not found. Please install Node.js${NC}"
19 |     exit 1
20 | fi
21 | 
22 | # Check if we have the dxt package
23 | if ! npm list @anthropic-ai/dxt &> /dev/null; then
24 |     echo -e "${RED}Error: @anthropic-ai/dxt not found. Please run 'npm install'${NC}"
25 |     exit 1
26 | fi
27 | 
28 | # Create build directory (not tracked in git)
29 | BUILD_DIR="build"
30 | rm -rf "$BUILD_DIR"
31 | mkdir -p "$BUILD_DIR"
32 | 
33 | echo -e "${YELLOW}Creating DXT package...${NC}"
34 | 
35 | # Get version from package.json
36 | VERSION=$(node -p "require('./package.json').version")
37 | DXT_FILE="mcp-ssh-${VERSION}.dxt"
38 | 
39 | # Create the DXT package
40 | npx dxt pack . "$BUILD_DIR/$DXT_FILE"
41 | 
42 | if [ $? -eq 0 ]; then
43 |     echo -e "${GREEN}✓ DXT package created successfully: $BUILD_DIR/$DXT_FILE${NC}"
44 |     echo -e "${GREEN}✓ Package size: $(ls -lh "$BUILD_DIR/$DXT_FILE" | awk '{print $5}')${NC}"
45 |     
46 |     # Display next steps
47 |     echo -e "\n${YELLOW}Next steps:${NC}"
48 |     echo "1. Test the DXT package locally"
49 |     echo "2. Upload to GitHub releases:"
50 |     echo "   gh release create v${VERSION} $BUILD_DIR/$DXT_FILE --title 'Release v${VERSION}' --notes 'MCP SSH Agent v${VERSION}'"
51 |     echo "3. Or upload manually to GitHub releases page"
52 | else
53 |     echo -e "${RED}✗ Failed to create DXT package${NC}"
54 |     exit 1
55 | fi
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | All notable changes to this project will be documented in this file.
 4 | 
 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 7 | 
 8 | ## [1.1.0] - 2025-08-17
 9 | 
10 | ### Added
11 | - **NEW FEATURE**: SSH config Include directive support
12 | - Added recursive processing of Include directives in SSH configuration files
13 | - Support for glob patterns in Include paths (e.g., `Include ~/.ssh/configs/*`)
14 | - Enhanced SSH host discovery from included configuration files
15 | - Added `glob` dependency for Include path pattern matching
16 | 
17 | ### Enhanced
18 | - Improved SSH configuration parsing to handle complex Include hierarchies
19 | - Enhanced host discovery to recursively process all included config files
20 | - Better error handling for malformed or inaccessible Include files
21 | 
22 | ## [1.0.4] - 2025-08-17
23 | 
24 | ### Security
25 | - **SECURITY FIX**: Fixed command injection vulnerability in SSH operations (commit 5b9b9c5)
26 | - **SECURITY FIX**: Upgraded `tmp` dependency to version 0.2.5 to address CVE vulnerability
27 | - Fixed arbitrary temporary file/directory write via symbolic link in `tmp` package (GHSA-52f5-9888-hmc6)
28 | - Added dependency overrides to ensure all transitive dependencies use secure `tmp` version
29 | - Enhanced input validation and sanitization for SSH commands and file paths
30 | 
31 | ### Technical
32 | - Added `tmp: ">=0.2.4"` to devDependencies to force secure version
33 | - Added npm overrides configuration to enforce secure tmp version across entire dependency tree
34 | - Updated package-lock.json to reflect security fixes
35 | 
36 | ## [1.0.3] - 2025-06-06
37 | 
38 | ### Added
39 | - Binary wrapper script (`bin/mcp-ssh.js`) for proper npx compatibility
40 | - Fixed npx execution issues by implementing wrapper pattern
41 | 
42 | ### Fixed
43 | - NPX executable resolution using wrapper script approach
44 | - Package binary configuration now points to proper wrapper
45 | 
46 | ### Technical
47 | - Added `bin/mcp-ssh.js` wrapper to handle npx execution
48 | - Updated package.json bin configuration to use wrapper script
49 | 
50 | ## [1.0.2] - 2025-06-06
51 | 
52 | ### Fixed
53 | - Build script temporary fix
54 | - File permissions for executable
55 | 
56 | ## [1.0.1] - 2025-06-06
57 | 
58 | ### Fixed
59 | - Initial package configuration
60 | - File permissions
61 | 
62 | ## [1.0.0] - 2025-06-06
63 | 
64 | ### Added
65 | - Initial release of MCP SSH Agent
66 | - Support for all SSH operations via native ssh/scp commands
67 | - Automatic SSH host discovery from ~/.ssh/config and ~/.ssh/known_hosts
68 | - Functions: listKnownHosts, runRemoteCommand, getHostInfo, checkConnectivity, uploadFile, downloadFile, runCommandBatch
69 | - Claude Desktop integration support
70 | - NPM package distribution via @aiondadotcom/mcp-ssh
71 | - npx compatibility for easy installation and usage
72 | 
73 | ### Features
74 | - Native SSH command execution for maximum compatibility
75 | - Silent mode for MCP clients (MCP_SILENT=true)
76 | - Comprehensive error handling with timeouts
77 | - Batch command execution support
78 | - File upload/download via scp
79 | - SSH connectivity testing
80 | 
81 | ### Documentation
82 | - Complete README with Claude Desktop setup instructions
83 | - Usage examples and troubleshooting guide
84 | - Professional npm package configuration
85 | 
```

--------------------------------------------------------------------------------
/src/ssh-config-parser.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { readFile } from 'fs/promises';
  2 | import { homedir } from 'os';
  3 | import { join } from 'path';
  4 | import * as sshConfig from 'ssh-config';
  5 | import { SSHHostInfo } from './types.js';
  6 | 
  7 | export class SSHConfigParser {
  8 |   private configPath: string;
  9 |   private knownHostsPath: string;
 10 | 
 11 |   constructor() {
 12 |     const homeDir = homedir();
 13 |     this.configPath = join(homeDir, '.ssh', 'config');
 14 |     this.knownHostsPath = join(homeDir, '.ssh', 'known_hosts');
 15 |   }
 16 | 
 17 |   /**
 18 |    * Reads and parses the SSH config file
 19 |    */
 20 |   async parseConfig(): Promise<SSHHostInfo[]> {
 21 |     try {
 22 |       const content = await readFile(this.configPath, 'utf-8');
 23 |       const config = sshConfig.parse(content);
 24 |       return this.extractHostsFromConfig(config);
 25 |     } catch (error) {
 26 |       console.error('Error reading SSH config:', error);
 27 |       return [];
 28 |     }
 29 |   }
 30 | 
 31 |   /**
 32 |    * Extracts host information from SSH Config
 33 |    */
 34 |   private extractHostsFromConfig(config: any[]): SSHHostInfo[] {
 35 |     const hosts: SSHHostInfo[] = [];
 36 | 
 37 |     for (const section of config) {
 38 |       if (section.param === 'Host' && section.value !== '*') {
 39 |         const hostInfo: SSHHostInfo = {
 40 |           hostname: '',
 41 |           alias: section.value,
 42 |         };
 43 | 
 44 |         // Search all entries for this host
 45 |         for (const param of section.config) {
 46 |           switch (param.param.toLowerCase()) {
 47 |             case 'hostname':
 48 |               hostInfo.hostname = param.value;
 49 |               break;
 50 |             case 'user':
 51 |               hostInfo.user = param.value;
 52 |               break;
 53 |             case 'port':
 54 |               hostInfo.port = parseInt(param.value, 10);
 55 |               break;
 56 |             case 'identityfile':
 57 |               hostInfo.identityFile = param.value;
 58 |               break;
 59 |             default:
 60 |               // Store other parameters
 61 |               hostInfo[param.param.toLowerCase()] = param.value;
 62 |           }
 63 |         }
 64 | 
 65 |         // Only add hosts with complete information
 66 |         if (hostInfo.hostname) {
 67 |           hosts.push(hostInfo);
 68 |         }
 69 |       }
 70 |     }
 71 | 
 72 |     return hosts;
 73 |   }
 74 | 
 75 |   /**
 76 |    * Reads the known_hosts file and extracts hostnames
 77 |    */
 78 |   async parseKnownHosts(): Promise<string[]> {
 79 |     try {
 80 |       const content = await readFile(this.knownHostsPath, 'utf-8');
 81 |       const knownHosts = content
 82 |         .split('\n')
 83 |         .filter(line => line.trim() !== '')
 84 |         .map(line => {
 85 |           // Format: hostname[,hostname2...] key-type public-key
 86 |           const parts = line.split(' ')[0];
 87 |           return parts.split(',')[0];
 88 |         });
 89 | 
 90 |       return knownHosts;
 91 |     } catch (error) {
 92 |       console.error('Error reading known_hosts file:', error);
 93 |       return [];
 94 |     }
 95 |   }
 96 | 
 97 |   /**
 98 |    * Consolidates information from config and known_hosts
 99 |    */
100 |   async getAllKnownHosts(): Promise<SSHHostInfo[]> {
101 |     const configHosts = await this.parseConfig();
102 |     const knownHostnames = await this.parseKnownHosts();
103 | 
104 |     // Add hosts from known_hosts that aren't in the config
105 |     for (const hostname of knownHostnames) {
106 |       if (!configHosts.some(host => 
107 |           host.hostname === hostname || 
108 |           host.alias === hostname)) {
109 |         configHosts.push({
110 |           hostname: hostname
111 |         });
112 |       }
113 |     }
114 | 
115 |     return configHosts;
116 |   }
117 | }
118 | 
```

--------------------------------------------------------------------------------
/src/ssh-client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { NodeSSH } from 'node-ssh';
  2 | import { SSHHostInfo, CommandResult, ConnectionStatus, BatchCommandResult } from './types.js';
  3 | import { SSHConfigParser } from './ssh-config-parser.js';
  4 | 
  5 | export class SSHClient {
  6 |   private ssh: NodeSSH;
  7 |   private configParser: SSHConfigParser;
  8 | 
  9 |   constructor() {
 10 |     this.ssh = new NodeSSH();
 11 |     this.configParser = new SSHConfigParser();
 12 |   }
 13 | 
 14 |   /**
 15 |    * Lists all known SSH hosts
 16 |    */
 17 |   async listKnownHosts(): Promise<SSHHostInfo[]> {
 18 |     return await this.configParser.getAllKnownHosts();
 19 |   }
 20 | 
 21 |   /**
 22 |    * Connects to an SSH host and executes a command
 23 |    */
 24 |   async runRemoteCommand(hostAlias: string, command: string): Promise<CommandResult> {
 25 |     try {
 26 |       // First connect to the host
 27 |       await this.connectToHost(hostAlias);
 28 | 
 29 |       // Execute the command
 30 |       const result = await this.ssh.execCommand(command);
 31 |       
 32 |       return {
 33 |         stdout: result.stdout,
 34 |         stderr: result.stderr,
 35 |         code: result.code || 0
 36 |       };
 37 |     } catch (error) {
 38 |       console.error(`Error executing command on ${hostAlias}:`, error);
 39 |       return {
 40 |         stdout: '',
 41 |         stderr: error instanceof Error ? error.message : String(error),
 42 |         code: 1
 43 |       };
 44 |     } finally {
 45 |       this.ssh.dispose();
 46 |     }
 47 |   }
 48 | 
 49 |   /**
 50 |    * Returns all details about a host
 51 |    */
 52 |   async getHostInfo(hostAlias: string): Promise<SSHHostInfo | null> {
 53 |     const hosts = await this.configParser.parseConfig();
 54 |     return hosts.find(host => host.alias === hostAlias || host.hostname === hostAlias) || null;
 55 |   }
 56 | 
 57 |   /**
 58 |    * Checks if a connection to the host is possible
 59 |    */
 60 |   async checkConnectivity(hostAlias: string): Promise<ConnectionStatus> {
 61 |     try {
 62 |       // Establish connection
 63 |       await this.connectToHost(hostAlias);
 64 |       
 65 |       // Execute ping command
 66 |       const result = await this.ssh.execCommand('echo connected');
 67 |       
 68 |       const connected = result.stdout.trim() === 'connected';
 69 |       
 70 |       this.ssh.dispose();
 71 |       
 72 |       return {
 73 |         connected,
 74 |         message: connected ? 'Connection successful' : 'Echo test failed'
 75 |       };
 76 |     } catch (error) {
 77 |       console.error(`Connectivity error with ${hostAlias}:`, error);
 78 |       return {
 79 |         connected: false,
 80 |         message: error instanceof Error ? error.message : String(error)
 81 |       };
 82 |     }
 83 |   }
 84 | 
 85 |   /**
 86 |    * Uploads a file to the remote host
 87 |    */
 88 |   async uploadFile(hostAlias: string, localPath: string, remotePath: string): Promise<boolean> {
 89 |     try {
 90 |       await this.connectToHost(hostAlias);
 91 |       
 92 |       await this.ssh.putFile(localPath, remotePath);
 93 |       
 94 |       this.ssh.dispose();
 95 |       return true;
 96 |     } catch (error) {
 97 |       console.error(`Error uploading file to ${hostAlias}:`, error);
 98 |       return false;
 99 |     }
100 |   }
101 | 
102 |   /**
103 |    * Downloads a file from the remote host
104 |    */
105 |   async downloadFile(hostAlias: string, remotePath: string, localPath: string): Promise<boolean> {
106 |     try {
107 |       await this.connectToHost(hostAlias);
108 |       
109 |       await this.ssh.getFile(localPath, remotePath);
110 |       
111 |       this.ssh.dispose();
112 |       return true;
113 |     } catch (error) {
114 |       console.error(`Error downloading file from ${hostAlias}:`, error);
115 |       return false;
116 |     }
117 |   }
118 | 
119 |   /**
120 |    * Executes multiple commands in sequence
121 |    */
122 |   async runCommandBatch(hostAlias: string, commands: string[]): Promise<BatchCommandResult> {
123 |     try {
124 |       await this.connectToHost(hostAlias);
125 |       
126 |       const results: CommandResult[] = [];
127 |       let success = true;
128 |       
129 |       for (const command of commands) {
130 |         const result = await this.ssh.execCommand(command);
131 |         const cmdResult: CommandResult = {
132 |           stdout: result.stdout,
133 |           stderr: result.stderr,
134 |           code: result.code || 0
135 |         };
136 |         
137 |         results.push(cmdResult);
138 |         
139 |         if (cmdResult.code !== 0) {
140 |           success = false;
141 |           // We don't abort, execute all commands
142 |         }
143 |       }
144 |       
145 |       this.ssh.dispose();
146 |       return {
147 |         results,
148 |         success
149 |       };
150 |     } catch (error) {
151 |       console.error(`Error during batch execution on ${hostAlias}:`, error);
152 |       return {
153 |         results: [{
154 |           stdout: '',
155 |           stderr: error instanceof Error ? error.message : String(error),
156 |           code: 1
157 |         }],
158 |         success: false
159 |       };
160 |     }
161 |   }
162 | 
163 |   /**
164 |    * Establishes a connection to a host
165 |    */
166 |   private async connectToHost(hostAlias: string): Promise<void> {
167 |     // Get host information
168 |     const hostInfo = await this.getHostInfo(hostAlias);
169 |     
170 |     if (!hostInfo) {
171 |       throw new Error(`Host ${hostAlias} not found`);
172 |     }
173 | 
174 |     // Create connection configuration
175 |     const connectionConfig = {
176 |       host: hostInfo.hostname,
177 |       username: hostInfo.user,
178 |       port: hostInfo.port || 22,
179 |       privateKeyPath: hostInfo.identityFile
180 |     };
181 | 
182 |     try {
183 |       await this.ssh.connect(connectionConfig);
184 |     } catch (error) {
185 |       throw new Error(`Connection to ${hostAlias} failed: ${error instanceof Error ? error.message : String(error)}`);
186 |     }
187 |   }
188 | }
189 | 
```

--------------------------------------------------------------------------------
/IMPLEMENTATION_NOTES.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP SSH Agent Implementation Notes
  2 | 
  3 | ## Final Implementation (v2.0 - Simplified SSH)
  4 | 
  5 | The MCP SSH Agent has been successfully implemented with a simplified, more reliable SSH approach that replaced the problematic `node-ssh` library.
  6 | 
  7 | ### Architecture
  8 | 
  9 | 1. **Main Server File**: `server-simple.mjs`
 10 |    - Pure JavaScript implementation using ES modules with createRequire
 11 |    - Uses `@modelcontextprotocol/sdk` for MCP protocol compliance
 12 |    - Uses native `ssh` and `scp` commands via `child_process.exec()`
 13 | 
 14 | 2. **Core Components**:
 15 |    - `SSHConfigParser`: Parses `~/.ssh/config` and `~/.ssh/known_hosts`
 16 |    - `SSHClient`: Handles all SSH operations using local SSH commands
 17 |    - MCP Server: Provides standardized tool interface
 18 | 
 19 | ### Key Changes in v2.0
 20 | 
 21 | **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.
 22 | 
 23 | **Solution**: Replaced `node-ssh` with direct execution of local `ssh` and `scp` commands using Node.js `child_process.exec()`. This approach:
 24 | - Leverages existing SSH infrastructure (agent, keys, config)
 25 | - Avoids JavaScript library authentication complexities  
 26 | - Is more reliable and simpler to maintain
 27 | - Provides better error messages and debugging
 28 | 
 29 | ### MCP Protocol Implementation
 30 | 
 31 | - **Server Creation**: Uses `Server` class from MCP SDK
 32 | - **Transport**: `StdioServerTransport` for STDIO communication
 33 | - **Request Handlers**: 
 34 |   - `ListToolsRequestSchema`: Returns available SSH tools
 35 |   - `CallToolRequestSchema`: Executes SSH operations
 36 | 
 37 | ### SSH Tools Provided
 38 | 
 39 | 1. **listKnownHosts**: Returns all configured SSH hosts
 40 | 2. **runRemoteCommand**: Executes single commands remotely using `ssh "host" "command"`
 41 | 3. **getHostInfo**: Returns host configuration details
 42 | 4. **checkConnectivity**: Tests SSH connectivity with simple echo test
 43 | 5. **uploadFile**: Transfers files to remote hosts using `scp`
 44 | 6. **downloadFile**: Downloads files from remote hosts using `scp`
 45 | 7. **runCommandBatch**: Executes multiple commands sequentially
 46 | 
 47 | ### Technical Implementation Details
 48 | 
 49 | 1. **SSH Command Execution**: 
 50 |    ```javascript
 51 |    const sshCommand = `ssh "${hostAlias}" "${command.replace(/"/g, '\\"')}"`;
 52 |    const { stdout, stderr } = await execAsync(sshCommand, { timeout: 30000 });
 53 |    ```
 54 | 
 55 | 2. **File Transfers**: 
 56 |    ```javascript
 57 |    // Upload: scp "localPath" "hostAlias:remotePath"
 58 |    // Download: scp "hostAlias:remotePath" "localPath"
 59 |    ```
 60 | 
 61 | 3. **Error Handling**: Proper timeout handling (30s for commands, 60s for transfers)
 62 | 
 63 | ### Key Technical Decisions
 64 | 
 65 | 1. **Module Resolution**: Used `createRequire()` to handle MCP SDK CommonJS exports
 66 | 2. **Schema Compliance**: Used proper MCP request schemas instead of string identifiers
 67 | 3. **SSH Approach**: Native SSH commands instead of JavaScript SSH libraries
 68 | 4. **Error Handling**: Comprehensive error handling with meaningful error messages
 69 | 5. **File Structure**: Simplified to single main file to avoid build complexity
 70 | 
 71 | ### Dependencies (Simplified)
 72 | 
 73 | - `@modelcontextprotocol/sdk`: MCP protocol implementation
 74 | - `ssh-config`: SSH configuration parsing
 75 | - Native Node.js modules: `child_process`, `util`, `os`, `fs`
 76 | 
 77 | **Removed**: `node-ssh`, `ssh2` (unreliable authentication)
 78 | 
 79 | ### Testing
 80 | 
 81 | The implementation has been thoroughly tested with:
 82 | 
 83 | 1. **Basic Functionality Tests**:
 84 |    ```bash
 85 |    # Test tools listing
 86 |    echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node server-simple.mjs
 87 |    
 88 |    # Test SSH command execution
 89 |    echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"runRemoteCommand","arguments":{"hostAlias":"prod","command":"echo \"test\""}}}' | node server-simple.mjs
 90 |    ```
 91 | 
 92 | 2. **SSH Connectivity**: Successfully tested against prod host (157.90.89.149:42077)
 93 | 3. **Authentication**: Works with SSH agent and default key resolution
 94 | 4. **Error Handling**: Proper error reporting for failed connections
 95 | 
 96 | ### Performance and Reliability
 97 | 
 98 | - **Startup Time**: Fast server initialization (~1-2 seconds)
 99 | - **Command Execution**: Efficient direct SSH command execution
100 | - **Memory Usage**: Minimal - no persistent SSH connections
101 | - **Reliability**: Leverages proven SSH infrastructure
102 | 
103 | ### Usage
104 | 
105 | ```bash
106 | npm start  # Starts the MCP server on STDIO
107 | ```
108 | 
109 | The server is ready for integration with MCP-compatible clients and language models.
110 | 
111 | ## Troubleshooting History
112 | 
113 | ### Issue: SSH Authentication Failures with node-ssh
114 | 
115 | **Problem**: The original implementation using `node-ssh` consistently failed with:
116 | ```
117 | Connection to prod failed: All configured authentication methods failed
118 | ```
119 | 
120 | **Investigation**:
121 | - SSH config parsing worked correctly (prod host found)
122 | - Manual SSH connection (`ssh prod echo test`) worked perfectly
123 | - Multiple attempts to configure SSH agent, keys, and connection options failed
124 | - Issue appeared to be with node-ssh library's authentication handling
125 | 
126 | **Solution**: Complete replacement with native SSH command execution
127 | - Removed dependencies: `node-ssh`, `ssh2`
128 | - Replaced with: `child_process.exec()` + local `ssh`/`scp` commands
129 | - Result: Immediate success, much simpler code, better reliability
130 | 
131 | This demonstrates the value of using proven system tools over complex JavaScript libraries for system operations.
132 | 
133 | ## Security Considerations
134 | 
135 | - Uses existing SSH key infrastructure
136 | - No password storage or handling
137 | - Relies on properly configured SSH authentication
138 | - All operations respect SSH configuration restrictions
139 | 
140 | ## Maintenance Notes
141 | 
142 | - Keep MCP SDK dependency updated for protocol compliance
143 | - Monitor SSH library updates for security patches
144 | - Test with various SSH configurations periodically
145 | 
```

--------------------------------------------------------------------------------
/server-simple.mjs:
--------------------------------------------------------------------------------

```
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * MCP SSH Agent - A Model Context Protocol server for managing SSH connections
  5 |  * 
  6 |  * This is a simplified implementation that directly imports from specific files
  7 |  * to avoid module resolution issues.
  8 |  */
  9 | 
 10 | // Import required Node.js modules
 11 | import { homedir } from 'os';
 12 | import { readFile } from 'fs/promises';
 13 | import { join } from 'path';
 14 | import { fileURLToPath } from 'url';
 15 | import { createRequire } from 'module';
 16 | 
 17 | // Use createRequire to work around ESM import issues
 18 | const require = createRequire(import.meta.url);
 19 | 
 20 | // Required libraries
 21 | const { spawn, exec, execFile } = require('child_process');
 22 | const { promisify } = require('util');
 23 | const sshConfig = require('ssh-config');
 24 | 
 25 | const execAsync = promisify(exec);
 26 | const execFileAsync = promisify(execFile);
 27 | 
 28 | // Silent mode for MCP clients - disable debug output when used as MCP server
 29 | const SILENT_MODE = process.env.MCP_SILENT === 'true' || process.argv.includes('--silent');
 30 | 
 31 | // Debug logging function - only outputs in non-silent mode
 32 | function debugLog(message) {
 33 |   if (!SILENT_MODE) {
 34 |     process.stderr.write(message);
 35 |   }
 36 | }
 37 | 
 38 | // Import MCP components using proper export paths
 39 | const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
 40 | const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
 41 | const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
 42 | 
 43 | // SSH Configuration Parser
 44 | class SSHConfigParser {
 45 |   constructor() {
 46 |     const homeDir = homedir();
 47 |     this.configPath = join(homeDir, '.ssh', 'config');
 48 |     this.knownHostsPath = join(homeDir, '.ssh', 'known_hosts');
 49 |   }
 50 | 
 51 |   async parseConfig() {
 52 |     try {
 53 |       const content = await readFile(this.configPath, 'utf-8');
 54 |       const config = sshConfig.parse(content);
 55 |       return this.extractHostsFromConfig(config, this.configPath);
 56 |     } catch (error) {
 57 |       debugLog(`Error reading SSH config: ${error.message}\n`);
 58 |       return [];
 59 |     }
 60 |   }
 61 | 
 62 |   async processIncludeDirectives(configPath) {
 63 |     try {
 64 |       const content = await readFile(configPath, 'utf-8');
 65 |       const config = sshConfig.parse(content);
 66 |       const hosts = [];
 67 |       
 68 |       for (const section of config) {
 69 |         if (section.param === 'Include' && section.value) {
 70 |           const includePaths = this.expandIncludePath(section.value, configPath);
 71 |           
 72 |           for (const includePath of includePaths) {
 73 |             try {
 74 |               const includeHosts = await this.processIncludeDirectives(includePath);
 75 |               hosts.push(...includeHosts);
 76 |             } catch (error) {
 77 |               debugLog(`Error processing include file ${includePath}: ${error.message}\n`);
 78 |             }
 79 |           }
 80 |         }
 81 |       }
 82 |       
 83 |       // Add hosts from the current config file
 84 |       const currentHosts = this.extractHostsFromConfig(config, configPath);
 85 |       hosts.push(...currentHosts);
 86 |       
 87 |       return hosts;
 88 |     } catch (error) {
 89 |       debugLog(`Error processing config file ${configPath}: ${error.message}\n`);
 90 |       return [];
 91 |     }
 92 |   }
 93 | 
 94 |   expandIncludePath(includePath, baseConfigPath) {
 95 |     const { dirname, resolve } = require('path');
 96 |     const { glob } = require('glob');
 97 |     const { existsSync } = require('fs');
 98 |     
 99 |     // Handle tilde expansion
100 |     if (includePath.startsWith('~/')) {
101 |       includePath = includePath.replace('~', homedir());
102 |     }
103 |     
104 |     // Handle relative paths
105 |     if (!includePath.startsWith('/')) {
106 |       const baseDir = dirname(baseConfigPath);
107 |       includePath = resolve(baseDir, includePath);
108 |     }
109 |     
110 |     try {
111 |       // Handle glob patterns
112 |       if (includePath.includes('*') || includePath.includes('?')) {
113 |         return glob.sync(includePath).filter(path => existsSync(path));
114 |       } else {
115 |         return existsSync(includePath) ? [includePath] : [];
116 |       }
117 |     } catch (error) {
118 |       debugLog(`Error expanding include path ${includePath}: ${error.message}\n`);
119 |       return [];
120 |     }
121 |   }
122 | 
123 |   extractHostsFromConfig(config, configPath) {
124 |     const hosts = [];
125 | 
126 |     for (const section of config) {
127 |       // Skip Include directives as they are processed separately
128 |       if (section.param === 'Include') {
129 |         continue;
130 |       }
131 |       
132 |       if (section.param === 'Host' && section.value !== '*') {
133 |         const hostInfo = {
134 |           hostname: '',
135 |           alias: section.value,
136 |           configFile: configPath
137 |         };
138 | 
139 |         // Search all entries for this host
140 |         for (const param of section.config) {
141 |           // Safety check for undefined param
142 |           if (!param || !param.param) {
143 |             continue;
144 |           }
145 |           
146 |           switch (param.param.toLowerCase()) {
147 |             case 'hostname':
148 |               hostInfo.hostname = param.value;
149 |               break;
150 |             case 'user':
151 |               hostInfo.user = param.value;
152 |               break;
153 |             case 'port':
154 |               hostInfo.port = parseInt(param.value, 10);
155 |               break;
156 |             case 'identityfile':
157 |               hostInfo.identityFile = param.value;
158 |               break;
159 |             default:
160 |               // Store other parameters
161 |               hostInfo[param.param.toLowerCase()] = param.value;
162 |           }
163 |         }
164 | 
165 |         // Only add hosts with complete information
166 |         if (hostInfo.hostname) {
167 |           hosts.push(hostInfo);
168 |         }
169 |       }
170 |     }
171 | 
172 |     return hosts;
173 |   }
174 | 
175 |   async parseKnownHosts() {
176 |     try {
177 |       const content = await readFile(this.knownHostsPath, 'utf-8');
178 |       const knownHosts = content
179 |         .split('\n')
180 |         .filter(line => line.trim() !== '')
181 |         .map(line => {
182 |           // Format: hostname[,hostname2...] key-type public-key
183 |           const parts = line.split(' ')[0];
184 |           return parts.split(',')[0];
185 |         });
186 | 
187 |       return knownHosts;
188 |     } catch (error) {
189 |       debugLog(`Error reading known_hosts file: ${error.message}\n`);
190 |       return [];
191 |     }
192 |   }
193 | 
194 |   async getAllKnownHosts() {
195 |     // First: Get all hosts from ~/.ssh/config including Include directives (these are prioritized)
196 |     const configHosts = await this.processIncludeDirectives(this.configPath);
197 |     
198 |     // Second: Get hostnames from ~/.ssh/known_hosts
199 |     const knownHostnames = await this.parseKnownHosts();
200 | 
201 |     // Create a comprehensive list starting with config hosts
202 |     const allHosts = [...configHosts];
203 | 
204 |     // Add hosts from known_hosts that aren't already in the config
205 |     // These will appear after the config hosts
206 |     for (const hostname of knownHostnames) {
207 |       if (!configHosts.some(host => 
208 |           host.hostname === hostname || 
209 |           host.alias === hostname)) {
210 |         allHosts.push({
211 |           hostname: hostname,
212 |           source: 'known_hosts'
213 |         });
214 |       }
215 |     }
216 | 
217 |     // Mark config hosts for clarity
218 |     configHosts.forEach(host => {
219 |       host.source = 'ssh_config';
220 |     });
221 | 
222 |     return allHosts;
223 |   }
224 | }
225 | 
226 | // SSH Client Implementation
227 | class SSHClient {
228 |   constructor() {
229 |     this.configParser = new SSHConfigParser();
230 |   }
231 | 
232 |   async listKnownHosts() {
233 |     return await this.configParser.getAllKnownHosts();
234 |   }
235 | 
236 |   async runRemoteCommand(hostAlias, command) {
237 |     try {
238 |       // Use execFile for security - prevents command injection
239 |       debugLog(`Executing: ssh ${hostAlias} ${command}\n`);
240 |       
241 |       const { stdout, stderr } = await execFileAsync('ssh', [hostAlias, command], {
242 |         timeout: 30000, // 30 second timeout
243 |         maxBuffer: 1024 * 1024 * 10 // 10MB buffer
244 |       });
245 |       
246 |       return {
247 |         stdout: stdout || '',
248 |         stderr: stderr || '',
249 |         code: 0
250 |       };
251 |     } catch (error) {
252 |       debugLog(`Error executing command on ${hostAlias}: ${error.message}\n`);
253 |       return {
254 |         stdout: error.stdout || '',
255 |         stderr: error.stderr || error.message,
256 |         code: error.code || 1
257 |       };
258 |     }
259 |   }
260 | 
261 |   async getHostInfo(hostAlias) {
262 |     const hosts = await this.configParser.processIncludeDirectives(this.configParser.configPath);
263 |     return hosts.find(host => host.alias === hostAlias || host.hostname === hostAlias) || null;
264 |   }
265 | 
266 |   async checkConnectivity(hostAlias) {
267 |     try {
268 |       // Simple connectivity test using ssh
269 |       const result = await this.runRemoteCommand(hostAlias, 'echo connected');
270 |       const connected = result.code === 0 && result.stdout.trim() === 'connected';
271 |       
272 |       return {
273 |         connected,
274 |         message: connected ? 'Connection successful' : 'Connection failed'
275 |       };
276 |     } catch (error) {
277 |       debugLog(`Connectivity error with ${hostAlias}: ${error.message}\n`);
278 |       return {
279 |         connected: false,
280 |         message: error instanceof Error ? error.message : String(error)
281 |       };
282 |     }
283 |   }
284 | 
285 |   async uploadFile(hostAlias, localPath, remotePath) {
286 |     try {
287 |       debugLog(`Executing: scp ${localPath} ${hostAlias}:${remotePath}\n`);
288 |       
289 |       await execFileAsync('scp', [localPath, `${hostAlias}:${remotePath}`], { 
290 |         timeout: 60000 // 60 second timeout for file transfer
291 |       });
292 |       return true;
293 |     } catch (error) {
294 |       debugLog(`Error uploading file to ${hostAlias}: ${error.message}\n`);
295 |       return false;
296 |     }
297 |   }
298 | 
299 |   async downloadFile(hostAlias, remotePath, localPath) {
300 |     try {
301 |       debugLog(`Executing: scp ${hostAlias}:${remotePath} ${localPath}\n`);
302 |       
303 |       await execFileAsync('scp', [`${hostAlias}:${remotePath}`, localPath], { 
304 |         timeout: 60000 // 60 second timeout for file transfer
305 |       });
306 |       return true;
307 |     } catch (error) {
308 |       debugLog(`Error downloading file from ${hostAlias}: ${error.message}\n`);
309 |       return false;
310 |     }
311 |   }
312 | 
313 |   async runCommandBatch(hostAlias, commands) {
314 |     try {
315 |       const results = [];
316 |       let success = true;
317 |       
318 |       for (const command of commands) {
319 |         const result = await this.runRemoteCommand(hostAlias, command);
320 |         results.push(result);
321 |         
322 |         if (result.code !== 0) {
323 |           success = false;
324 |           // Continue executing remaining commands
325 |         }
326 |       }
327 |       
328 |       return {
329 |         results,
330 |         success
331 |       };
332 |     } catch (error) {
333 |       debugLog(`Error during batch execution on ${hostAlias}: ${error.message}\n`);
334 |       return {
335 |         results: [{
336 |           stdout: '',
337 |           stderr: error instanceof Error ? error.message : String(error),
338 |           code: 1
339 |         }],
340 |         success: false
341 |       };
342 |     }
343 |   }
344 | }
345 | 
346 | // Main function to start the MCP server
347 | async function main() {
348 |   try {
349 |     // Create an instance of the SSH client
350 |     debugLog("Initializing SSH client...\n");
351 |     const sshClient = new SSHClient();
352 | 
353 |     debugLog("Creating MCP server...\n");
354 |     // Create an MCP server
355 |     const server = new Server(
356 |       { name: "mcp-ssh", version: "1.0.0" },
357 |       { capabilities: { tools: {} } }
358 |     );
359 | 
360 |     debugLog("Setting up request handlers...\n");
361 |     // Handler for listing available tools
362 |     server.setRequestHandler(ListToolsRequestSchema, async () => {
363 |       debugLog("Received listTools request\n");
364 |       return {
365 |         tools: [
366 |           {
367 |             name: "listKnownHosts",
368 |             description: "Returns a consolidated list of all known SSH hosts, prioritizing ~/.ssh/config entries first, then additional hosts from ~/.ssh/known_hosts",
369 |             inputSchema: {
370 |               type: "object",
371 |               properties: {},
372 |               required: [],
373 |             },
374 |           },
375 |           {
376 |             name: "runRemoteCommand",
377 |             description: "Executes a shell command on an SSH host",
378 |             inputSchema: {
379 |               type: "object",
380 |               properties: {
381 |                 hostAlias: {
382 |                   type: "string",
383 |                   description: "Alias or hostname of the SSH host",
384 |                 },
385 |                 command: {
386 |                   type: "string",
387 |                   description: "The shell command to execute",
388 |                 },
389 |               },
390 |               required: ["hostAlias", "command"],
391 |             },
392 |           },
393 |           {
394 |             name: "getHostInfo",
395 |             description: "Returns all configuration details for an SSH host",
396 |             inputSchema: {
397 |               type: "object",
398 |               properties: {
399 |                 hostAlias: {
400 |                   type: "string",
401 |                   description: "Alias or hostname of the SSH host",
402 |                 },
403 |               },
404 |               required: ["hostAlias"],
405 |             },
406 |           },
407 |           {
408 |             name: "checkConnectivity",
409 |             description: "Checks if an SSH connection to the host is possible",
410 |             inputSchema: {
411 |               type: "object",
412 |               properties: {
413 |                 hostAlias: {
414 |                   type: "string",
415 |                   description: "Alias or hostname of the SSH host",
416 |                 },
417 |               },
418 |               required: ["hostAlias"],
419 |             },
420 |           },
421 |           {
422 |             name: "uploadFile",
423 |             description: "Uploads a local file to an SSH host",
424 |             inputSchema: {
425 |               type: "object",
426 |               properties: {
427 |                 hostAlias: {
428 |                   type: "string",
429 |                   description: "Alias or hostname of the SSH host",
430 |                 },
431 |                 localPath: {
432 |                   type: "string",
433 |                   description: "Path to the local file",
434 |                 },
435 |                 remotePath: {
436 |                   type: "string",
437 |                   description: "Path on the remote host",
438 |                 },
439 |               },
440 |               required: ["hostAlias", "localPath", "remotePath"],
441 |             },
442 |           },
443 |           {
444 |             name: "downloadFile",
445 |             description: "Downloads a file from an SSH host",
446 |             inputSchema: {
447 |               type: "object",
448 |               properties: {
449 |                 hostAlias: {
450 |                   type: "string",
451 |                   description: "Alias or hostname of the SSH host",
452 |                 },
453 |                 remotePath: {
454 |                   type: "string",
455 |                   description: "Path on the remote host",
456 |                 },
457 |                 localPath: {
458 |                   type: "string",
459 |                   description: "Path to the local destination",
460 |                 },
461 |               },
462 |               required: ["hostAlias", "remotePath", "localPath"],
463 |             },
464 |           },
465 |           {
466 |             name: "runCommandBatch",
467 |             description: "Executes multiple shell commands sequentially on an SSH host",
468 |             inputSchema: {
469 |               type: "object",
470 |               properties: {
471 |                 hostAlias: {
472 |                   type: "string",
473 |                   description: "Alias or hostname of the SSH host",
474 |                 },
475 |                 commands: {
476 |                   type: "array",
477 |                   items: { type: "string" },
478 |                   description: "List of shell commands to execute",
479 |                 },
480 |               },
481 |               required: ["hostAlias", "commands"],
482 |             },
483 |           },
484 |         ],
485 |       };
486 |     });
487 | 
488 |     // Handler for tool calls
489 |     server.setRequestHandler(CallToolRequestSchema, async (request) => {
490 |       const { name, arguments: args } = request.params;
491 |       debugLog(`Received callTool request for tool: ${name}\n`);
492 | 
493 |       if (!args && name !== "listKnownHosts") {
494 |         throw new Error(`No arguments provided for tool: ${name}`);
495 |       }
496 | 
497 |       try {
498 |         switch (name) {
499 |           case "listKnownHosts": {
500 |             const hosts = await sshClient.listKnownHosts();
501 |             return {
502 |               content: [{ type: "text", text: JSON.stringify(hosts, null, 2) }],
503 |             };
504 |           }
505 | 
506 |           case "runRemoteCommand": {
507 |             const result = await sshClient.runRemoteCommand(
508 |               args.hostAlias,
509 |               args.command
510 |             );
511 |             return {
512 |               content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
513 |             };
514 |           }
515 | 
516 |           case "getHostInfo": {
517 |             const hostInfo = await sshClient.getHostInfo(args.hostAlias);
518 |             return {
519 |               content: [{ type: "text", text: JSON.stringify(hostInfo, null, 2) }],
520 |             };
521 |           }
522 | 
523 |           case "checkConnectivity": {
524 |             const status = await sshClient.checkConnectivity(args.hostAlias);
525 |             return {
526 |               content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
527 |             };
528 |           }
529 | 
530 |           case "uploadFile": {
531 |             const success = await sshClient.uploadFile(
532 |               args.hostAlias,
533 |               args.localPath,
534 |               args.remotePath
535 |             );
536 |             return {
537 |               content: [{ type: "text", text: JSON.stringify({ success }, null, 2) }],
538 |             };
539 |           }
540 | 
541 |           case "downloadFile": {
542 |             const success = await sshClient.downloadFile(
543 |               args.hostAlias,
544 |               args.remotePath,
545 |               args.localPath
546 |             );
547 |             return {
548 |               content: [{ type: "text", text: JSON.stringify({ success }, null, 2) }],
549 |             };
550 |           }
551 | 
552 |           case "runCommandBatch": {
553 |             const result = await sshClient.runCommandBatch(
554 |               args.hostAlias,
555 |               args.commands
556 |             );
557 |             return {
558 |               content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
559 |             };
560 |           }
561 | 
562 |           default:
563 |             throw new Error(`Unknown tool: ${name}`);
564 |         }
565 |       } catch (error) {
566 |         debugLog(`Error executing tool ${name}: ${error.message}\n`);
567 |         return {
568 |           content: [
569 |             {
570 |               type: "text",
571 |               text: JSON.stringify({
572 |                 error: error instanceof Error ? error.message : String(error),
573 |               }),
574 |             },
575 |           ],
576 |         };
577 |       }
578 |     });
579 | 
580 |     debugLog("Starting MCP SSH Agent on STDIO...\n");
581 |     const transport = new StdioServerTransport();
582 |     await server.connect(transport);
583 |     debugLog("MCP SSH Agent connected and ready!\n");
584 |     
585 |   } catch (error) {
586 |     debugLog(`Error starting MCP SSH Agent: ${error.message}\n`);
587 |     process.exit(1);
588 |   }
589 | }
590 | 
591 | // Start the server
592 | main().catch(error => {
593 |   debugLog(`Unhandled error: ${error.message}\n`);
594 |   process.exit(1);
595 | });
596 | 
```