import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { SessionManager } from './core/session-manager.js';
import { startCredentialHelper, stopCredentialHelper } from './core/credential-helper.js';
import { sshConnect, schema as connectSchema } from './tools/ssh-connect.js';
import { sshExec, schema as execSchema } from './tools/ssh-exec.js';
import { sshReadBuffer, schema as readBufferSchema } from './tools/ssh-read-buffer.js';
import { sshDisconnect, schema as disconnectSchema } from './tools/ssh-disconnect.js';
import { listSessions } from './tools/list-sessions.js';
import { scpUpload, schema as uploadSchema } from './tools/scp-upload.js';
import { scpDownload, schema as downloadSchema } from './tools/scp-download.js';
const server = new McpServer({
name: 'remote-ssh-mcp',
version: '1.0.0',
});
// ssh_connect
server.tool(
'ssh_connect',
'Connect to a remote SSH server. Returns session_id for subsequent commands.',
{
host: z.string().describe('Hostname or IP address'),
port: z.number().default(22).describe('SSH port'),
username: z.string().optional().describe('SSH username (or set SSH_USER env var)'),
auth_mode: z.enum(['password', 'key']).describe('Authentication mode'),
password: z.string().optional().describe('SSH password (or set SSH_PASSWORD env var)'),
key_path: z.string().optional().describe('Path to SSH private key'),
force_new: z.boolean().default(false).describe('Force new session even if one exists'),
timeout_minutes: z.number().default(15).describe('Session inactivity timeout'),
},
async (params) => ({
content: [{ type: 'text', text: JSON.stringify(await sshConnect(connectSchema.parse(params))) }],
})
);
// ssh_exec
server.tool(
'ssh_exec',
'Execute a command on a connected SSH session.',
{
session_id: z.string().describe('Session ID from ssh_connect'),
command: z.string().describe('Shell command to execute'),
timeout_ms: z.number().default(30000).describe('Command timeout in milliseconds'),
background: z.boolean().default(false).describe('Run command in background'),
},
async (params) => ({
content: [{ type: 'text', text: JSON.stringify(await sshExec(execSchema.parse(params))) }],
})
);
// ssh_read_buffer
server.tool(
'ssh_read_buffer',
'Read recent terminal output from session buffer.',
{
session_id: z.string().describe('Session ID'),
lines: z.number().default(100).describe('Lines to retrieve (max 1000)'),
},
async (params) => ({
content: [{ type: 'text', text: JSON.stringify(await sshReadBuffer(readBufferSchema.parse(params))) }],
})
);
// ssh_disconnect
server.tool(
'ssh_disconnect',
'Disconnect an SSH session.',
{ session_id: z.string().describe('Session ID to disconnect') },
async (params) => ({
content: [{ type: 'text', text: JSON.stringify(await sshDisconnect(disconnectSchema.parse(params))) }],
})
);
// list_sessions
server.tool(
'list_sessions',
'List all active SSH sessions.',
{},
async () => ({
content: [{ type: 'text', text: JSON.stringify(await listSessions()) }],
})
);
// scp_upload
server.tool(
'scp_upload',
'Upload a local file to remote server via SFTP.',
{
session_id: z.string().describe('Session ID'),
local_path: z.string().describe('Local file path'),
remote_path: z.string().describe('Remote destination path'),
},
async (params) => ({
content: [{ type: 'text', text: JSON.stringify(await scpUpload(uploadSchema.parse(params))) }],
})
);
// scp_download
server.tool(
'scp_download',
'Download a file from remote server via SFTP.',
{
session_id: z.string().describe('Session ID'),
remote_path: z.string().describe('Remote file path'),
local_path: z.string().describe('Local destination path'),
},
async (params) => ({
content: [{ type: 'text', text: JSON.stringify(await scpDownload(downloadSchema.parse(params))) }],
})
);
async function main() {
await startCredentialHelper().catch(() => {});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[remote-ssh-mcp] Server running');
const cleanup = () => {
console.error('[remote-ssh-mcp] Shutting down...');
stopCredentialHelper();
SessionManager.getInstance().closeAll();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
}
main().catch((err) => {
console.error('[remote-ssh-mcp] Fatal:', err);
SessionManager.getInstance().closeAll();
process.exit(1);
});