#!/usr/bin/env node
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const { v4: uuidv4 } = require('uuid');
const { NodeSSH } = require('node-ssh');
const app = express();
const PORT = process.env.PORT || 8787;
app.use(cors());
app.use(bodyParser.json());
// --- Simple in-memory SSE clients list ---
const sseClients = new Set();
app.get('/sse', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no',
'Access-Control-Allow-Origin': '*'
});
const clientId = uuidv4();
const client = { id: clientId, res };
sseClients.add(client);
// Initial hello event
res.write(`event: message\n`);
res.write(`data: ${JSON.stringify({ type: 'ready', id: clientId })}\n\n`);
req.on('close', () => {
sseClients.delete(client);
});
});
function broadcastEvent(event) {
const payload = `event: message\ndata: ${JSON.stringify(event)}\n\n`;
for (const client of sseClients) {
try {
client.res.write(payload);
} catch {
// ignore broken pipe
}
}
}
// Helper to wrap content according to MCP-style response shape
function wrapContent(obj) {
return {
content: [
{
type: 'text',
text: JSON.stringify(obj)
}
]
};
}
// --- Tools implementation ---
async function handleSearch(body) {
// Dummy implementation: always return single ssh tool description
return wrapContent({
tools: [
{
name: 'ssh',
description: 'Execute a command over SSH',
type: 'ssh.exec'
}
]
});
}
async function handleFetch(body) {
// Dummy implementation mirroring search
return handleSearch(body);
}
async function handleSshExec(args) {
const { host, user, username, password, privateKey, command } = args || {};
if (!host || !(user || username) || !command) {
return wrapContent({
error: 'host, user/username, and command are required'
});
}
if (!password && !privateKey) {
return wrapContent({
error: 'either password or privateKey must be provided'
});
}
const ssh = new NodeSSH();
const resultPayload = {
host,
user: user || username,
command,
stdout: '',
stderr: '',
code: null,
error: null
};
try {
await ssh.connect({
host,
username: user || username,
password,
privateKey
});
const result = await ssh.execCommand(command, { cwd: undefined });
resultPayload.stdout = result.stdout || '';
resultPayload.stderr = result.stderr || '';
resultPayload.code = typeof result.code === 'number' ? result.code : 0;
await ssh.dispose();
} catch (err) {
resultPayload.error = err && err.message ? err.message : String(err);
}
// Also push to SSE subscribers as an event
broadcastEvent({
type: 'ssh.exec.result',
data: resultPayload
});
return wrapContent(resultPayload);
}
app.post('/query', async (req, res) => {
const { tool, action, args } = req.body || {};
try {
if (tool === 'search' || action === 'search') {
const out = await handleSearch(req.body);
return res.json(out);
}
if (tool === 'fetch' || action === 'fetch') {
const out = await handleFetch(req.body);
return res.json(out);
}
if (tool === 'ssh.exec' || action === 'ssh.exec') {
const out = await handleSshExec(args || req.body.args || req.body);
return res.json(out);
}
return res.status(400).json(
wrapContent({ error: 'Unknown tool/action. Use search, fetch, or ssh.exec.' })
);
} catch (err) {
return res.status(500).json(
wrapContent({ error: err && err.message ? err.message : String(err) })
);
}
});
app.get('/', (req, res) => {
res.json({
status: 'ok',
message: 'ssh-mcp-remote server',
sse: '/sse',
query: '/query'
});
});
app.listen(PORT, () => {
console.log('ssh-mcp-remote listening on port', PORT);
console.log('SSE endpoint: GET /sse');
console.log('Query endpoint: POST /query');
});