@l4t/mcp-ai
Version:
A set of tools for making integration and aggregation of MCP servers extremely easy.
137 lines • 5.44 kB
JavaScript
import { randomUUID } from 'node:crypto';
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { McpClientConfigs } from '../../common/types.js';
import { create as createFeatures } from '../features.js';
const DEFAULT_PORT = 3000;
const BAD_REQUEST_STATUS = 400;
const NOT_FOUND_STATUS = 404;
const create = (config, options) => {
const app = express();
app.use(bodyParser.json(options?.jsonBodyParser));
app.use(cors());
options?.preRouteMiddleware?.forEach(middleware => app.use(middleware));
// Map to store transports by session ID
const transports = {};
const setupServer = async (features) => {
features.validateConfig();
const server = new McpServer(Object.assign({}, McpClientConfigs.aggregator, {
capabilities: { tools: config.tools },
name: config.name,
version: config.version,
}));
const formatted = features.getFormattedTools();
formatted.forEach(tool => {
//@ts-ignore
server.tool(...tool);
});
return server;
};
const handleRequest = (features) => async (req, res) => {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'];
// eslint-disable-next-line functional/no-let
let transport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
}
else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
const server = await setupServer(features);
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true,
onsessioninitialized: sessionId => {
// Store the transport by session ID
// eslint-disable-next-line functional/immutable-data
transports[sessionId] = transport;
},
});
// Clean up transport when closed
// eslint-disable-next-line functional/immutable-data
transport.onclose = () => {
if (transport.sessionId) {
// eslint-disable-next-line functional/immutable-data
delete transports[transport.sessionId];
}
};
// Connect to the MCP server
await server.connect(transport);
}
else {
// Invalid request
res.status(BAD_REQUEST_STATUS).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// Handle the request
await transport.handleRequest(req, res, req.body);
};
// Reusable handler for GET and DELETE requests
const handleSessionRequest = async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
if (!sessionId || !transports[sessionId]) {
res.status(BAD_REQUEST_STATUS).send('Invalid or missing session ID');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
const _routeWrapper = async (func) => {
if (options?.afterRouteCallback) {
return async (req, res) => {
await func(req, res);
// @ts-ignore
await options.afterRouteCallback(req, res);
};
}
return func;
};
const getApp = async () => {
const features = createFeatures(config);
options?.additionalRoutes?.forEach(route => {
app[route.method.toLowerCase()](route.path, route.handler);
});
// Handle POST requests for client-to-server communication
app.post(config.server.path || '/', _routeWrapper(handleRequest(features)));
// Handle GET requests for server-to-client notifications
app.get(config.server.path || '/', _routeWrapper(handleSessionRequest));
// Handle DELETE requests for session termination
app.delete(config.server.path || '/', _routeWrapper(handleSessionRequest));
// Add catch-all route for non-existent URLs
app.use((req, res) => {
res.status(NOT_FOUND_STATUS).json({
error: 'Not Found',
message: `The requested URL ${req.url} was not found on this server`,
status: NOT_FOUND_STATUS,
});
});
return app;
};
return {
getApp,
set: (key, value) => {
app.set(key, value);
},
start: async () => {
const app = await getApp();
app.listen(config.server.connection.port || DEFAULT_PORT);
},
stop: async () => {
Object.values(transports).forEach(transport => transport.close());
},
};
};
export { create };
//# sourceMappingURL=http.js.map