UNPKG

ssh-mcp

Version:

MCP server exposing SSH control for Linux and Windows systems via Model Context Protocol.

164 lines (163 loc) 6.08 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { Client as SSHClient } from 'ssh2'; import { z } from 'zod'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; // Example usage: node build/index.js --host=1.2.3.4 --port=22 --user=root --password=pass --key=path/to/key --timeout=5000 function parseArgv() { const args = process.argv.slice(2); const config = {}; for (const arg of args) { const match = arg.match(/^--([^=]+)=(.*)$/); if (match) { config[match[1]] = match[2]; } } return config; } const argvConfig = parseArgv(); const HOST = argvConfig.host; const PORT = argvConfig.port ? parseInt(argvConfig.port) : 22; const USER = argvConfig.user; const PASSWORD = argvConfig.password; const KEY = argvConfig.key; const DEFAULT_TIMEOUT = argvConfig.timeout ? parseInt(argvConfig.timeout) : 60000; // 60 seconds default timeout function validateConfig(config) { const errors = []; if (!config.host) errors.push('Missing required --host'); if (!config.user) errors.push('Missing required --user'); if (config.port && isNaN(Number(config.port))) errors.push('Invalid --port'); if (errors.length > 0) { throw new Error('Configuration error:\n' + errors.join('\n')); } } validateConfig(argvConfig); const server = new McpServer({ name: 'SSH MCP Server', version: '1.0.7', capabilities: { resources: {}, tools: {}, }, }); server.tool("exec", "Execute a shell command on the remote SSH server and return the output.", { command: z.string().describe("Shell command to execute on the remote SSH server"), }, async ({ command }) => { // Sanitize command input if (typeof command !== 'string' || !command.trim()) { throw new McpError(ErrorCode.InternalError, 'Command must be a non-empty string.'); } const sshConfig = { host: HOST, port: PORT, username: USER, }; try { if (PASSWORD) { sshConfig.password = PASSWORD; } else if (KEY) { const fs = await import('fs/promises'); sshConfig.privateKey = await fs.readFile(KEY, 'utf8'); } const result = await execSshCommand(sshConfig, command); return result; } catch (err) { // Wrap unexpected errors if (err instanceof McpError) throw err; throw new McpError(ErrorCode.InternalError, `Unexpected error: ${err?.message || err}`); } }); async function execSshCommand(sshConfig, command) { return new Promise((resolve, reject) => { const conn = new SSHClient(); let timeoutId; let isResolved = false; // Set up timeout timeoutId = setTimeout(() => { if (!isResolved) { isResolved = true; // Try to abort the running command before closing connection const abortTimeout = setTimeout(() => { // If abort command itself times out, force close connection conn.end(); }, 5000); // 5 second timeout for abort command conn.exec('timeout 3s pkill -f "' + command + '" 2>/dev/null || true', (err, abortStream) => { if (abortStream) { abortStream.on('close', () => { clearTimeout(abortTimeout); conn.end(); }); } else { clearTimeout(abortTimeout); conn.end(); } }); reject(new McpError(ErrorCode.InternalError, `Command execution timed out after ${DEFAULT_TIMEOUT}ms`)); } }, DEFAULT_TIMEOUT); conn.on('ready', () => { conn.exec(command, (err, stream) => { if (err) { if (!isResolved) { isResolved = true; clearTimeout(timeoutId); reject(new McpError(ErrorCode.InternalError, `SSH exec error: ${err.message}`)); } conn.end(); return; } let stdout = ''; let stderr = ''; stream.on('close', (code, signal) => { if (!isResolved) { isResolved = true; clearTimeout(timeoutId); conn.end(); if (stderr) { reject(new McpError(ErrorCode.InternalError, `Error (code ${code}):\n${stderr}`)); } else { resolve({ content: [{ type: 'text', text: stdout, }], }); } } }); stream.on('data', (data) => { stdout += data.toString(); }); stream.stderr.on('data', (data) => { stderr += data.toString(); }); }); }); conn.on('error', (err) => { if (!isResolved) { isResolved = true; clearTimeout(timeoutId); reject(new McpError(ErrorCode.InternalError, `SSH connection error: ${err.message}`)); } }); conn.connect(sshConfig); }); } async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("SSH MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });