import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { authMiddleware } from '../middleware/auth.js';
import { ipAllowlistMiddleware } from '../middleware/ip-allowlist.js';
import { securityHeaders } from '../middleware/security-headers.js';
import { logger } from '../utils/logger.js';
import { getMetrics } from '../utils/metrics.js';
import { setSession, getSession, deleteSession } from './session.js';
import { config } from '../config.js';
import { apiRouter, type ApiDeps } from '../routes/api-router.js';
export function createExpressApp(mcpServer: McpServer, apiDeps?: ApiDeps): express.Application {
const app = express();
app.use(cors());
app.use(express.json());
app.use(cookieParser());
app.use(securityHeaders);
app.use(ipAllowlistMiddleware);
app.use(authMiddleware);
// Health endpoint
app.get('/health', (_req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// Metrics endpoint
app.get('/metrics', (_req, res) => {
res.json(getMetrics());
});
// MCP Streamable HTTP endpoint — POST (new sessions + messages)
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId) {
// Existing session
const transport = getSession(sessionId);
if (!transport) {
res.status(404).json({ error: 'Session not found' });
return;
}
await transport.handleRequest(req, res, req.body);
return;
}
// New session
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => {
setSession(sid, transport);
logger.info({ sessionId: sid }, 'MCP session initialized');
},
});
transport.onclose = () => {
const sid = transport.sessionId;
if (sid) {
deleteSession(sid);
logger.info({ sessionId: sid }, 'MCP session closed');
}
};
await mcpServer.connect(transport);
await transport.handleRequest(req, res, req.body);
});
// MCP Streamable HTTP endpoint — GET (SSE stream for notifications)
app.get('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId) {
res.status(400).json({ error: 'mcp-session-id header required' });
return;
}
const transport = getSession(sessionId);
if (!transport) {
res.status(404).json({ error: 'Session not found' });
return;
}
await transport.handleRequest(req, res);
});
// MCP Streamable HTTP endpoint — DELETE (session termination)
app.delete('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId) {
res.status(400).json({ error: 'mcp-session-id header required' });
return;
}
const transport = getSession(sessionId);
if (!transport) {
res.status(404).json({ error: 'Session not found' });
return;
}
await transport.handleRequest(req, res);
});
// Dashboard API routes
if (apiDeps) {
app.use('/api', apiRouter(apiDeps));
}
return app;
}
export function startServer(app: express.Application): ReturnType<express.Application['listen']> {
return app.listen(config.server.port, config.server.host, () => {
logger.info(
{ host: config.server.host, port: config.server.port },
`MCP Context Hub listening on ${config.server.host}:${config.server.port}`
);
});
}