@edicarlos.lds/businessmap-mcp
Version:
Model Context Protocol server for BusinessMap (Kanbanize) integration
154 lines • 6.42 kB
JavaScript
import { randomUUID } from 'node:crypto';
import express from 'express';
import cors from 'cors';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { BusinessMapMcpServer } from './mcp-server.js';
import { config } from '../config/environment.js';
import { logger } from '../utils/logger.js';
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
async function closeSession(id, session) {
try {
await session.server.server.close();
}
catch (error) {
logger.warn(`Error while closing MCP session ${id}:`, error);
}
logger.info(`MCP session closed: ${id}`);
}
export async function startHttpServer(options = {}) {
const app = express();
// Disable X-Powered-By header to prevent disclosing version/framework info
app.disable('x-powered-by');
// Parse JSON bodies (required for StreamableHTTP transport)
app.use(express.json());
// Enable CORS with configured allowed origins
app.use(cors({
origin: config.server.allowedOrigins,
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'mcp-session-id', 'last-event-id'],
exposedHeaders: ['mcp-session-id'],
}));
// Apply custom middlewares (e.g. for authentication/authorization)
if (options.middlewares && options.middlewares.length > 0) {
options.middlewares.forEach((middleware) => {
app.use(middleware);
});
}
// Request logging middleware
app.use((req, _res, next) => {
const sessionId = req.headers['mcp-session-id'];
const sessionSuffix = sessionId ? ` [session: ${sessionId}]` : '';
logger.debug(`${req.method} ${req.path}${sessionSuffix}`);
next();
});
const sessions = new Map();
// Periodic cleanup of inactive sessions
const cleanupInterval = setInterval(async () => {
const now = Date.now();
for (const [id, session] of sessions) {
if (now - session.lastActivityAt > SESSION_TIMEOUT_MS) {
logger.info(`Closing idle MCP session (timeout): ${id}`);
sessions.delete(id);
await closeSession(id, session);
}
}
}, 60_000); // check every minute
// Prevent the interval from blocking process exit
cleanupInterval.unref();
const handleNewSession = async (req, res, logResponse, sendError) => {
const sessionServer = new BusinessMapMcpServer();
// New session: create a fresh transport
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
sessions.set(id, { transport, server: sessionServer, lastActivityAt: Date.now() });
logger.info(`New MCP session initialized: ${id}`);
},
onsessionclosed: async (id) => {
const session = sessions.get(id);
sessions.delete(id);
if (session) {
await closeSession(id, session);
}
},
allowedHosts: config.server.allowedHosts,
allowedOrigins: config.server.allowedOrigins,
enableDnsRebindingProtection: true,
});
try {
await sessionServer.server.connect(transport);
await transport.handleRequest(req, res, req.body);
logResponse(res.statusCode);
}
catch (error) {
logger.error('Failed to handle new MCP session:', error);
try {
await sessionServer.server.close();
}
catch (closeError) {
logger.warn('Error while cleaning up failed session:', closeError);
}
if (!res.headersSent) {
sendError(500, 'Failed to initialize MCP session');
}
}
};
const handleExistingSession = async (req, res, sessionId, logResponse, sendError) => {
const session = sessions.get(sessionId);
if (!session) {
sendError(404, `Session not found: ${sessionId}`);
return;
}
session.lastActivityAt = Date.now();
try {
await session.transport.handleRequest(req, res, req.body);
logResponse(res.statusCode);
}
catch (error) {
logger.error(`Error handling request for session ${sessionId}:`, error);
if (!res.headersSent) {
sendError(500, 'Internal server error');
}
}
};
// Single /mcp endpoint handles GET, POST and DELETE (Streamable HTTP spec 2025-03-26)
const handleMcpRequest = async (req, res) => {
const start = Date.now();
const sessionId = req.headers['mcp-session-id'];
const logResponse = (statusCode) => {
const duration = Date.now() - start;
const sessionSuffix = sessionId ? ` [session: ${sessionId}]` : '';
logger.debug(`${req.method} ${req.path} -> ${statusCode} (${duration}ms)${sessionSuffix}`);
};
const sendError = (statusCode, message) => {
logResponse(statusCode);
res.status(statusCode).json({ error: message });
};
if (req.method === 'POST' && !sessionId) {
await handleNewSession(req, res, logResponse, sendError);
return;
}
// Existing session
if (sessionId) {
await handleExistingSession(req, res, sessionId, logResponse, sendError);
return;
}
sendError(400, 'Missing mcp-session-id header');
};
app.get('/mcp', handleMcpRequest);
app.post('/mcp', handleMcpRequest);
app.delete('/mcp', handleMcpRequest);
// Health check endpoint
app.get('/health', (_req, res) => {
res.json({ status: 'ok', version: config.server.version });
});
const port = config.server.port;
return app.listen(port, () => {
logger.success(`HTTP Server running on port ${port}`);
logger.info(`MCP Endpoint: http://localhost:${port}/mcp`);
logger.info(`Health Check: http://localhost:${port}/health`);
logger.info(`Allowed origins: ${config.server.allowedOrigins.join(', ')}`);
logger.info(`Allowed hosts: ${config.server.allowedHosts.join(', ')}`);
});
}
//# sourceMappingURL=http.js.map