UNPKG

mcp-cisco-support

Version:

MCP server for Cisco Support APIs including Bug Search and future tools

467 lines 20.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createSSEServer = createSSEServer; const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js"); const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const node_crypto_1 = require("node:crypto"); const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const helmet_1 = __importDefault(require("helmet")); const morgan_1 = __importDefault(require("morgan")); const mcp_server_js_1 = require("./mcp-server.js"); // Generate a secure session token for authentication function generateAuthToken() { // Check if token is provided via environment variable const envToken = process.env.MCP_BEARER_TOKEN; if (envToken) { return envToken; } // Generate a new random token return (0, node_crypto_1.randomUUID)().replace(/-/g, ''); } // Create authentication middleware function createAuthMiddleware(authToken, enableAuth) { return (req, res, next) => { if (!enableAuth) { return next(); } // Skip auth for public endpoints if (req.path === '/health' || req.path === '/') { return next(); } // Check for Bearer token in Authorization header (primary method) const authHeader = req.headers['authorization']; let token; if (authHeader && authHeader.startsWith('Bearer ')) { token = authHeader.replace('Bearer ', ''); } else { // Fallback: check query parameter for convenience (less secure) token = req.query.token; } if (!token || token !== authToken) { mcp_server_js_1.logger.warn('Unauthorized request', { path: req.path, method: req.method, hasToken: !!token, tokenMatch: token === authToken, authHeader: !!authHeader }); res.status(401).json({ error: 'Unauthorized', message: 'Valid Bearer token required', hint: 'Include "Authorization: Bearer <token>" header or ?token=<token> query parameter' }); return; } next(); }; } function createSSEServer(mcpServer) { const app = (0, express_1.default)(); // Check environment variables for auth configuration const enableAuth = process.env.DANGEROUSLY_OMIT_AUTH !== 'true'; const authToken = generateAuthToken(); // Display authentication info prominently like MCP Inspector if (enableAuth) { const port = process.env.PORT || 3000; const isEnvToken = !!process.env.MCP_BEARER_TOKEN; console.log('Starting MCP Cisco Support server...'); console.log(`⚙️ Server listening on 127.0.0.1:${port}`); console.log(`🔑 Bearer token: ${authToken}`); if (isEnvToken) { console.log(' ✅ Using token from MCP_BEARER_TOKEN environment variable'); } else { console.log(' 🎲 Generated random token (set MCP_BEARER_TOKEN to use custom token)'); } console.log('Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth'); console.log(''); console.log('🔗 Access with Bearer token:'); console.log(` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}/mcp`); console.log(` (Query parameter also supported: ?token=${authToken})`); console.log(''); console.log(`🌐 MCP Server is up and running at http://127.0.0.1:${port} 🚀`); console.log(''); } else { console.log('⚠️ HTTP authentication DISABLED (DANGEROUSLY_OMIT_AUTH=true)'); console.log(' This is not recommended for production use'); } // Security middleware app.use((0, helmet_1.default)()); app.use((0, cors_1.default)()); app.use((0, morgan_1.default)('combined')); app.use(express_1.default.json({ limit: '10mb' })); app.use(express_1.default.urlencoded({ extended: true })); // MCP Protocol Version header middleware app.use((req, res, next) => { res.setHeader('MCP-Protocol-Version', '2025-06-18'); next(); }); // Apply authentication middleware app.use(createAuthMiddleware(authToken, enableAuth)); const transportMap = new Map(); const streamableTransports = {}; // Heartbeat configuration const HEARTBEAT_INTERVAL = 30000; // 30 seconds const CONNECTION_TIMEOUT = 120000; // 2 minutes const heartbeatIntervals = new Map(); // Helper function to start heartbeat for SSE connection function startHeartbeat(sessionId, res) { return setInterval(() => { try { // Send heartbeat message to keep connection alive res.write('event: heartbeat\n'); res.write(`data: {"type":"heartbeat","timestamp":"${new Date().toISOString()}"}\n\n`); mcp_server_js_1.logger.info('Heartbeat sent', { sessionId }); } catch (error) { mcp_server_js_1.logger.warn('Heartbeat failed, cleaning up connection', { sessionId, error }); stopHeartbeat(sessionId); cleanupConnection(sessionId, res); } }, HEARTBEAT_INTERVAL); } // Helper function to stop heartbeat function stopHeartbeat(sessionId) { const interval = heartbeatIntervals.get(sessionId); if (interval) { clearInterval(interval); heartbeatIntervals.delete(sessionId); mcp_server_js_1.logger.info('Heartbeat stopped', { sessionId }); } } // Helper function to cleanup connection function cleanupConnection(sessionId, res) { transportMap.delete(sessionId); stopHeartbeat(sessionId); if (res && !res.destroyed) { try { res.end(); } catch (error) { mcp_server_js_1.logger.info('Response already ended', { sessionId }); } } mcp_server_js_1.logger.info('SSE connection cleaned up', { sessionId, remainingConnections: transportMap.size }); } // SSE endpoint - establishes SSE connection and connects to MCP server app.get("/sse", async (req, res) => { let sessionId; try { mcp_server_js_1.logger.info('SSE connection request received'); // Set connection timeout req.setTimeout(CONNECTION_TIMEOUT, () => { mcp_server_js_1.logger.warn('SSE connection timeout', { sessionId }); if (sessionId) { cleanupConnection(sessionId, res); } }); // Create SSE transport with proper endpoint - it will set the headers const transport = new sse_js_1.SSEServerTransport("/messages", res); sessionId = transport.sessionId; // Store transport for message handling transportMap.set(sessionId, transport); // Start heartbeat to keep connection alive const heartbeatInterval = startHeartbeat(sessionId, res); heartbeatIntervals.set(sessionId, heartbeatInterval); mcp_server_js_1.logger.info('SSE transport created', { sessionId: transport.sessionId }); // Connect MCP server to transport await mcpServer.connect(transport); mcp_server_js_1.logger.info('MCP server connected to SSE transport', { sessionId: transport.sessionId, totalTransports: transportMap.size }); // Set up cleanup handlers before connecting transport.onclose = () => { mcp_server_js_1.logger.info('SSE connection closed', { sessionId }); if (sessionId) { cleanupConnection(sessionId); } }; // Enhanced error handling with detailed logging transport.onerror = (error) => { mcp_server_js_1.logger.error('SSE transport error', { sessionId: sessionId, error: error.message, errorType: typeof error, stack: error instanceof Error ? error.stack : undefined }); // Clean up on transport error if (sessionId) { cleanupConnection(sessionId, res); } }; // Handle cleanup when connection closes req.on('close', () => { mcp_server_js_1.logger.info('SSE request closed by client', { sessionId }); if (sessionId) { cleanupConnection(sessionId); } }); // Handle errors on response stream res.on('error', (error) => { mcp_server_js_1.logger.error('SSE response stream error', { sessionId: sessionId, error: error.message, code: error.code, errno: error.errno }); if (sessionId) { cleanupConnection(sessionId); } }); // Handle connection timeout res.on('timeout', () => { mcp_server_js_1.logger.warn('SSE response timeout', { sessionId }); if (sessionId) { cleanupConnection(sessionId, res); } }); } catch (error) { mcp_server_js_1.logger.error('Failed to establish SSE connection', { sessionId, error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : undefined }); // Cleanup on error if (sessionId) { cleanupConnection(sessionId, res); } if (!res.headersSent) { res.status(500).json({ error: 'Failed to establish SSE connection', message: error instanceof Error ? error.message : 'Unknown error', retryAfter: '5' // Suggest client retry after 5 seconds }); } } }); // MCP JSON-RPC endpoint - handles direct MCP calls using StreamableHTTP app.post("/mcp", async (req, res) => { try { mcp_server_js_1.logger.info('Direct MCP call received', { method: req.body?.method }); // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && streamableTransports[sessionId]) { // Reuse existing transport transport = streamableTransports[sessionId]; mcp_server_js_1.logger.info('Reusing existing transport', { sessionId }); } else if (!sessionId && (0, types_js_1.isInitializeRequest)(req.body)) { // New initialization request transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(), onsessioninitialized: (sessionId) => { mcp_server_js_1.logger.info('Session initialized', { sessionId }); streamableTransports[sessionId] = transport; } }); // Set up cleanup handler transport.onclose = () => { const sid = transport.sessionId; if (sid && streamableTransports[sid]) { mcp_server_js_1.logger.info('Transport closed, cleaning up', { sessionId: sid }); delete streamableTransports[sid]; } }; // Connect the transport to the MCP server await mcpServer.connect(transport); await transport.handleRequest(req, res, req.body); return; // Already handled } else { // Invalid request - no session ID or not initialization request mcp_server_js_1.logger.error('Invalid MCP request', { sessionId, isInit: (0, types_js_1.isInitializeRequest)(req.body) }); res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); return; } // Handle the request with existing transport await transport.handleRequest(req, res, req.body); } catch (error) { mcp_server_js_1.logger.error('Failed to handle MCP call', { error }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // MCP GET endpoint - handles SSE streams for StreamableHTTP app.get("/mcp", async (req, res) => { try { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !streamableTransports[sessionId]) { mcp_server_js_1.logger.error('Invalid session ID for GET request', { sessionId }); res.status(400).send('Invalid or missing session ID'); return; } mcp_server_js_1.logger.info('SSE stream request', { sessionId }); const transport = streamableTransports[sessionId]; await transport.handleRequest(req, res); } catch (error) { mcp_server_js_1.logger.error('Failed to handle SSE stream', { error }); if (!res.headersSent) { res.status(500).send('Error establishing SSE stream'); } } }); // MCP DELETE endpoint - handles session termination app.delete("/mcp", async (req, res) => { try { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !streamableTransports[sessionId]) { mcp_server_js_1.logger.error('Invalid session ID for DELETE request', { sessionId }); res.status(400).send('Invalid or missing session ID'); return; } mcp_server_js_1.logger.info('Session termination request', { sessionId }); const transport = streamableTransports[sessionId]; await transport.handleRequest(req, res); } catch (error) { mcp_server_js_1.logger.error('Failed to handle session termination', { error }); if (!res.headersSent) { res.status(500).send('Error processing session termination'); } } }); // Messages endpoint - handles MCP JSON-RPC messages app.post("/messages", async (req, res) => { const sessionId = req.query.sessionId; if (!sessionId) { mcp_server_js_1.logger.error('Message received without sessionId'); res.status(400).json({ error: 'sessionId is required' }); return; } mcp_server_js_1.logger.info('Message received for session', { sessionId, method: req.body?.method }); const transport = transportMap.get(sessionId); if (transport) { try { // Let the transport handle the message - must await and pass req.body await transport.handlePostMessage(req, res, req.body); } catch (error) { mcp_server_js_1.logger.error('Failed to handle message', { sessionId, error: error instanceof Error ? error.message : error }); if (!res.headersSent) { res.status(500).json({ error: 'Failed to handle message', message: error instanceof Error ? error.message : 'Unknown error' }); } } } else { mcp_server_js_1.logger.error('Transport not found for session', { sessionId }); res.status(404).json({ error: 'Session not found', sessionId }); } }); // Health check endpoint app.get('/health', (_req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), activeTransports: transportMap.size, server: 'mcp-cisco-support-sse' }); }); // Server info endpoint (publicly accessible to show auth info) app.get('/', (_req, res) => { const port = process.env.PORT || 3000; res.json({ name: 'Cisco Support MCP SSE Server', description: 'MCP Server-Sent Events transport for Cisco Support APIs', authentication: { enabled: enableAuth, type: 'Bearer Token', note: enableAuth ? 'Token displayed in console logs on startup. Use Authorization: Bearer <token> header (recommended) or ?token=<token> query parameter' : 'Authentication disabled via DANGEROUSLY_OMIT_AUTH=true' }, endpoints: { mcp: '/mcp (POST/GET/DELETE) - MCP StreamableHTTP endpoint', sse: '/sse (GET) - Legacy SSE connection', messages: '/messages (POST) - Legacy SSE messages', health: '/health (GET) - Health check (no auth required)' }, examples: enableAuth ? { curl: `curl -H "Authorization: Bearer <token>" http://localhost:${port}/mcp`, curlQuery: `curl http://localhost:${port}/mcp?token=<token>`, javascript: `fetch('http://localhost:${port}/mcp', { headers: { 'Authorization': 'Bearer <token>' } })` } : undefined, activeTransports: transportMap.size + Object.keys(streamableTransports).length, timestamp: new Date().toISOString() }); }); // Error handling middleware app.use((error, req, res, _next) => { mcp_server_js_1.logger.error('Unhandled SSE server error', { error: error.message, path: req.path, method: req.method }); res.status(500).json({ error: 'Internal server error', timestamp: new Date().toISOString() }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found', path: req.path, availableEndpoints: ['/mcp (POST/GET/DELETE)', '/sse', '/messages', '/health', '/'], timestamp: new Date().toISOString() }); }); // Graceful shutdown handler function gracefulShutdown() { mcp_server_js_1.logger.info('Shutting down SSE server, cleaning up connections', { activeConnections: transportMap.size, activeHeartbeats: heartbeatIntervals.size }); // Clean up all connections for (const sessionId of transportMap.keys()) { cleanupConnection(sessionId); } // Clear any remaining heartbeat intervals for (const [sessionId, interval] of heartbeatIntervals.entries()) { clearInterval(interval); heartbeatIntervals.delete(sessionId); } mcp_server_js_1.logger.info('SSE server cleanup completed'); } // Handle process termination process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown); return app; } //# sourceMappingURL=sse-server.js.map