UNPKG

@aashari/boilerplate-mcp-server

Version:

TypeScript MCP server boilerplate with STDIO and HTTP transport support, CLI tools, and extensible architecture

373 lines (372 loc) 15.6 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startServer = startServer; const node_crypto_1 = require("node:crypto"); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const logger_util_js_1 = require("./utils/logger.util.js"); const config_util_js_1 = require("./utils/config.util.js"); const constants_util_js_1 = require("./utils/constants.util.js"); const index_js_1 = require("./cli/index.js"); const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); // Import tools, resources, and prompts const ipaddress_tool_js_1 = __importDefault(require("./tools/ipaddress.tool.js")); const ipaddress_link_tool_js_1 = __importDefault(require("./tools/ipaddress-link.tool.js")); const ipaddress_resource_js_1 = __importDefault(require("./resources/ipaddress.resource.js")); const analysis_prompt_js_1 = __importDefault(require("./prompts/analysis.prompt.js")); const logger = logger_util_js_1.Logger.forContext('index.ts'); let serverInstance = null; let transportInstance = null; let httpServerInstance = null; const httpSessions = new Map(); function createMcpServer() { const server = new mcp_js_1.McpServer({ name: constants_util_js_1.PACKAGE_NAME, version: constants_util_js_1.VERSION, }); ipaddress_tool_js_1.default.registerTools(server); ipaddress_link_tool_js_1.default.registerTools(server); ipaddress_resource_js_1.default.registerResources(server); analysis_prompt_js_1.default.registerPrompts(server); return server; } function getSessionId(req) { const rawSessionId = req.headers['mcp-session-id']; if (Array.isArray(rawSessionId)) { return rawSessionId[0] ?? null; } if (typeof rawSessionId === 'string' && rawSessionId.length > 0) { return rawSessionId; } return null; } /** * Start the MCP server with the specified transport mode */ async function startServer(mode = 'stdio') { logger.info(`Starting MCP server in ${mode} mode with ${constants_util_js_1.PACKAGE_NAME} v${constants_util_js_1.VERSION}`); const serverLogger = logger_util_js_1.Logger.forContext('index.ts', 'startServer'); // Load configuration serverLogger.info('Starting MCP server initialization...'); config_util_js_1.config.load(); if (config_util_js_1.config.getBoolean('DEBUG')) { serverLogger.debug('Debug mode enabled'); } if (mode === 'stdio') { serverLogger.info(`Initializing Boilerplate MCP server v${constants_util_js_1.VERSION}`); serverLogger.info('Registering MCP tools, resources, and prompts...'); serverInstance = createMcpServer(); serverLogger.debug('All tools, resources, and prompts registered'); serverLogger.info('Using STDIO transport'); transportInstance = new stdio_js_1.StdioServerTransport(); try { await serverInstance.connect(transportInstance); serverLogger.info('MCP server started successfully on STDIO transport'); setupGracefulShutdown(); return serverInstance; } catch (err) { serverLogger.error('Failed to start server on STDIO transport', err); process.exit(1); } } else { // HTTP transport with Express serverLogger.info('Using Streamable HTTP transport'); const app = (0, express_1.default)(); // DNS rebinding protection - validate Origin header // See: https://modelcontextprotocol.io/docs/concepts/transports app.use((req, res, next) => { const origin = req.headers.origin; // Allow requests without Origin (direct API calls, curl, etc.) if (!origin) { return next(); } // Validate Origin matches expected localhost patterns const allowedOrigins = [ 'http://localhost', 'http://127.0.0.1', 'http://[::1]', 'https://localhost', 'https://127.0.0.1', 'https://[::1]', ]; const isAllowed = allowedOrigins.some((allowed) => origin === allowed || origin.startsWith(`${allowed}:`)); if (!isAllowed) { serverLogger.warn(`Rejected request with invalid origin: ${origin}`); res.status(403).json({ error: 'Forbidden', message: 'Invalid origin for MCP server', }); return; } next(); }); app.use((0, cors_1.default)({ origin: true })); app.use(express_1.default.json({ limit: '1mb' })); const mcpEndpoint = '/mcp'; serverLogger.debug(`MCP endpoint: ${mcpEndpoint}`); // Handle MCP POST requests (initialize + normal RPC calls) app.post(mcpEndpoint, async (req, res) => { const sessionId = getSessionId(req); try { if (sessionId) { const session = httpSessions.get(sessionId); if (!session) { res.status(404).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Session not found for provided Mcp-Session-Id', }, id: null, }); return; } session.lastActivity = Date.now(); await session.transport.handleRequest(req, res, req.body); return; } if (!(0, types_js_1.isInitializeRequest)(req.body)) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: Missing Mcp-Session-Id header', }, id: null, }); return; } serverLogger.info('Creating new HTTP MCP session for initialize request'); let initializedSessionId = null; const sessionServer = createMcpServer(); const sessionTransport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(), onsessioninitialized: (newSessionId) => { initializedSessionId = newSessionId; httpSessions.set(newSessionId, { server: sessionServer, transport: sessionTransport, lastActivity: Date.now(), }); serverLogger.info(`Initialized HTTP MCP session ${newSessionId}`); }, onsessionclosed: (closedSessionId) => { const session = httpSessions.get(closedSessionId); if (!session) { return; } httpSessions.delete(closedSessionId); void session.server.close().catch((err) => { serverLogger.error(`Error closing server for session ${closedSessionId}`, err); }); serverLogger.info(`Closed HTTP MCP session ${closedSessionId}`); }, }); await sessionServer.connect(sessionTransport); try { await sessionTransport.handleRequest(req, res, req.body); } catch (err) { if (initializedSessionId) { httpSessions.delete(initializedSessionId); } await sessionTransport.close(); await sessionServer.close(); throw err; } } catch (err) { serverLogger.error('Error in HTTP MCP handler', err); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Handle optional GET requests for streamable HTTP SSE app.get(mcpEndpoint, async (req, res) => { const sessionId = getSessionId(req); if (!sessionId) { res.status(400).send('Missing Mcp-Session-Id header'); return; } const session = httpSessions.get(sessionId); if (!session) { res.status(404).send('Session not found'); return; } try { await session.transport.handleRequest(req, res); } catch (err) { serverLogger.error('Error in HTTP MCP GET handler', err); if (!res.headersSent) { res.status(500).send('Internal Server Error'); } } }); // Handle session termination requests app.delete(mcpEndpoint, async (req, res) => { const sessionId = getSessionId(req); if (!sessionId) { res.status(400).send('Missing Mcp-Session-Id header'); return; } const session = httpSessions.get(sessionId); if (!session) { res.status(404).send('Session not found'); return; } try { await session.transport.handleRequest(req, res); } catch (err) { serverLogger.error('Error in HTTP MCP DELETE handler', err); if (!res.headersSent) { res.status(500).send('Internal Server Error'); } } }); // Health check endpoint app.get('/', (_req, res) => { res.send(`Boilerplate MCP Server v${constants_util_js_1.VERSION} is running`); }); // Start HTTP server const PORT = Number(process.env.PORT ?? 3000); const HOST = '127.0.0.1'; // Explicit localhost binding for security await new Promise((resolve) => { httpServerInstance = app.listen(PORT, HOST, () => { serverLogger.info(`HTTP transport listening on http://${HOST}:${PORT}${mcpEndpoint}`); serverLogger.info('Server bound to localhost only for security'); resolve(); }); }); // Reap idle sessions every 5 minutes (TTL: 30 minutes) const SESSION_TTL_MS = 30 * 60 * 1000; const REAP_INTERVAL_MS = 5 * 60 * 1000; const reapInterval = setInterval(() => { const now = Date.now(); for (const [sessionId, session] of httpSessions.entries()) { if (now - session.lastActivity > SESSION_TTL_MS) { serverLogger.info(`Reaping idle HTTP session ${sessionId}`); httpSessions.delete(sessionId); void session.transport .close() .catch((err) => serverLogger.debug(`Error closing transport for reaped session ${sessionId}`, err)) .then(() => session.server.close()) .catch((err) => serverLogger.debug(`Error closing server for reaped session ${sessionId}`, err)); } } }, REAP_INTERVAL_MS); reapInterval.unref(); setupGracefulShutdown(); // HTTP mode uses per-session servers (managed in httpSessions map). // Return a reference server for API compatibility; not connected to any transport. return createMcpServer(); } } /** * Main entry point */ async function main() { const mainLogger = logger_util_js_1.Logger.forContext('index.ts', 'main'); // Load configuration config_util_js_1.config.load(); // CLI mode - if any arguments are provided if (process.argv.length > 2) { mainLogger.info('CLI mode detected'); await (0, index_js_1.runCli)(process.argv.slice(2)); return; } // Server mode - determine transport const transportMode = (process.env.TRANSPORT_MODE || 'stdio').toLowerCase(); let mode; if (transportMode === 'stdio') { mode = 'stdio'; } else if (transportMode === 'http') { mode = 'http'; } else { mainLogger.warn(`Unknown TRANSPORT_MODE "${transportMode}", defaulting to stdio`); mode = 'stdio'; } mainLogger.info(`Starting server with ${mode.toUpperCase()} transport`); await startServer(mode); } // Run main if executed directly if (require.main === module) { main().catch((err) => { logger.error('Unhandled error in main process', err); process.exit(1); }); } /** * Graceful shutdown handler */ function setupGracefulShutdown() { const shutdownLogger = logger_util_js_1.Logger.forContext('index.ts', 'shutdown'); let shuttingDown = false; const shutdown = async () => { if (shuttingDown) return; shuttingDown = true; try { shutdownLogger.info('Shutting down gracefully...'); if (httpSessions.size > 0) { shutdownLogger.info(`Closing ${httpSessions.size} active HTTP session(s)`); } for (const [sessionId, session] of httpSessions.entries()) { try { await session.transport.close(); } catch (err) { shutdownLogger.error(`Error closing transport for session ${sessionId}`, err); } try { await session.server.close(); } catch (err) { shutdownLogger.error(`Error closing server for session ${sessionId}`, err); } } httpSessions.clear(); if (httpServerInstance) { await new Promise((resolve) => { httpServerInstance.close(() => resolve()); }); } if (transportInstance && 'close' in transportInstance && typeof transportInstance.close === 'function') { await transportInstance.close(); } if (serverInstance && typeof serverInstance.close === 'function') { await serverInstance.close(); } process.exit(0); } catch (err) { shutdownLogger.error('Error during shutdown', err); process.exit(1); } }; ['SIGINT', 'SIGTERM'].forEach((signal) => { process.on(signal, shutdown); }); }