UNPKG

mcp-web-ui

Version:

Ultra-lightweight vanilla JavaScript framework for MCP servers - Zero dependencies, perfect security, 2-3KB bundle size

863 lines 40.2 kB
import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { WebSocketServer } from 'ws'; import { MongoClient } from 'mongodb'; import { TokenRegistry } from './TokenRegistry.js'; import fs from 'fs'; /** * Gateway Proxy Server for ephemeral MCP Web UIs * * Provides a single stable endpoint that proxies requests to ephemeral backends * based on secure tokens. Supports HTTP, WebSocket, and SSE connections. * * URL Pattern: /mcp/:token/* * - Validates token against MongoDB registry * - Routes to appropriate ephemeral backend * - Supports real-time bidirectional communication */ export class GatewayProxyServer { app; server; wss; mongoClient; tokenRegistry; config; constructor(config) { this.config = { host: '0.0.0.0', mongoDbName: 'mcp_webui', corsOrigins: ['*'], proxyPrefix: '/mcp', enableLogging: true, ...config }; this.app = express(); this.setupMiddleware(); this.setupRoutes(); } /** * Setup Express middleware */ setupMiddleware() { // Enable trust proxy for proper IP forwarding this.app.set('trust proxy', true); // CORS middleware this.app.use((req, res, next) => { const origin = req.headers.origin; if (this.config.corsOrigins.includes('*') || (origin && this.config.corsOrigins.includes(origin))) { res.header('Access-Control-Allow-Origin', origin || '*'); } res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control'); res.header('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // Request logging (filter out polling noise) if (this.config.enableLogging) { this.app.use((req, res, next) => { // Skip logging polling requests to /api/data const isPollingRequest = req.method === 'GET' && req.path?.includes('/api/data'); if (!isPollingRequest) { this.log('debug', `${req.method} ${req.path}`, { ip: req.ip, userAgent: req.get('User-Agent') }); } next(); }); } // JSON parsing - but skip for proxy routes to avoid consuming request body this.app.use((req, res, next) => { // Skip body parsing for proxy routes that need to forward the raw body const isProxyRoute = req.path.startsWith(this.config.proxyPrefix); if (isProxyRoute) { return next(); } // Apply body parsing for gateway management routes only express.json()(req, res, next); }); this.app.use((req, res, next) => { const isProxyRoute = req.path.startsWith(this.config.proxyPrefix); if (isProxyRoute) { return next(); } express.urlencoded({ extended: true })(req, res, next); }); } /** * Setup Express routes */ setupRoutes() { // Health check endpoint this.app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // Session creation endpoint for MCP servers this.app.post('/create-session', async (req, res) => { if (!this.tokenRegistry) { return res.status(503).json({ error: 'Token registry not available' }); } try { const { userId, serverName, serverType, backend, ttlMinutes = 30 } = req.body; if (!userId || !serverName || !backend) { return res.status(400).json({ error: 'Missing required fields', required: ['userId', 'serverName', 'backend'] }); } // Check for existing active session first using composite key const existingSession = await this.tokenRegistry.findActiveSession(userId, serverName, serverType); let session; if (existingSession) { const sessionKey = `${userId}:${serverName}:${serverType || 'mcp-webui'}`; this.log('info', `Reusing existing session for composite key ${sessionKey}`); session = existingSession; } else { // Create a new session with JWT token session = await this.tokenRegistry.createSession({ userId, serverName, serverType, backend, ttlMinutes }); } this.log('info', `Created session for user ${userId}, server ${serverName}`); res.json({ success: true, token: session.token, expiresAt: session.expiresAt }); } catch (error) { this.log('error', 'Failed to create session:', error); res.status(500).json({ error: 'Failed to create session' }); } }); // Server registration endpoint for MCP servers this.app.post('/register-server', async (req, res) => { if (!this.tokenRegistry) { return res.status(503).json({ error: 'Token registry not available' }); } try { const { serverName, backend, metadata = {} } = req.body; if (!serverName || !backend) { return res.status(400).json({ error: 'Missing required fields', required: ['serverName', 'backend'] }); } // Validate backend configuration if (backend.type === 'tcp' && (!backend.host || !backend.port)) { return res.status(400).json({ error: 'TCP backend requires host and port' }); } // Register the server await this.tokenRegistry.registerServer(serverName, backend, metadata); this.log('info', `Registered MCP server: ${serverName}`, { backend: backend.type === 'tcp' ? `${backend.host}:${backend.port}` : backend.socketPath, metadata }); res.json({ success: true, serverName, registeredAt: new Date().toISOString() }); } catch (error) { this.log('error', 'Failed to register server:', error); res.status(500).json({ error: 'Failed to register server' }); } }); // Server discovery endpoint - get registered server info this.app.get('/discover-server/:serverName', async (req, res) => { if (!this.tokenRegistry) { return res.status(503).json({ error: 'Token registry not available' }); } try { const { serverName } = req.params; const serverInfo = await this.tokenRegistry.getRegisteredServer(serverName); if (!serverInfo) { return res.status(404).json({ error: 'Server not found', serverName }); } res.json({ success: true, serverName, backend: serverInfo.backend, metadata: serverInfo.metadata, registeredAt: serverInfo.registeredAt }); } catch (error) { this.log('error', 'Failed to discover server:', error); res.status(500).json({ error: 'Failed to discover server' }); } }); // Proxy stats endpoint (optional admin endpoint) this.app.get('/stats', async (req, res) => { if (!this.tokenRegistry) { return res.status(503).json({ error: 'Token registry not available' }); } try { const stats = await this.tokenRegistry.getStats(); res.json({ ...stats, server: { uptime: process.uptime(), timestamp: new Date().toISOString() } }); } catch (error) { this.log('error', 'Failed to get stats:', error); res.status(500).json({ error: 'Failed to get stats' }); } }); // Handle API routes specifically (highest priority) this.app.use(`${this.config.proxyPrefix}/:token/api/*`, this.createTokenValidationMiddleware(), this.createApiProxyMiddleware()); // Handle static resources for MCP Web UI framework this.app.use(`${this.config.proxyPrefix}/:token/static/:filename`, this.createTokenValidationMiddleware(), this.createStaticProxyMiddleware()); // Main proxy route for HTML content: /mcp/:token/* this.app.use(`${this.config.proxyPrefix}/:token`, this.createTokenValidationMiddleware(), this.createProxyMiddleware()); // Catch-all for invalid routes this.app.use('*', (req, res) => { res.status(404).json({ error: 'Not Found', message: `Route ${req.originalUrl} not found`, hint: `Use ${this.config.proxyPrefix}/:token/... for proxied requests` }); }); } /** * Token validation middleware */ createTokenValidationMiddleware() { return async (req, res, next) => { const token = req.params.token; if (!token) { return res.status(400).json({ error: 'Token required' }); } if (!this.tokenRegistry) { return res.status(503).json({ error: 'Token registry not available' }); } try { const session = await this.tokenRegistry.validateToken(token); if (!session) { return res.status(401).json({ error: 'Invalid or expired token', hint: 'Request a new web UI session' }); } // Attach session info to request for proxy middleware req.mcpSession = session; this.log('debug', `Valid token for user ${session.userId}, server ${session.serverName}`); next(); } catch (error) { this.log('error', 'Token validation error:', error); res.status(500).json({ error: 'Token validation failed' }); } }; } /** * Create static resource proxy middleware */ createStaticProxyMiddleware() { return createProxyMiddleware({ target: 'http://placeholder', // Will be overridden by router changeOrigin: true, ws: false, // No WebSocket for static files // Dynamic target resolution router: (req) => { const session = req.mcpSession; if (session.backend.type === 'tcp') { // Always use HTTP for internal backend communication // The gateway handles HTTPS termination, backends run HTTP const protocol = 'http'; return `http://${session.backend.host}:${session.backend.port}`; } else if (session.backend.type === 'unix') { return `http://unix:${session.backend.socketPath}:`; } throw new Error(`Unsupported backend type: ${session.backend.type}`); }, // Path rewriting - remove /mcp/:token prefix but keep /static/:filename pathRewrite: (path, req) => { const token = req.params?.token || ''; const prefix = `${this.config.proxyPrefix}/${token}`; return path.replace(prefix, '') || '/'; }, // Logging logLevel: this.config.enableLogging ? 'info' : 'silent', logProvider: () => ({ log: (message) => this.log('debug', `[StaticProxy] ${message}`), debug: (message) => this.log('debug', `[StaticProxy] ${message}`), info: (message) => this.log('info', `[StaticProxy] ${message}`), warn: (message) => this.log('warn', `[StaticProxy] ${message}`), error: (message) => this.log('error', `[StaticProxy] ${message}`) }), // Error handling onError: (err, req, res) => { this.log('error', 'Static proxy error:', err); if (!res.headersSent) { res.status(502).json({ error: 'Bad Gateway', message: 'Failed to connect to backend service for static resource', hint: 'The ephemeral service may have stopped or be unreachable' }); } }, // Proxy events for debugging onProxyReq: (proxyReq, req, res) => { const session = req.mcpSession; this.log('debug', `Proxying static ${req.method} ${req.path} to ${session.backend.type}`, { userId: session.userId, serverName: session.serverName, target: session.backend.type === 'tcp' ? `${session.backend.host}:${session.backend.port}` : session.backend.socketPath }); // Forward the JWT token to the backend server const token = req.params?.token; if (token) { // Add token as query parameter (preferred by GenericUIServer) const separator = proxyReq.path.includes('?') ? '&' : '?'; proxyReq.path = `${proxyReq.path}${separator}token=${token}`; // Also add as Authorization header for compatibility proxyReq.setHeader('Authorization', `Bearer ${token}`); this.log('debug', `Added token to static proxy request: ${token.substring(0, 20)}...`); } } }); } /** * Create API-specific proxy middleware */ createApiProxyMiddleware() { return createProxyMiddleware({ target: 'http://placeholder', // Will be overridden by router changeOrigin: true, ws: false, // No WebSocket for API calls timeout: 30000, // 30 second timeout proxyTimeout: 30000, // 30 second proxy timeout // Dynamic target resolution router: (req) => { const session = req.mcpSession; if (session.backend.type === 'gateway') { throw new Error('Gateway session - no backend server available'); } else if (session.backend.type === 'tcp') { if (!session.backend.host || !session.backend.port) { throw new Error('TCP backend missing host or port'); } // Always use HTTP for internal backend communication // The gateway handles HTTPS termination, backends run HTTP const protocol = 'http'; return `http://${session.backend.host}:${session.backend.port}`; } else if (session.backend.type === 'unix') { if (!session.backend.socketPath) { throw new Error('Unix backend missing socket path'); } return `http://unix:${session.backend.socketPath}:`; } throw new Error(`Unsupported backend type: ${session.backend.type}`); }, // Path rewriting - remove /mcp/:token prefix pathRewrite: (path, req) => { const token = req.params?.token || ''; const prefix = `${this.config.proxyPrefix}/${token}`; return path.replace(prefix, '') || '/'; }, // Logging logLevel: this.config.enableLogging ? 'info' : 'silent', logProvider: () => ({ log: (message) => this.log('debug', `[ApiProxy] ${message}`), debug: (message) => this.log('debug', `[ApiProxy] ${message}`), info: (message) => this.log('info', `[ApiProxy] ${message}`), warn: (message) => this.log('warn', `[ApiProxy] ${message}`), error: (message) => this.log('error', `[ApiProxy] ${message}`) }), // Error handling onError: (err, req, res) => { this.log('error', 'API proxy error:', err); if (!res.headersSent) { res.status(502).json({ error: 'Bad Gateway', message: 'Failed to connect to backend service for API call', hint: 'The ephemeral service may have stopped or be unreachable', path: req.path, method: req.method }); } }, // NO HTML rewriting for API responses - this is the key fix onProxyRes: (proxyRes, req, res) => { const session = req.mcpSession; const token = req.params?.token; // Enhanced logging for API responses (skip polling requests) const isPollingRequest = req.method === 'GET' && (req.url?.includes('/api/data') || req.path?.includes('/api/data')); if (!isPollingRequest) { this.log('info', `API response received`, { statusCode: proxyRes.statusCode, contentType: proxyRes.headers['content-type'], contentLength: proxyRes.headers['content-length'], path: req.path, method: req.method, userId: session.userId, serverName: session.serverName }); } // For API calls, we should NOT intercept HTML responses // Instead, let them pass through as-is to help debug backend issues const contentType = proxyRes.headers['content-type'] || ''; if (contentType.includes('text/html')) { this.log('warn', `Backend returned HTML for API call - passing through for debugging`, { path: req.path, method: req.method, contentType, statusCode: proxyRes.statusCode, hint: 'Check if the backend API endpoint exists and is working correctly' }); } // Log response body for debugging (skip polling requests) if (this.config.enableLogging && !isPollingRequest) { let responseBody = ''; proxyRes.on('data', (chunk) => { responseBody += chunk.toString(); }); proxyRes.on('end', () => { this.log('debug', `API response body`, { contentType, totalLength: responseBody.length, fullResponse: responseBody.length <= 500 ? responseBody : responseBody.substring(0, 500) + '...', isJSON: this.isValidJSON(responseBody) }); }); } }, // Proxy events for debugging onProxyReq: (proxyReq, req, res) => { const session = req.mcpSession; // Skip logging for polling requests to reduce noise const isPollingRequest = req.method === 'GET' && (req.url?.includes('/api/data') || req.path?.includes('/api/data')); if (!isPollingRequest) { this.log('info', `Proxying API ${req.method} ${req.path || req.url} to ${session.backend.type}`, { userId: session.userId, serverName: session.serverName, target: session.backend.type === 'tcp' ? `${session.backend.host}:${session.backend.port}` : session.backend.socketPath }); // Log the exact proxy request details this.log('debug', `API proxy request details`, { originalPath: req.path || req.url, proxyPath: proxyReq.path, targetHost: proxyReq.getHeader('host'), targetProtocol: proxyReq.protocol, method: proxyReq.method, contentType: proxyReq.getHeader('content-type'), contentLength: proxyReq.getHeader('content-length'), hasBody: req.method === 'POST' && req.body ? 'yes' : 'no', bodyPreview: req.method === 'POST' && req.body ? JSON.stringify(req.body).substring(0, 200) : 'none' }); } // Forward the JWT token to the backend server // Extract token from URL pattern /mcp/:token/api/... const token = session.token; // Use token from validated session // Debug: Log token extraction details (skip for polling requests) if (!isPollingRequest) { this.log('debug', `Token extraction debug`, { hasParams: !!req.params, paramsKeys: req.params ? Object.keys(req.params) : [], sessionToken: token ? `${token.substring(0, 20)}...` : 'null', originalUrl: req.url, pathInfo: req.path, sessionUserId: session.userId, sessionServerName: session.serverName }); } if (token) { // Add token as query parameter (preferred by GenericUIServer) const separator = proxyReq.path.includes('?') ? '&' : '?'; const oldPath = proxyReq.path; proxyReq.path = `${proxyReq.path}${separator}token=${token}`; // Also add as Authorization header for compatibility proxyReq.setHeader('Authorization', `Bearer ${token}`); // Debug: Log token addition (skip for polling requests) if (!isPollingRequest) { this.log('debug', `Token added to proxy request`, { originalPath: oldPath, newPath: proxyReq.path, tokenPreview: `${token.substring(0, 20)}...` }); } } else { this.log('error', `No token found in session - backend request will fail!`); } } }); } /** * Create dynamic proxy middleware */ createProxyMiddleware() { return createProxyMiddleware({ target: 'http://placeholder', // Will be overridden by router changeOrigin: true, ws: true, // Enable WebSocket proxying timeout: 30000, // 30 second timeout proxyTimeout: 30000, // 30 second proxy timeout // Dynamic target resolution router: (req) => { const session = req.mcpSession; if (session.backend.type === 'gateway') { // Gateway-managed sessions don't have backend servers // For now, we'll redirect to a placeholder or serve a message // This could be enhanced to serve static content directly throw new Error('Gateway session - no backend server available'); } else if (session.backend.type === 'tcp') { if (!session.backend.host || !session.backend.port) { throw new Error('TCP backend missing host or port'); } // Always use HTTP for internal backend communication // The gateway handles HTTPS termination, backends run HTTP const protocol = 'http'; return `http://${session.backend.host}:${session.backend.port}`; } else if (session.backend.type === 'unix') { // For UNIX sockets, we need special handling if (!session.backend.socketPath) { throw new Error('Unix backend missing socket path'); } return `http://unix:${session.backend.socketPath}:`; } throw new Error(`Unsupported backend type: ${session.backend.type}`); }, // Path rewriting - remove /mcp/:token prefix pathRewrite: (path, req) => { const token = req.params?.token || ''; const prefix = `${this.config.proxyPrefix}/${token}`; return path.replace(prefix, '') || '/'; }, // Logging logLevel: this.config.enableLogging ? 'info' : 'silent', logProvider: () => ({ log: (message) => this.log('debug', `[Proxy] ${message}`), debug: (message) => this.log('debug', `[Proxy] ${message}`), info: (message) => this.log('info', `[Proxy] ${message}`), warn: (message) => this.log('warn', `[Proxy] ${message}`), error: (message) => this.log('error', `[Proxy] ${message}`) }), // Error handling onError: (err, req, res) => { this.log('error', 'Proxy error:', err); if (!res.headersSent) { res.status(502).json({ error: 'Bad Gateway', message: 'Failed to connect to backend service', hint: 'The ephemeral service may have stopped or be unreachable' }); } }, // Response modification for HTML content onProxyRes: (proxyRes, req, res) => { const session = req.mcpSession; const token = req.params?.token; // Debug logging for all responses this.log('debug', `Backend response received`, { statusCode: proxyRes.statusCode, contentType: proxyRes.headers['content-type'], contentLength: proxyRes.headers['content-length'], path: req.path, method: req.method }); // Only modify HTML responses const contentType = proxyRes.headers['content-type'] || ''; if (contentType.includes('text/html') && token) { // Check if this is an API call that should return JSON const isApiCall = req.path.includes('/api/'); if (isApiCall) { this.log('warn', `Backend returned HTML for API call - this indicates a backend issue`, { path: req.path, method: req.method, contentType, statusCode: proxyRes.statusCode, hint: 'The backend server may have crashed or be serving an error page' }); } // Remove content-length to allow modification delete proxyRes.headers['content-length']; // Buffer the response let body = ''; proxyRes.on('data', (chunk) => { body += chunk.toString(); }); proxyRes.on('end', () => { // Rewrite static resource URLs - replace both href and src attributes let rewrittenBody = body .replace(/href=["']\/static\//g, `href="/mcp/${token}/static/`) .replace(/src=["']\/static\//g, `src="/mcp/${token}/static/`); const urlMatches = rewrittenBody.match(/\/mcp\/[^\/]+\/static\//g) || []; this.log('debug', `Rewrote ${body.length} bytes of HTML, ${urlMatches.length} static URLs updated`); // Send the modified response res.end(rewrittenBody); }); // Don't let the proxy send the original response proxyRes.removeAllListeners('data'); proxyRes.removeAllListeners('end'); } else { // For non-HTML responses, log the first chunk to help debug if (this.config.enableLogging) { let firstChunk = true; let responseBody = ''; proxyRes.on('data', (chunk) => { responseBody += chunk.toString(); if (firstChunk) { const chunkStr = chunk.toString(); this.log('debug', `Non-HTML response first chunk`, { contentType, chunkLength: chunkStr.length, chunkPreview: chunkStr.substring(0, 200) + (chunkStr.length > 200 ? '...' : ''), isJSON: this.isValidJSON(chunkStr) }); firstChunk = false; } }); // Log the complete response body when the response ends proxyRes.on('end', () => { this.log('debug', `Complete non-HTML response received`, { contentType, totalLength: responseBody.length, fullResponse: responseBody.length <= 1000 ? responseBody : responseBody.substring(0, 1000) + '...', isCompleteJSON: this.isValidJSON(responseBody) }); }); } } }, // Proxy events for debugging onProxyReq: (proxyReq, req, res) => { const session = req.mcpSession; this.log('debug', `Proxying ${req.method} ${req.path} to ${session.backend.type}`, { userId: session.userId, serverName: session.serverName, target: session.backend.type === 'tcp' ? `${session.backend.host}:${session.backend.port}` : session.backend.socketPath }); // Log the exact proxy request details this.log('debug', `Proxy request details`, { originalPath: req.path, proxyPath: proxyReq.path, targetHost: proxyReq.getHeader('host'), targetProtocol: proxyReq.protocol, method: proxyReq.method }); // Forward the JWT token to the backend server // The backend expects either query.token or Authorization header const token = req.params?.token; if (token) { // Add token as query parameter (preferred by GenericUIServer) const separator = proxyReq.path.includes('?') ? '&' : '?'; proxyReq.path = `${proxyReq.path}${separator}token=${token}`; // Also add as Authorization header for compatibility proxyReq.setHeader('Authorization', `Bearer ${token}`); this.log('debug', `Added token to proxy request: ${token.substring(0, 20)}...`); } }, // WebSocket upgrade handling onProxyReqWs: (proxyReq, req, socket, options, head) => { const session = req.mcpSession; this.log('info', `WebSocket upgrade for user ${session.userId}`, { serverName: session.serverName, target: session.backend.type === 'tcp' ? `${session.backend.host}:${session.backend.port}` : session.backend.socketPath }); } }); } /** * Initialize MongoDB connection and token registry */ async initializeDatabase() { try { this.mongoClient = new MongoClient(this.config.mongoUrl); await this.mongoClient.connect(); const db = this.mongoClient.db(this.config.mongoDbName); this.tokenRegistry = new TokenRegistry(db, { jwtSecret: this.config.jwtSecret, logger: this.config.logger || this.log.bind(this) }); this.log('info', `Connected to MongoDB: ${this.config.mongoDbName}`); } catch (error) { this.log('error', 'Failed to initialize database:', error); throw error; } } /** * Start the gateway proxy server */ async start() { try { // Initialize database first await this.initializeDatabase(); // Create HTTP/HTTPS server if (this.config.ssl) { const https = await import('https'); const cert = fs.readFileSync(this.config.ssl.cert); const key = fs.readFileSync(this.config.ssl.key); this.server = https.createServer({ cert, key }, this.app); this.log('info', 'Created HTTPS server'); } else { const http = await import('http'); this.server = http.createServer(this.app); this.log('info', 'Created HTTP server'); } // Setup WebSocket server for WebSocket proxying this.wss = new WebSocketServer({ server: this.server }); // Start listening await new Promise((resolve, reject) => { this.server.listen(this.config.port, this.config.host, () => { resolve(); }); this.server.on('error', reject); }); const protocol = this.config.ssl ? 'https' : 'http'; this.log('info', `Gateway Proxy Server started on ${protocol}://${this.config.host}:${this.config.port}`); this.log('info', `Proxy endpoint: ${protocol}://${this.config.host}:${this.config.port}${this.config.proxyPrefix}/:token/...`); } catch (error) { this.log('error', 'Failed to start server:', error); throw error; } } /** * Stop the gateway proxy server */ async stop() { this.log('info', 'Stopping Gateway Proxy Server...'); try { // Close WebSocket server with timeout if (this.wss) { this.log('debug', 'Closing WebSocket server...'); await Promise.race([ new Promise((resolve) => { this.wss.close(() => resolve()); }), new Promise((resolve) => setTimeout(resolve, 5000)) // 5 second timeout ]); this.wss = undefined; this.log('debug', 'WebSocket server closed'); } // Close HTTP server with timeout if (this.server) { this.log('debug', 'Closing HTTP server...'); await Promise.race([ new Promise((resolve) => { this.server.close(() => resolve()); }), new Promise((resolve) => setTimeout(resolve, 10000)) // 10 second timeout ]); this.server = undefined; this.log('debug', 'HTTP server closed'); } // Close MongoDB connection with timeout if (this.mongoClient) { this.log('debug', 'Closing MongoDB connection...'); await Promise.race([ this.mongoClient.close(), new Promise((resolve) => setTimeout(resolve, 5000)) // 5 second timeout ]); this.mongoClient = undefined; this.tokenRegistry = undefined; this.log('debug', 'MongoDB connection closed'); } this.log('info', 'Gateway Proxy Server stopped successfully'); } catch (error) { this.log('error', 'Error during shutdown:', error); // Force cleanup even if there were errors this.wss = undefined; this.server = undefined; this.mongoClient = undefined; this.tokenRegistry = undefined; throw error; } } /** * Get the token registry instance */ getTokenRegistry() { return this.tokenRegistry; } /** * Get server stats */ async getStats() { if (!this.tokenRegistry) { return { error: 'Token registry not initialized' }; } const registryStats = await this.tokenRegistry.getStats(); return { ...registryStats, server: { uptime: process.uptime(), port: this.config.port, host: this.config.host, ssl: !!this.config.ssl, proxyPrefix: this.config.proxyPrefix } }; } /** * Simple logging utility */ log(level, message, data) { if (this.config.logger) { this.config.logger(level, message, data); } else if (this.config.enableLogging) { const timestamp = new Date().toISOString(); console.error(`[${timestamp}][${level.toUpperCase()}][GatewayProxy] ${message}`); if (data) { console.error(JSON.stringify(data, null, 2)); } } } /** * Helper to check if a string is valid JSON. * This is a simplified check and might not be perfect for all JSON structures. */ isValidJSON(str) { try { JSON.parse(str); return true; } catch (e) { return false; } } } //# sourceMappingURL=GatewayProxyServer.js.map