#!/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
};