Skip to main content
Glama
timetetng

MCP SSH Server with Streamable HTTP

by timetetng
mcp-streamable-http-server.ts8.21 kB
#!/usr/bin/env node /** * Streamable HTTP MCP Server for SSH operations * 支持标准HTTP和Streamable HTTP双协议 */ import { Hono } from 'hono'; import { toFetchResponse, toReqRes } from 'fetch-to-node'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { SSHManager } from './ssh-manager.js'; // 创建SSH管理器实例 const sshManager = new SSHManager(); const PORT = process.env.STREAMABLE_PORT ? parseInt(process.env.STREAMABLE_PORT) : 3001; const AUTH_TOKEN = process.env.MCP_AUTH_TOKEN || ''; if (!AUTH_TOKEN) { console.error('ERROR: MCP_AUTH_TOKEN environment variable must be set'); process.exit(1); } // 创建MCP服务器实例 function createServer(): Server { return new Server({ name: 'mcp-ssh-server-streamable', version: '2.0.0', }, { capabilities: { tools: {} } }); } // 定义SSH工具 const tools = [ { name: 'ssh_connect', description: 'Connect to a remote server via SSH', inputSchema: { type: 'object', properties: { host: { type: 'string', description: 'SSH server hostname or IP address' }, port: { type: 'number', description: 'SSH server port (default: 22)', default: 22 }, username: { type: 'string', description: 'SSH username' }, password: { type: 'string', description: 'SSH password' }, }, required: ['host', 'username', 'password'], }, handler: async (args: any) => { const { host, port = 22, username, password } = args; const connectionId = await sshManager.connect({ host, port, username, password }); return { content: [ { type: 'text', text: `Successfully connected to ${host}:${port}. Connection ID: ${connectionId}`, } ] }; } }, { name: 'ssh_execute', description: 'Execute a command on the connected SSH server', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'Connection ID from ssh_connect' }, command: { type: 'string', description: 'Command to execute' }, }, required: ['connectionId', 'command'], }, handler: async (args: any) => { const { connectionId, command } = args; const output = await sshManager.execute(connectionId, command); return { content: [ { type: 'text', text: output, } ] }; } }, { name: 'ssh_upload', description: 'Upload a file to the remote server', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'Connection ID from ssh_connect' }, localPath: { type: 'string', description: 'Local file path' }, remotePath: { type: 'string', description: 'Remote file path' }, }, required: ['connectionId', 'localPath', 'remotePath'], }, handler: async (args: any) => { const { connectionId, localPath, remotePath } = args; await sshManager.upload(connectionId, localPath, remotePath); return { content: [ { type: 'text', text: `Successfully uploaded ${localPath} to ${remotePath}`, } ] }; } }, { name: 'ssh_download', description: 'Download a file from the remote server', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'Connection ID from ssh_connect' }, remotePath: { type: 'string', description: 'Remote file path' }, localPath: { type: 'string', description: 'Local file path' }, }, required: ['connectionId', 'remotePath', 'localPath'], }, handler: async (args: any) => { const { connectionId, remotePath, localPath } = args; await sshManager.download(connectionId, remotePath, localPath); return { content: [ { type: 'text', text: `Successfully downloaded ${remotePath} to ${localPath}`, } ] }; } }, { name: 'ssh_disconnect', description: 'Disconnect from the SSH server', inputSchema: { type: 'object', properties: { connectionId: { type: 'string', description: 'Connection ID to disconnect' }, }, required: ['connectionId'], }, handler: async (args: any) => { const { connectionId } = args; await sshManager.disconnect(connectionId); return { content: [ { type: 'text', text: `Disconnected from connection ${connectionId}`, } ] }; } }, { name: 'ssh_list_connections', description: 'List all active SSH connections', inputSchema: { type: 'object', properties: {}, }, handler: async (args: any) => { const connections = sshManager.listConnections(); const text = connections.length > 0 ? `Active connections:\n${connections.map(c => `- ${c.id}: ${c.host}:${c.port} (${c.username})`).join('\n')}` : 'No active connections'; return { content: [ { type: 'text', text, } ] }; } } ]; // 设置服务器工具 function setupServerTools(server: Server) { for (const tool of tools) { server.setTool(tool.name, tool); } } const app = new Hono(); // 健康检查端点(Streamable HTTP模式) app.get('/health', (c) => { return c.json({ status: 'healthy', timestamp: new Date().toISOString(), version: '2.0.0', transport: 'streamable-http' }); }); // Streamable HTTP MCP端点 app.post('/mcp', async (c) => { const { req, res } = toReqRes(c.req.raw); const server = createServer(); setupServerTools(server); try { const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(transport); await transport.handleRequest(req, res, await c.req.json()); res.on('close', () => { console.log('Streamable HTTP request closed'); transport.close(); server.close(); }); return toFetchResponse(res); } catch (error) { console.error('Streamable HTTP error:', error); return c.json({ error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : String(error) } }, 500); } }); app.get('/mcp', async (c) => { const { req, res } = toReqRes(c.req.raw); const server = createServer(); setupServerTools(server); try { const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(transport); await transport.handleRequest(req, res, { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }); res.on('close', () => { console.log('Streamable HTTP request closed'); transport.close(); server.close(); }); return toFetchResponse(res); } catch (error) { console.error('Streamable HTTP error:', error); return c.json({ error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : String(error) } }, 500); } }); // 启动服务器 console.log(`MCP SSH Streamable HTTP Server listening on port ${PORT}`); console.log(`Health check: GET /health`); console.log(`Streamable HTTP MCP endpoint: POST /mcp`); export default { port: PORT, fetch: app.fetch };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/timetetng/mcpssh-streamable'

If you have feedback or need assistance with the MCP directory API, please join our Discord server