import { spawn } from 'child_process';
import { platform } from 'os';
import { randomBytes } from 'crypto';
import { createServer, IncomingMessage, ServerResponse } from 'http';
let server: ReturnType<typeof createServer> | null = null;
const SECRET = randomBytes(32).toString('hex');
const BASE_PORT = 7275;
let assignedPort: number | null = null;
export const getHelperSecret = () => SECRET;
export const getHelperPort = () => assignedPort;
// Platform-specific GUI prompts
async function guiPrompt(prompt: string, hidden = false): Promise<string | null> {
const os = platform();
if (os === 'darwin') {
return osascriptPrompt(prompt, hidden);
} else if (os === 'win32') {
return powershellPrompt(prompt, hidden);
} else if (os === 'linux' && process.env.DISPLAY) {
return zenityPrompt(prompt, hidden);
}
return null;
}
function zenityPrompt(prompt: string, hidden: boolean): Promise<string | null> {
return new Promise((resolve) => {
const args = hidden
? ['--password', '--title', prompt]
: ['--entry', '--title', 'SSH Credentials', '--text', prompt];
const proc = spawn('zenity', args);
let out = '';
proc.stdout.on('data', (d) => { out += d; });
proc.on('close', (code) => resolve(code === 0 ? out.trim() : null));
proc.on('error', () => resolve(null));
});
}
function osascriptPrompt(prompt: string, hidden: boolean): Promise<string | null> {
return new Promise((resolve) => {
const hiddenPart = hidden ? ' with hidden answer' : '';
const script = `display dialog "${prompt}" default answer ""${hiddenPart} buttons {"Cancel", "OK"} default button "OK"\ntext returned of result`;
const proc = spawn('osascript', ['-e', script]);
let out = '';
proc.stdout.on('data', (d) => { out += d; });
proc.on('close', (code) => resolve(code === 0 ? out.trim() : null));
});
}
function powershellPrompt(prompt: string, hidden: boolean): Promise<string | null> {
return new Promise((resolve) => {
const script = hidden
? `$c = Get-Credential -Message '${prompt}' -UserName ' '; if($c){$c.GetNetworkCredential().Password}`
: `Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.Interaction]::InputBox('${prompt}', 'SSH')`;
const proc = spawn('powershell', ['-Command', script]);
let out = '';
proc.stdout.on('data', (d) => { out += d; });
proc.on('close', (code) => {
const r = out.trim();
resolve(code === 0 && r ? r : null);
});
});
}
async function handleRequest(host: string, authMode: string, existingUser?: string) {
let username = existingUser;
if (!username) {
username = await guiPrompt(`Username for ${host}:`) ?? undefined;
if (!username) return { error: 'Cancelled' };
}
if (authMode === 'password') {
const password = await guiPrompt(`Password for ${username}@${host}`, true);
if (!password) return { error: 'Cancelled' };
return { username, password };
} else {
const keyPath = await guiPrompt(`SSH Key path for ${username}@${host}:`);
if (!keyPath) return { error: 'Cancelled' };
return { username, key_path: keyPath };
}
}
function parseBody(req: IncomingMessage): Promise<any> {
return new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try { resolve(JSON.parse(body)); }
catch { resolve({}); }
});
});
}
function sendJson(res: ServerResponse, data: any, status = 200) {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
export function startCredentialHelper(): Promise<void> {
return new Promise((resolve) => {
if (server) { resolve(); return; }
const tryPort = (port: number): void => {
if (port > BASE_PORT + 9) {
console.error('[ssh-mcp] No available ports for credential helper');
resolve();
return;
}
const srv = createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://127.0.0.1:${port}`);
if (url.pathname === '/health') {
res.writeHead(200);
res.end('ok');
return;
}
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ') || auth.slice(7) !== SECRET) {
res.writeHead(401);
res.end('Unauthorized');
return;
}
if (url.pathname === '/credentials' && req.method === 'POST') {
const body = await parseBody(req);
const result = await handleRequest(body.host, body.auth_mode, body.username);
sendJson(res, result);
return;
}
res.writeHead(404);
res.end('Not Found');
});
srv.on('error', (e: any) => {
if (e.code === 'EADDRINUSE') {
tryPort(port + 1);
} else {
resolve();
}
});
srv.listen(port, '127.0.0.1', () => {
server = srv;
assignedPort = port;
console.error(`[ssh-mcp] Credential helper on 127.0.0.1:${port}`);
resolve();
});
};
tryPort(BASE_PORT);
});
}
export function stopCredentialHelper(): void {
server?.close();
server = null;
assignedPort = null;
}