UNPKG

digital-samba-mcp-server

Version:

Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API

1,039 lines 49.1 kB
// Node.js built-in modules import { randomUUID } from 'crypto'; import { createServer as createHttpServer } from 'http'; import { config as loadEnv } from 'dotenv'; // Check if we're in MCP JSON-RPC mode (used by Claude Desktop) const isMcpJsonRpcMode = process.env.MCP_JSON_RPC_MODE === 'true'; // If in MCP mode, we need to be very careful about console output // as it would interfere with the JSON-RPC protocol if (isMcpJsonRpcMode) { // In MCP mode, redirect all stdout console outputs to stderr const originalConsole = { log: console.log, info: console.info, warn: console.warn }; // Only output errors and warnings to stderr, suppress other logs console.log = () => { }; console.info = () => { }; // Keep console.error and console.warn as they go to stderr } // If NO_CONSOLE_OUTPUT is set, completely suppress console output if (process.env.NO_CONSOLE_OUTPUT === 'true') { console.log = () => { }; console.error = () => { }; console.warn = () => { }; console.info = () => { }; console.debug = () => { }; } // Load environment variables from .env file loadEnv(); // External dependencies import express from 'express'; import { z } from 'zod'; // MCP SDK imports import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; // Local modules import apiKeyContext, { extractApiKey, getApiKeyFromRequest } from './auth.js'; import { EnhancedDigitalSambaApiClient } from './digital-samba-api-enhanced.js'; import { CircuitBreakerApiClient } from './digital-samba-api-circuit-breaker.js'; import ResilientApiClient from './digital-samba-api-resilient.js'; import { MemoryCache } from './cache.js'; import { DigitalSambaApiClient } from './digital-samba-api.js'; import logger from './logger.js'; import metricsRegistry, { initializeMetrics } from './metrics.js'; import { createApiKeyRateLimiter } from './rate-limiter.js'; import { setupRecordingFunctionality } from './recordings.js'; import WebhookService, { setupWebhookTools } from './webhooks.js'; import gracefulDegradation, { ServiceHealthStatus } from './graceful-degradation.js'; // Create and configure the MCP server export function createServer(options) { // Configure environment let PORT = 4521; // Uncommon port to avoid conflicts if (options?.port !== undefined) { PORT = options.port; } else if (process.env.PORT) { PORT = parseInt(process.env.PORT); } const API_URL = options?.apiUrl || process.env.DIGITAL_SAMBA_API_URL || 'https://api.digitalsamba.com/api/v1'; const WEBHOOK_SECRET = options?.webhookSecret || process.env.WEBHOOK_SECRET; const WEBHOOK_ENDPOINT = options?.webhookEndpoint || process.env.WEBHOOK_ENDPOINT || '/webhooks/digitalsamba'; const PUBLIC_URL = options?.publicUrl || process.env.PUBLIC_URL || `http://localhost:${PORT}`; const ENABLE_RATE_LIMITING = options?.enableRateLimiting !== undefined ? options.enableRateLimiting : process.env.ENABLE_RATE_LIMITING === 'true'; const RATE_LIMIT_REQUESTS_PER_MINUTE = options?.rateLimitRequestsPerMinute || (process.env.RATE_LIMIT_REQUESTS_PER_MINUTE ? parseInt(process.env.RATE_LIMIT_REQUESTS_PER_MINUTE) : 60); const ENABLE_CACHE = options?.enableCache !== undefined ? options.enableCache : process.env.ENABLE_CACHE === 'true'; const CACHE_TTL = options?.cacheTtl || (process.env.CACHE_TTL ? parseInt(process.env.CACHE_TTL) : 5 * 60 * 1000); // 5 minutes default const ENABLE_CONNECTION_MANAGEMENT = options?.enableConnectionManagement !== undefined ? options.enableConnectionManagement : process.env.ENABLE_CONNECTION_MANAGEMENT === 'true'; const ENABLE_TOKEN_MANAGEMENT = options?.enableTokenManagement !== undefined ? options.enableTokenManagement : process.env.ENABLE_TOKEN_MANAGEMENT === 'true'; const ENABLE_RESOURCE_OPTIMIZATION = options?.enableResourceOptimization !== undefined ? options.enableResourceOptimization : process.env.ENABLE_RESOURCE_OPTIMIZATION === 'true'; const ENABLE_CIRCUIT_BREAKER = options?.enableCircuitBreaker !== undefined ? options.enableCircuitBreaker : process.env.ENABLE_CIRCUIT_BREAKER === 'true'; const CIRCUIT_BREAKER_FAILURE_THRESHOLD = options?.circuitBreakerFailureThreshold || (process.env.CIRCUIT_BREAKER_FAILURE_THRESHOLD ? parseInt(process.env.CIRCUIT_BREAKER_FAILURE_THRESHOLD) : 5); const CIRCUIT_BREAKER_RESET_TIMEOUT = options?.circuitBreakerResetTimeout || (process.env.CIRCUIT_BREAKER_RESET_TIMEOUT ? parseInt(process.env.CIRCUIT_BREAKER_RESET_TIMEOUT) : 30000); const ENABLE_GRACEFUL_DEGRADATION = options?.enableGracefulDegradation !== undefined ? options.enableGracefulDegradation : process.env.ENABLE_GRACEFUL_DEGRADATION === 'true'; const GRACEFUL_DEGRADATION_MAX_RETRIES = options?.gracefulDegradationMaxRetries || (process.env.GRACEFUL_DEGRADATION_MAX_RETRIES ? parseInt(process.env.GRACEFUL_DEGRADATION_MAX_RETRIES) : 3); const GRACEFUL_DEGRADATION_INITIAL_DELAY = options?.gracefulDegradationInitialDelay || (process.env.GRACEFUL_DEGRADATION_INITIAL_DELAY ? parseInt(process.env.GRACEFUL_DEGRADATION_INITIAL_DELAY) : 1000); const CONNECTION_POOL_SIZE = options?.connectionPoolSize || (process.env.CONNECTION_POOL_SIZE ? parseInt(process.env.CONNECTION_POOL_SIZE) : 5); const ENABLE_METRICS = options?.enableMetrics !== undefined ? options.enableMetrics : process.env.ENABLE_METRICS === 'true'; const METRICS_ENDPOINT = options?.metricsEndpoint || process.env.METRICS_ENDPOINT || '/metrics'; const METRICS_PREFIX = options?.metricsPrefix || process.env.METRICS_PREFIX || 'digital_samba_mcp_'; const COLLECT_DEFAULT_METRICS = options?.collectDefaultMetrics !== undefined ? options.collectDefaultMetrics : process.env.COLLECT_DEFAULT_METRICS === 'true'; // Create the MCP server const server = new McpServer({ name: 'Digital Samba MCP Server', version: '0.1.0', }); // Initialize cache if enabled let apiCache; if (ENABLE_CACHE) { logger.info('Initializing API response cache', { cacheTtl: CACHE_TTL }); apiCache = new MemoryCache({ ttl: CACHE_TTL, maxItems: 1000, useEtag: true }); } // Create webhook service const webhookService = new WebhookService(server, { secret: WEBHOOK_SECRET, endpoint: WEBHOOK_ENDPOINT }); // Set up webhook tools setupWebhookTools(server, webhookService, API_URL); // Set up recording functionality setupRecordingFunctionality(server, API_URL); // ------------------------------------------------------------------- // Resources // ------------------------------------------------------------------- // Resource for listing rooms server.resource('rooms', new ResourceTemplate('digitalsamba://rooms', { list: undefined }), async (uri, _params, request) => { logger.info('Listing rooms'); // Get API key from various sources in priority order: // 1. Direct API key passed in options during server creation // 2. API key from session context // 3. Environment variable let apiKey = options?.apiKey; if (!apiKey) { apiKey = getApiKeyFromRequest(request); } if (!apiKey && process.env.DIGITAL_SAMBA_API_KEY) { apiKey = process.env.DIGITAL_SAMBA_API_KEY; logger.debug('Using API key from environment variable'); } if (!apiKey) { throw new Error('No API key found. Please include an Authorization header with a Bearer token.'); } // Create API client logger.debug('Creating API client using available API key'); let client; if (ENABLE_CIRCUIT_BREAKER && ENABLE_GRACEFUL_DEGRADATION) { // Use resilient API client with circuit breaker and graceful degradation logger.debug('Using resilient API client with circuit breaker and graceful degradation'); // First create the base client const baseClient = new DigitalSambaApiClient(apiKey, API_URL, apiCache); // Then wrap it with resilient client client = ResilientApiClient.withResilience(baseClient, { cache: apiCache, circuitBreaker: { failureThreshold: CIRCUIT_BREAKER_FAILURE_THRESHOLD, resetTimeout: CIRCUIT_BREAKER_RESET_TIMEOUT, requestTimeout: 15000, // 15 second timeout for requests initialRequestTimeout: 60000 // 60 second timeout for initial requests }, gracefulDegradation: { maxRetryAttempts: GRACEFUL_DEGRADATION_MAX_RETRIES, initialRetryDelay: GRACEFUL_DEGRADATION_INITIAL_DELAY, retryBackoffFactor: 2, maxRetryDelay: 30000 } }); } else if (ENABLE_CIRCUIT_BREAKER) { // Use circuit breaker API client logger.debug('Using circuit breaker API client'); // First create the base client const baseClient = new DigitalSambaApiClient(undefined, API_URL, apiCache); // Then wrap it with circuit breaker client = CircuitBreakerApiClient.withCircuitBreaker(baseClient, { defaultOptions: { failureThreshold: CIRCUIT_BREAKER_FAILURE_THRESHOLD, resetTimeout: CIRCUIT_BREAKER_RESET_TIMEOUT, requestTimeout: 15000, // 15 second timeout for requests initialRequestTimeout: 60000 // 60 second timeout for initial requests } }); // Initialize the API connection with a timeout-bypassing request // This helps prevent timeout errors during the MCP server initialization try { logger.info('Initializing API connection...'); const initResult = await client.initializeConnection(); if (initResult) { logger.info('API connection initialized successfully'); } else { logger.warn('API connection initialization did not complete successfully'); } } catch (initError) { logger.warn('Failed to initialize API connection', { error: initError instanceof Error ? initError.message : String(initError) }); // Continue anyway, as the regular requests might still work } } else if (ENABLE_CONNECTION_MANAGEMENT || ENABLE_TOKEN_MANAGEMENT || ENABLE_RESOURCE_OPTIMIZATION) { // Use enhanced API client logger.debug('Using enhanced API client with additional features enabled'); client = new EnhancedDigitalSambaApiClient(undefined, API_URL, apiCache, { enableConnectionManagement: ENABLE_CONNECTION_MANAGEMENT, enableTokenManagement: ENABLE_TOKEN_MANAGEMENT, enableResourceOptimization: ENABLE_RESOURCE_OPTIMIZATION, connectionPoolSize: CONNECTION_POOL_SIZE }); } else { // Use standard API client client = new DigitalSambaApiClient(undefined, API_URL, apiCache); } try { // Get rooms from API const response = await client.listRooms(); const rooms = response.data || []; logger.debug(`Found ${rooms.length} rooms`); // Format rooms as resource contents const contents = rooms.map(room => ({ uri: `digitalsamba://rooms/${room.id}`, text: JSON.stringify(room, null, 2), })); return { contents }; } catch (error) { logger.error('Error fetching rooms', { error: error instanceof Error ? error.message : String(error) }); throw error; } }); // Resource for getting a specific room server.resource('room', new ResourceTemplate('digitalsamba://rooms/{roomId}', { list: undefined }), async (uri, params, request) => { const { roomId } = params; if (!roomId) { throw new Error('Room ID is required.'); } logger.info('Getting room details', { roomId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { throw new Error('No API key found. Please include an Authorization header with a Bearer token.'); } // Create API client logger.debug('Creating API client using context API key'); let client; if (ENABLE_CONNECTION_MANAGEMENT || ENABLE_TOKEN_MANAGEMENT || ENABLE_RESOURCE_OPTIMIZATION) { // Use enhanced API client logger.debug('Using enhanced API client with additional features enabled'); client = new EnhancedDigitalSambaApiClient(undefined, API_URL, apiCache, { enableConnectionManagement: ENABLE_CONNECTION_MANAGEMENT, enableTokenManagement: ENABLE_TOKEN_MANAGEMENT, enableResourceOptimization: ENABLE_RESOURCE_OPTIMIZATION, connectionPoolSize: CONNECTION_POOL_SIZE }); } else { // Use standard API client client = new DigitalSambaApiClient(undefined, API_URL, apiCache); } try { // Get room from API const room = await client.getRoom(roomId); // Format room as resource content const content = { uri: uri.href, text: JSON.stringify(room, null, 2), }; return { contents: [content] }; } catch (error) { logger.error('Error fetching room', { roomId, error: error instanceof Error ? error.message : String(error) }); throw error; } }); // ------------------------------------------------------------------- // Tools // ------------------------------------------------------------------- // Tool for creating a room server.tool('create-room', { name: z.string().min(3).max(100).optional(), description: z.string().max(500).optional(), friendly_url: z.string().min(3).max(32).optional(), privacy: z.enum(['public', 'private']).default('public'), max_participants: z.number().min(2).max(2000).optional(), }, async (params, request) => { const { name, description, friendly_url, privacy, max_participants } = params; logger.info('Creating room', { roomName: name, privacy }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [{ type: 'text', text: 'No API key found. Please include an Authorization header with a Bearer token.' }], isError: true, }; } // Create API client with the provided key logger.debug('Creating API client with key', { apiKeyLength: apiKey ? apiKey.length : 0, apiUrl: API_URL }); let client; if (ENABLE_CONNECTION_MANAGEMENT || ENABLE_TOKEN_MANAGEMENT || ENABLE_RESOURCE_OPTIMIZATION) { // Use enhanced API client logger.debug('Using enhanced API client with additional features enabled'); client = new EnhancedDigitalSambaApiClient(apiKey, API_URL, apiCache, { enableConnectionManagement: ENABLE_CONNECTION_MANAGEMENT, enableTokenManagement: ENABLE_TOKEN_MANAGEMENT, enableResourceOptimization: ENABLE_RESOURCE_OPTIMIZATION, connectionPoolSize: CONNECTION_POOL_SIZE }); } else { // Use standard API client client = new DigitalSambaApiClient(apiKey, API_URL, apiCache); } try { // Create room settings object const roomSettings = { name: name || 'Test Room', // Ensure we have a name description, friendly_url, privacy, max_participants, }; // Create room const room = await client.createRoom(roomSettings); logger.info('Room created successfully', { roomId: room.id }); return { content: [ { type: 'text', text: `Room created successfully!\n\n${JSON.stringify(room, null, 2)}`, }, ], }; } catch (error) { logger.error('Error creating room', { error: error instanceof Error ? error.message : String(error) }); return { content: [ { type: 'text', text: `Error creating room: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Tool for generating a room token server.tool('generate-token', { roomId: z.string(), userName: z.string().min(1).max(100).optional(), role: z.string().optional(), externalId: z.string().optional(), }, async (params, request) => { const { roomId, userName, role, externalId } = params; logger.info('Generating room token', { roomId, userName, role }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [{ type: 'text', text: 'No API key found. Please include an Authorization header with a Bearer token.' }], isError: true, }; } // Create API client with the provided key logger.debug('Creating API client with key', { apiKeyLength: apiKey ? apiKey.length : 0, apiUrl: API_URL }); let client; if (ENABLE_CONNECTION_MANAGEMENT || ENABLE_TOKEN_MANAGEMENT || ENABLE_RESOURCE_OPTIMIZATION) { // Use enhanced API client logger.debug('Using enhanced API client with additional features enabled'); client = new EnhancedDigitalSambaApiClient(apiKey, API_URL, apiCache, { enableConnectionManagement: ENABLE_CONNECTION_MANAGEMENT, enableTokenManagement: ENABLE_TOKEN_MANAGEMENT, enableResourceOptimization: ENABLE_RESOURCE_OPTIMIZATION, connectionPoolSize: CONNECTION_POOL_SIZE }); } else { // Use standard API client client = new DigitalSambaApiClient(apiKey, API_URL, apiCache); } try { // Generate token options const tokenOptions = { u: userName || undefined, role: role || undefined, ud: externalId || undefined, }; // Generate token - use token refresh if enabled let token; if (ENABLE_TOKEN_MANAGEMENT && client instanceof EnhancedDigitalSambaApiClient && request.sessionId) { // Use token refresh token = await client.generateRoomTokenWithRefresh(roomId, tokenOptions, request.sessionId); logger.info('Generated token with auto-refresh', { roomId, expiresAt: token.expiresAt }); } else { // Standard token generation token = await client.generateRoomToken(roomId, tokenOptions); } logger.info('Token generated successfully', { roomId }); return { content: [ { type: 'text', text: `Token generated successfully!\n\n${JSON.stringify(token, null, 2)}`, }, ], }; } catch (error) { logger.error('Error generating token', { roomId, error: error instanceof Error ? error.message : String(error) }); return { content: [ { type: 'text', text: `Error generating token: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Update room tool server.tool('update-room', { roomId: z.string(), name: z.string().min(3).max(100).optional(), description: z.string().max(500).optional(), friendly_url: z.string().min(3).max(32).optional(), privacy: z.enum(['public', 'private']).optional(), max_participants: z.number().min(2).max(2000).optional(), }, async (params, request) => { const { roomId, name, description, friendly_url, privacy, max_participants } = params; if (!roomId) { return { content: [{ type: 'text', text: 'Room ID is required.' }], isError: true, }; } logger.info('Updating room', { roomId, name, privacy }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [{ type: 'text', text: 'No API key found. Please include an Authorization header with a Bearer token.' }], isError: true, }; } // Create API client with the provided key logger.debug('Creating API client with key', { apiKeyLength: apiKey ? apiKey.length : 0, apiUrl: API_URL }); let client; if (ENABLE_CONNECTION_MANAGEMENT || ENABLE_TOKEN_MANAGEMENT || ENABLE_RESOURCE_OPTIMIZATION) { // Use enhanced API client logger.debug('Using enhanced API client with additional features enabled'); client = new EnhancedDigitalSambaApiClient(apiKey, API_URL, apiCache, { enableConnectionManagement: ENABLE_CONNECTION_MANAGEMENT, enableTokenManagement: ENABLE_TOKEN_MANAGEMENT, enableResourceOptimization: ENABLE_RESOURCE_OPTIMIZATION, connectionPoolSize: CONNECTION_POOL_SIZE }); } else { // Use standard API client client = new DigitalSambaApiClient(apiKey, API_URL, apiCache); } try { // Create room settings object const roomSettings = { name, description, friendly_url, privacy, max_participants, }; // Update room const room = await client.updateRoom(roomId, roomSettings); logger.info('Room updated successfully', { roomId: room.id }); return { content: [ { type: 'text', text: `Room updated successfully!\n\n${JSON.stringify(room, null, 2)}`, }, ], }; } catch (error) { logger.error('Error updating room', { roomId, error: error instanceof Error ? error.message : String(error) }); return { content: [ { type: 'text', text: `Error updating room: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Delete room tool server.tool('delete-room', { roomId: z.string(), }, async (params, request) => { const { roomId } = params; if (!roomId) { return { content: [{ type: 'text', text: 'Room ID is required.' }], isError: true, }; } logger.info('Deleting room', { roomId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [{ type: 'text', text: 'No API key found. Please include an Authorization header with a Bearer token.' }], isError: true, }; } // Create API client with the provided key logger.debug('Creating API client with key', { apiKeyLength: apiKey ? apiKey.length : 0, apiUrl: API_URL }); let client; if (ENABLE_CONNECTION_MANAGEMENT || ENABLE_TOKEN_MANAGEMENT || ENABLE_RESOURCE_OPTIMIZATION) { // Use enhanced API client logger.debug('Using enhanced API client with additional features enabled'); client = new EnhancedDigitalSambaApiClient(apiKey, API_URL, apiCache, { enableConnectionManagement: ENABLE_CONNECTION_MANAGEMENT, enableTokenManagement: ENABLE_TOKEN_MANAGEMENT, enableResourceOptimization: ENABLE_RESOURCE_OPTIMIZATION, connectionPoolSize: CONNECTION_POOL_SIZE }); } else { // Use standard API client client = new DigitalSambaApiClient(apiKey, API_URL, apiCache); } try { // Delete room await client.deleteRoom(roomId); logger.info('Room deleted successfully', { roomId }); return { content: [ { type: 'text', text: `Room ${roomId} deleted successfully!`, }, ], }; } catch (error) { logger.error('Error deleting room', { roomId, error: error instanceof Error ? error.message : String(error) }); return { content: [ { type: 'text', text: `Error deleting room: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); return { server, port: PORT, apiUrl: API_URL, webhookEndpoint: WEBHOOK_ENDPOINT, publicUrl: PUBLIC_URL, cache: apiCache, enableConnectionManagement: ENABLE_CONNECTION_MANAGEMENT, enableTokenManagement: ENABLE_TOKEN_MANAGEMENT, enableResourceOptimization: ENABLE_RESOURCE_OPTIMIZATION, enableCircuitBreaker: ENABLE_CIRCUIT_BREAKER, circuitBreakerFailureThreshold: CIRCUIT_BREAKER_FAILURE_THRESHOLD, circuitBreakerResetTimeout: CIRCUIT_BREAKER_RESET_TIMEOUT, enableGracefulDegradation: ENABLE_GRACEFUL_DEGRADATION, gracefulDegradationMaxRetries: GRACEFUL_DEGRADATION_MAX_RETRIES, gracefulDegradationInitialDelay: GRACEFUL_DEGRADATION_INITIAL_DELAY, enableMetrics: ENABLE_METRICS, metricsEndpoint: METRICS_ENDPOINT, metricsPrefix: METRICS_PREFIX, collectDefaultMetrics: COLLECT_DEFAULT_METRICS }; } // Start a server with the provided options export function startServer(options) { // Check if we should run in silent mode const enableSilentMode = options?.enableSilentMode || process.env.MCP_JSON_RPC_MODE === 'true'; // Only log if not in silent mode const shouldLog = !enableSilentMode && process.env.NO_CONSOLE_OUTPUT !== 'true'; if (shouldLog) logger.debug("startServer function called with options:", options); try { // Detect debug options const debugTimeouts = options?.debugTimeouts || process.env.DEBUG_TIMEOUTS === 'true'; const debugInitialization = options?.debugInitialization || process.env.DEBUG_INITIALIZATION === 'true'; const initialRequestTimeout = options?.initialRequestTimeout || process.env.INITIAL_REQUEST_TIMEOUT ? parseInt(process.env.INITIAL_REQUEST_TIMEOUT) : 60000; if (debugTimeouts) { logger.info("Timeout debugging enabled"); console.log("Timeout debugging enabled"); console.log("Initial request timeout:", initialRequestTimeout); // Set the environment variable for child processes process.env.DEBUG_TIMEOUTS = 'true'; process.env.INITIAL_REQUEST_TIMEOUT = initialRequestTimeout.toString(); } if (debugInitialization) { logger.info("Initialization debugging enabled"); console.log("Initialization debugging enabled"); // Set the environment variable for child processes process.env.DEBUG_INITIALIZATION = 'true'; } // Create Express app if (shouldLog) logger.debug("Creating Express app..."); const app = express(); app.use(express.json()); if (shouldLog) logger.debug("Express app created and JSON middleware added"); // Create the MCP server if (shouldLog) logger.debug("Creating MCP server..."); const serverConfig = createServer({ ...options, initialRequestTimeout, debugTimeouts, debugInitialization }); // Add metrics middleware if enabled if (serverConfig.enableMetrics) { if (shouldLog) logger.debug("Adding metrics middleware..."); app.use(metricsRegistry.createHttpMetricsMiddleware()); metricsRegistry.registerMetricsEndpoint(app, serverConfig.metricsEndpoint); if (shouldLog) logger.debug(`Metrics endpoint registered at ${serverConfig.metricsEndpoint}`); } const { server, port, apiUrl, webhookEndpoint, publicUrl, cache, enableConnectionManagement, enableTokenManagement, enableResourceOptimization, enableMetrics, metricsEndpoint, metricsPrefix, collectDefaultMetrics } = serverConfig; if (shouldLog) logger.debug("MCP server created with configuration:", { port, apiUrl, webhookEndpoint, publicUrl, hasCache: !!cache, enableConnectionManagement, enableTokenManagement, enableResourceOptimization, enableMetrics, metricsEndpoint }); // Initialize metrics if enabled if (enableMetrics) { console.log("Initializing metrics collection..."); const metrics = initializeMetrics({ prefix: metricsPrefix, defaultMetrics: collectDefaultMetrics, enableHttpMetrics: true, enableApiMetrics: true, enableCacheMetrics: cache !== undefined, enableRateLimitMetrics: options?.enableRateLimiting || process.env.ENABLE_RATE_LIMITING === 'true' }); // Track active sessions metrics.activeSessions.set(0); console.log("Metrics collection initialized"); } // Map to store transports by session ID const transports = {}; console.log("Transport map initialized"); // Add rate limiting middleware if enabled if (options?.enableRateLimiting || process.env.ENABLE_RATE_LIMITING === 'true') { const requestsPerMinute = options?.rateLimitRequestsPerMinute || (process.env.RATE_LIMIT_REQUESTS_PER_MINUTE ? parseInt(process.env.RATE_LIMIT_REQUESTS_PER_MINUTE) : 60); console.log("Enabling rate limiting:", { requestsPerMinute }); logger.info('Enabling rate limiting', { requestsPerMinute }); app.use('/mcp', createApiKeyRateLimiter({ maxRequests: requestsPerMinute, windowMs: 60 * 1000, // 1 minute message: 'Too many requests from this API key, please try again later.' })); console.log("Rate limiting middleware added"); } // Handle POST requests for client-to-server communication console.log("Setting up POST handler for /mcp..."); app.post('/mcp', async (req, res) => { try { console.log("Received POST request to /mcp"); logger.info('Received POST MCP request', { contentLength: req.headers['content-length'], sessionId: req.headers['mcp-session-id'] || 'new-session' }); // Extract API key from Authorization header const apiKey = extractApiKey(req); if (apiKey) { console.log("Found API key in Authorization header"); logger.debug('Found API key in Authorization header'); } // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; console.log(`Using existing transport for session: ${sessionId}`); logger.debug(`Using existing transport for session: ${sessionId}`); // Update API key if provided if (apiKey) { apiKeyContext.setApiKey(sessionId, apiKey); } } else { // Create new transport console.log("Creating new transport"); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { // Store the transport by session ID transports[sessionId] = transport; console.log(`Initialized new session: ${sessionId}`); logger.info(`Initialized new session: ${sessionId}`); // Store API key in the context if provided if (apiKey) { apiKeyContext.setApiKey(sessionId, apiKey); } // Update session metrics if enabled if (enableMetrics) { metricsRegistry.activeSessions.inc(); } }, // No timeout option is available for StreamableHTTPServerTransport // We'll rely on circuit breaker timeouts instead }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { console.log(`Closing session: ${transport.sessionId}`); logger.info(`Closing session: ${transport.sessionId}`); apiKeyContext.removeApiKey(transport.sessionId); delete transports[transport.sessionId]; // Update session metrics if enabled if (enableMetrics) { metricsRegistry.activeSessions.dec(); } } }; // Connect to the MCP server console.log("Connecting transport to MCP server..."); await server.connect(transport); console.log("Transport connected to MCP server"); logger.info('Connected transport to MCP server'); } // Handle the request console.log("Handling MCP request..."); await transport.handleRequest(req, res, req.body); console.log("MCP request handled successfully"); } catch (error) { console.error("Error handling MCP request:", error); logger.error('Error handling MCP request:', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: error instanceof Error ? error.message : 'Internal server error', }, id: null, }); } } }); console.log("POST handler for /mcp set up successfully"); // Reusable handler for GET and DELETE requests console.log("Setting up session request handler..."); const handleSessionRequest = async (req, res) => { try { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { console.warn(`Invalid or missing session ID: ${sessionId}`); logger.warn(`Invalid or missing session ID: ${sessionId}`); res.status(400).send('Invalid or missing session ID'); return; } // Extract API key from Authorization header const apiKey = extractApiKey(req); if (apiKey) { console.log("Found API key in Authorization header"); logger.debug('Found API key in Authorization header'); apiKeyContext.setApiKey(sessionId, apiKey); } console.log(`Processing ${req.method} request for session: ${sessionId}`); logger.info(`Processing ${req.method} request for session: ${sessionId}`); const transport = transports[sessionId]; await transport.handleRequest(req, res); } catch (error) { console.error("Error handling session request:", error); logger.error('Error handling session request:', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); if (!res.headersSent) { res.status(500).send('Internal server error'); } } }; console.log("Session request handler created"); // Handle GET requests for server-to-client notifications via SSE console.log("Setting up GET handler for /mcp..."); app.get('/mcp', handleSessionRequest); console.log("GET handler for /mcp set up successfully"); // Handle DELETE requests for session termination console.log("Setting up DELETE handler for /mcp..."); app.delete('/mcp', handleSessionRequest); console.log("DELETE handler for /mcp set up successfully"); // Health check endpoint console.log("Setting up health check endpoint..."); app.get('/health', (req, res) => { console.log("Received request to /health"); // Always respond with 200 OK for health checks // This is critical for the proxy to work correctly try { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString(), version: '0.1.0', name: 'Digital Samba MCP Server', port: port, apiUrl: apiUrl, environment: process.env.NODE_ENV || 'development' }); console.log("Health check responded successfully"); } catch (error) { console.error("Error in health check response:", error); // Even if there's an error, still try to send a 200 response if (!res.headersSent) { res.status(200).send('Health check OK'); } } }); console.log("Health check endpoint set up successfully"); // Secondary health check endpoint on root path // This provides a fallback if '/health' is not accessible app.get('/', (req, res) => { console.log("Received request to root path"); res.status(200).send('Digital Samba MCP Server is running'); }); // System health status endpoint console.log("Setting up system health endpoint..."); app.get('/health/system', (req, res) => { console.log("Received request to /health/system"); // Get graceful degradation health status const overallHealth = gracefulDegradation.getOverallHealth(); const componentHealth = gracefulDegradation.getComponentHealth(); // Format component health for response const formattedComponents = componentHealth.map(component => ({ name: component.name, status: component.status, lastCheck: component.lastCheck, errorCount: component.errorCount, message: component.message })); res.status(200).json({ status: overallHealth === ServiceHealthStatus.HEALTHY ? 'ok' : overallHealth === ServiceHealthStatus.PARTIALLY_DEGRADED ? 'degraded' : overallHealth === ServiceHealthStatus.SEVERELY_DEGRADED ? 'critical' : 'unavailable', timestamp: new Date().toISOString(), version: '0.1.0', name: 'Digital Samba MCP Server', degradation: { overall: overallHealth, components: formattedComponents }, features: { cache: options?.enableCache ?? (process.env.ENABLE_CACHE === 'true'), circuitBreaker: options?.enableCircuitBreaker ?? (process.env.ENABLE_CIRCUIT_BREAKER === 'true'), gracefulDegradation: options?.enableGracefulDegradation ?? (process.env.ENABLE_GRACEFUL_DEGRADATION === 'true'), connectionManagement: options?.enableConnectionManagement ?? (process.env.ENABLE_CONNECTION_MANAGEMENT === 'true'), tokenManagement: options?.enableTokenManagement ?? (process.env.ENABLE_TOKEN_MANAGEMENT === 'true'), resourceOptimization: options?.enableResourceOptimization ?? (process.env.ENABLE_RESOURCE_OPTIMIZATION === 'true'), metrics: options?.enableMetrics ?? (process.env.ENABLE_METRICS === 'true') } }); }); console.log("System health endpoint set up successfully"); // Get the webhook service instance console.log("Setting up webhook service..."); const webhookService = new WebhookService(server, { secret: process.env.WEBHOOK_SECRET, endpoint: webhookEndpoint }); // Register webhook endpoint console.log("Registering webhook endpoint..."); webhookService.registerWebhookEndpoint(app); console.log("Webhook endpoint registered successfully"); // Start the server console.log("Creating HTTP server..."); const httpServer = createHttpServer(app); console.log("HTTP server created"); console.log(`Starting server on port ${port}...`); httpServer.listen(port, () => { console.log(`Digital Samba MCP Server running on port ${port}`); logger.info(`Digital Samba MCP Server running on port ${port}`); logger.info(`Digital Samba API URL: ${apiUrl}`); logger.info(`Webhook endpoint: ${publicUrl}${webhookEndpoint}`); if (enableMetrics) { logger.info(`Metrics endpoint: ${publicUrl}${metricsEndpoint}`); } }); console.log("Server listen call completed"); // Handle graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); logger.info('SIGTERM received, shutting down gracefully'); httpServer.close(() => { logger.info('HTTP server closed'); }); // Close all transport connections Object.keys(transports).forEach(sessionId => { logger.info(`Closing transport for session: ${sessionId}`); transports[sessionId].close(); }); process.exit(0); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); logger.info('SIGINT received, shutting down gracefully'); httpServer.close(() => { logger.info('HTTP server closed'); }); // Close all transport connections Object.keys(transports).forEach(sessionId => { logger.info(`Closing transport for session: ${sessionId}`); transports[sessionId].close(); }); process.exit(0); }); console.log("Server startup completed successfully"); return httpServer; } catch (error) { console.error("Error in startServer function:", error); logger.error("Failed to start server:", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); throw error; } } // Export transport functions for unified API export { runStdioServer, validateStdioConfig } from './transports/stdio-transport.js'; export { runHttpServer, createHttpServerConfig, validateHttpConfig } from './transports/http-transport.js'; // If this file is executed directly or via npm run dev, start the server // Only start if the file is executed directly, not when imported // The module initialization check had to be rewritten to work with tsx const isMainModule = () => { // In a more straightforward environment, we would check: // import.meta.url === `file://${process.argv[1]}` // But with tsx and different environments, we need to be more flexible // Allow explicit opt-out via environment variable if (process.env.MCP_DISABLE_AUTO_START === 'true') { console.log("Auto-start disabled by MCP_DISABLE_AUTO_START environment variable"); return false; } // Check if this is a test environment if (process.env.NODE_ENV === 'test') { console.log("Not starting server in test environment"); return false; } // Allow explicit opt-in via environment variable if (process.env.MCP_FORCE_START === 'true') { console.log("Auto-start forced by MCP_FORCE_START environment variable"); return true; } return true; // Default to starting the server }; if (isMainModule()) { console.log("Starting server initialization..."); try { console.log("Creating server instance..."); const serverConfig = createServer(); console.log("Server created:", serverConfig); console.log("Starting server with config:", { port: serverConfig.port, apiUrl: serverConfig.apiUrl, webhookEndpoint: serverConfig.webhookEndpoint, publicUrl: serverConfig.publicUrl, cacheEnabled: !!serverConfig.cache }); const server = startServer(); console.log(`Server started successfully`); } catch (error) { console.error('Failed to start server:', error); logger.error('Failed to start server:', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); process.exit(1); } } //# sourceMappingURL=index.js.map