@l4t/mcp-ai
Version:
A set of tools for making integration and aggregation of MCP servers extremely easy.
129 lines • 5.41 kB
JavaScript
import { randomUUID } from 'node:crypto';
import express from 'express';
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';
import { openApiToZodSchema } from '../../common/libs.js';
import bodyParser from 'body-parser';
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) => {
const rawTools = await features.getTools();
const tools = rawTools.map(tool => ({
name: tool.name,
description: tool.description || '',
parameters: tool.inputSchema,
}));
const server = new McpServer(Object.assign({}, McpClientConfigs.aggregator, {
capabilities: { tools },
}));
tools.forEach(tool => {
// @ts-ignore
server.tool(tool.name, tool.description || '', openApiToZodSchema(tool.parameters), async (extra) => {
const results = await features.executeTool(tool.name, extra);
return results;
});
});
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);
};
return {
start: async () => {
const features = await createFeatures(config);
await features.connect();
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 || '/', handleRequest(features));
// Handle GET requests for server-to-client notifications
app.get(config.server.path || '/', handleSessionRequest);
// Handle DELETE requests for session termination
app.delete(config.server.path || '/', 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,
});
});
app.listen(config.server.connection.port || DEFAULT_PORT);
},
stop: async () => {
Object.values(transports).forEach(transport => transport.close());
},
set: (key, value) => {
app.set(key, value);
},
};
};
export { create };
//# sourceMappingURL=http.js.map