ssh-mcp
Version:
MCP server exposing SSH control for Linux and Windows systems via Model Context Protocol.
164 lines (163 loc) • 6.08 kB
JavaScript
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);
});