import express from 'express'; import cors from 'cors'; import { spawn } from 'child_process'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { getVersion } from '../lib/getVersion.js'; import { onSignals } from '../lib/onSignals.js'; import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { res.setHeader(key, value); }); export async function stdioToStatelessStreamableHttp(args) { const { stdioCmd, port, host, baseUrl, streamableHttpPath, logger, corsOrigin, healthEndpoints, headers, } = args; logger.info(` - Headers: ${Object(headers).length ? JSON.stringify(headers) : '(none)'}`); logger.info(` - host: ${host}`); logger.info(` - port: ${port}`); logger.info(` - stdio: ${stdioCmd}`); logger.info(` - streamableHttpPath: ${streamableHttpPath}`); if (baseUrl) { logger.info(` - baseUrl: ${baseUrl}`); } logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); onSignals({ logger }); const app = express(); app.use(express.json()); if (corsOrigin) { app.use(cors({ origin: corsOrigin })); } for (const ep of healthEndpoints) { app.get(ep, (_req, res) => { setResponseHeaders({ res, headers, }); res.send('ok'); }); } app.post(streamableHttpPath, async (req, res) => { // In stateless mode, create a new instance of transport and server for each request // to ensure complete isolation. A single instance would cause request ID collisions // when multiple clients connect concurrently. try { const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(transport); const child = spawn(stdioCmd, { shell: true }); child.on('exit', (code, signal) => { logger.error(`Child exited: code=${code}, signal=${signal}`); transport.close(); }); let buffer = ''; child.stdout.on('data', (chunk) => { buffer += chunk.toString('utf8'); const lines = buffer.split(/\r?\n/); buffer = lines.pop() ?? ''; lines.forEach((line) => { if (!line.trim()) return; try { const jsonMsg = JSON.parse(line); logger.info('Child → StreamableHttp:', line); try { transport.send(jsonMsg); } catch (e) { logger.error(`Failed to send to StreamableHttp`, e); } } catch { logger.error(`Child non-JSON: ${line}`); } }); }); child.stderr.on('data', (chunk) => { logger.error(`Child stderr: ${chunk.toString('utf8')}`); }); transport.onmessage = (msg) => { logger.info(`StreamableHttp → Child: ${JSON.stringify(msg)}`); child.stdin.write(JSON.stringify(msg) + '\n'); }; transport.onclose = () => { logger.info('StreamableHttp connection closed'); child.kill(); }; transport.onerror = (err) => { logger.error(`StreamableHttp error:`, err); child.kill(); }; await transport.handleRequest(req, res, req.body); } catch (error) { logger.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); app.get(streamableHttpPath, async (req, res) => { logger.info('Received GET MCP request'); res.writeHead(405).end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, })); }); app.delete(streamableHttpPath, async (req, res) => { logger.info('Received DELETE MCP request'); res.writeHead(405).end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, })); }); const publicHost = baseUrl ?? (host === '0.0.0.0' ? 'localhost' : host); app.listen(port, host, () => { logger.info(`Listening on ${host}:${port}`); logger.info(`StreamableHttp endpoint: http://${publicHost}:${port}${streamableHttpPath}`); }); } //# sourceMappingURL=stdioToStatelessStreamableHttp.js.map