UNPKG

@tb.p/terminai

Version:

MCP (Model Context Protocol) server for secure SSH remote command execution. Enables AI assistants like Claude, Cursor, and VS Code to execute commands on remote servers via SSH with command validation, history tracking, and web-based configuration UI.

153 lines (130 loc) 4.22 kB
import { Client } from 'ssh2'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { homedir } from 'os'; import sshpk from 'sshpk'; export function expandPath(filePath) { if (filePath.startsWith('~/')) { return resolve(homedir(), filePath.slice(2)); } return resolve(filePath); } function parseAndConvertKey(keyData, passphrase) { try { // Check if it's already in OpenSSH format if (keyData.includes('BEGIN OPENSSH PRIVATE KEY')) { return Buffer.from(keyData, 'utf-8'); } // Try to parse the key with sshpk const key = sshpk.parsePrivateKey(keyData, 'auto', { passphrase }); // For ed25519 keys, convert PKCS#8 to OpenSSH format if (key.type === 'ed25519' || key.type === 'curve25519') { // Convert to OpenSSH format that ssh2 prefers return key.toBuffer('ssh'); } else { // For RSA/ECDSA, convert to pkcs1 return key.toBuffer('pkcs1'); } } catch (error) { // If sshpk fails, return original key data and let ssh2 try if (typeof keyData === 'string') { return Buffer.from(keyData, 'utf-8'); } return keyData; } } export async function executeCommand(connection, command, timeout = 30000) { return new Promise((resolve, reject) => { const conn = new Client(); const startTime = Date.now(); let stdout = ''; let stderr = ''; let exitCode = null; let timedOut = false; const timeoutHandle = setTimeout(() => { timedOut = true; conn.end(); reject(new Error('Command execution timed out')); }, timeout); conn.on('ready', () => { conn.exec(command, (err, stream) => { if (err) { clearTimeout(timeoutHandle); conn.end(); reject(err); return; } stream.on('data', (data) => { stdout += data.toString(); }); stream.stderr.on('data', (data) => { stderr += data.toString(); }); stream.on('close', (code) => { clearTimeout(timeoutHandle); exitCode = code; conn.end(); if (!timedOut) { const duration = Date.now() - startTime; resolve({ stdout, stderr, exitCode, duration }); } }); }); }); conn.on('error', (err) => { clearTimeout(timeoutHandle); reject(err); }); try { const privateKeyPath = expandPath(connection.identityFile); let privateKeyData; try { privateKeyData = readFileSync(privateKeyPath, 'utf-8'); } catch (readError) { throw new Error(`Failed to read private key file: ${readError.message}`); } // Convert OpenSSH format keys to PEM format const privateKey = parseAndConvertKey(privateKeyData, connection.passphrase); const connectOptions = { host: connection.host, port: connection.port || 22, username: connection.username, privateKey, readyTimeout: 10000, debug: (msg) => { // Uncomment for debugging: console.error('SSH Debug:', msg); } }; // Add passphrase if provided (for encrypted keys) if (connection.passphrase) { connectOptions.passphrase = connection.passphrase; } conn.connect(connectOptions); } catch (err) { clearTimeout(timeoutHandle); reject(err); } }); } export async function testConnection(connection) { try { // Validate connection details if (!connection.identityFile) { return { success: false, error: 'Identity file path is required' }; } const privateKeyPath = expandPath(connection.identityFile); // Check if file exists try { readFileSync(privateKeyPath, 'utf-8'); } catch (err) { return { success: false, error: `Cannot read private key at ${privateKeyPath}: ${err.message}` }; } const result = await executeCommand(connection, 'echo "SSH connection test"', 5000); if (result.exitCode === 0) { return { success: true }; } return { success: false, error: `Command failed with exit code ${result.exitCode}` }; } catch (error) { return { success: false, error: error.message }; } }