144 lines
5.7 KiB
JavaScript
144 lines
5.7 KiB
JavaScript
import express from 'express';
|
|
import bodyParser from 'body-parser';
|
|
import cors from 'cors';
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
import { getVersion } from '../lib/getVersion.js';
|
|
import { onSignals } from '../lib/onSignals.js';
|
|
import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js';
|
|
import { loadConfig } from '../lib/config.js';
|
|
import { McpServerManager } from '../lib/mcpServerManager.js';
|
|
const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => {
|
|
res.setHeader(key, value);
|
|
});
|
|
export async function configToSse(args) {
|
|
const { configPath, port, host, baseUrl, ssePath, messagePath, logger, corsOrigin, healthEndpoints, headers, } = args;
|
|
logger.info(` - config: ${configPath}`);
|
|
logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`);
|
|
logger.info(` - host: ${host}`);
|
|
logger.info(` - port: ${port}`);
|
|
if (baseUrl) {
|
|
logger.info(` - baseUrl: ${baseUrl}`);
|
|
}
|
|
logger.info(` - ssePath: ${ssePath}`);
|
|
logger.info(` - messagePath: ${messagePath}`);
|
|
logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`);
|
|
logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`);
|
|
const serverManager = new McpServerManager(logger);
|
|
const cleanup = async () => {
|
|
await serverManager.cleanup();
|
|
};
|
|
onSignals({ logger, cleanup });
|
|
let config;
|
|
try {
|
|
config = loadConfig(configPath);
|
|
logger.info(`Loaded config with ${Object.keys(config.mcpServers).length} servers`);
|
|
}
|
|
catch (err) {
|
|
logger.error(`Failed to load config: ${err}`);
|
|
process.exit(1);
|
|
}
|
|
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
try {
|
|
await serverManager.addServer(serverName, serverConfig);
|
|
logger.info(`Successfully initialized server: ${serverName}`);
|
|
}
|
|
catch (err) {
|
|
logger.error(`Failed to initialize server ${serverName}: ${err}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} });
|
|
const sessions = {};
|
|
const app = express();
|
|
if (corsOrigin) {
|
|
app.use(cors({ origin: corsOrigin }));
|
|
}
|
|
app.use((req, res, next) => {
|
|
if (req.path === messagePath)
|
|
return next();
|
|
return bodyParser.json()(req, res, next);
|
|
});
|
|
for (const ep of healthEndpoints) {
|
|
app.get(ep, (_req, res) => {
|
|
setResponseHeaders({
|
|
res,
|
|
headers,
|
|
});
|
|
res.send('ok');
|
|
});
|
|
}
|
|
app.get(ssePath, async (req, res) => {
|
|
logger.info(`New SSE connection from ${req.ip}`);
|
|
setResponseHeaders({
|
|
res,
|
|
headers,
|
|
});
|
|
const sseTransport = new SSEServerTransport(`${baseUrl}${messagePath}`, res);
|
|
await server.connect(sseTransport);
|
|
const sessionId = sseTransport.sessionId;
|
|
if (sessionId) {
|
|
sessions[sessionId] = { transport: sseTransport, response: res };
|
|
}
|
|
sseTransport.onmessage = async (msg) => {
|
|
logger.info(`SSE → Servers (session ${sessionId}): ${JSON.stringify(msg)}`);
|
|
if ('method' in msg && 'id' in msg) {
|
|
try {
|
|
const response = await serverManager.handleRequest(msg);
|
|
logger.info(`Servers → SSE (session ${sessionId}):`);
|
|
logger.debug(`Servers → SSE (session ${sessionId}):`, response);
|
|
sseTransport.send(response);
|
|
}
|
|
catch (err) {
|
|
logger.error(`Error handling request in session ${sessionId}:`, err);
|
|
const errorResponse = {
|
|
jsonrpc: '2.0',
|
|
id: msg.id,
|
|
error: {
|
|
code: -32000,
|
|
message: 'Internal error',
|
|
},
|
|
};
|
|
sseTransport.send(errorResponse);
|
|
}
|
|
}
|
|
};
|
|
sseTransport.onclose = () => {
|
|
logger.info(`SSE connection closed (session ${sessionId})`);
|
|
delete sessions[sessionId];
|
|
};
|
|
sseTransport.onerror = (err) => {
|
|
logger.error(`SSE error (session ${sessionId}):`, err);
|
|
delete sessions[sessionId];
|
|
};
|
|
req.on('close', () => {
|
|
logger.info(`Client disconnected (session ${sessionId})`);
|
|
delete sessions[sessionId];
|
|
});
|
|
});
|
|
app.post(messagePath, async (req, res) => {
|
|
const sessionId = req.query.sessionId;
|
|
setResponseHeaders({
|
|
res,
|
|
headers,
|
|
});
|
|
if (!sessionId) {
|
|
return res.status(400).send('Missing sessionId parameter');
|
|
}
|
|
const session = sessions[sessionId];
|
|
if (session?.transport?.handlePostMessage) {
|
|
logger.info(`POST to SSE transport (session ${sessionId})`);
|
|
await session.transport.handlePostMessage(req, res);
|
|
}
|
|
else {
|
|
res.status(503).send(`No active SSE connection for session ${sessionId}`);
|
|
}
|
|
});
|
|
app.listen(port, host, () => {
|
|
logger.info(`Listening on ${host}:${port}`);
|
|
logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`);
|
|
logger.info(`POST messages: http://${host}:${port}${messagePath}`);
|
|
});
|
|
logger.info('Config-to-SSE gateway ready');
|
|
}
|
|
//# sourceMappingURL=configToSse.js.map
|