132 lines
5.3 KiB
JavaScript
132 lines
5.3 KiB
JavaScript
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, 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}`);
|
|
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,
|
|
}));
|
|
});
|
|
app.listen(port, host, () => {
|
|
logger.info(`Listening on ${host}:${port}`);
|
|
logger.info(`StreamableHttp endpoint: http://${host}:${port}${streamableHttpPath}`);
|
|
});
|
|
}
|
|
//# sourceMappingURL=stdioToStatelessStreamableHttp.js.map
|