Skip to main content
Glama
https-transport.ts•15.5 kB
/** * HTTPS/HTTP Transport for MCP Server * Implements the MCP protocol over HTTPS or HTTP with OAuth 2.1 authentication * Supports both SSL mode (for direct access) and HTTP mode (for ALB/proxy deployments) */ import https from 'https'; import http from 'http'; import express, { Request, Response, NextFunction } from 'express'; import fs from 'fs'; import path from 'path'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { OAuthSessionManager, Session } from './oauth-session-manager.js'; interface AuthenticatedRequest extends Request { session?: Session; } export class HttpsTransport { private app: express.Application; private server: https.Server | http.Server | null = null; private sessionManager: OAuthSessionManager; private mcpServer: Server; private port: number; private host: string; private useSSL: boolean; constructor(mcpServer: Server, sessionManager: OAuthSessionManager) { this.app = express(); this.mcpServer = mcpServer; this.sessionManager = sessionManager; this.port = parseInt(process.env.MCP_HTTPS_PORT || '8443'); this.host = process.env.MCP_HOST || '127.0.0.1'; // Check if SSL should be used (default: true for backward compatibility) // Set MCP_USE_SSL=false to run in HTTP mode (for ALB deployments) this.useSSL = process.env.MCP_USE_SSL !== 'false'; this.setupMiddleware(); this.setupRoutes(); } /** * Setup Express middleware */ private setupMiddleware(): void { // Parse JSON bodies this.app.use(express.json({ limit: '10mb' })); // CORS headers for MCP clients this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.header('X-MCP-Version', '2025-06-18'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // Request logging this.app.use((req, res, next) => { console.error(`[HTTPS] ${req.method} ${req.path}`); next(); }); } /** * OAuth authentication middleware */ private authenticateOAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction): void => { const authHeader = req.headers.authorization; if (!authHeader) { res.status(401).json({ error: 'Unauthorized', message: 'Missing Authorization header', authUrl: this.getAuthUrl() }); return; } const session = this.sessionManager.validateBearerToken(authHeader); if (!session) { res.status(401).json({ error: 'Unauthorized', message: 'Invalid or expired token', authUrl: this.getAuthUrl() }); return; } req.session = session; next(); }; /** * Setup routes */ private setupRoutes(): void { // Health check endpoint (no auth required) this.app.get('/health', (req, res) => { res.json({ status: 'healthy', transport: 'https', sessions: this.sessionManager.getActiveSessionCount(), timestamp: new Date().toISOString() }); }); // OAuth endpoints this.app.get('/oauth/authorize', (req, res) => { // Serve the authentication page const authPagePath = path.join(__dirname, '..', 'auth-page.html'); if (fs.existsSync(authPagePath)) { res.sendFile(authPagePath); } else { // Return a simple authentication form if the file doesn't exist res.send(this.getSimpleAuthPage()); } }); this.app.post('/oauth/token', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'invalid_request', error_description: 'Missing username or password' }); } try { // Use the existing dual-auth system to authenticate const { UmbrellaDualAuth } = await import('./dual-auth.js'); const dualAuth = new UmbrellaDualAuth(process.env.UMBRELLA_API_BASE_URL || 'https://api.umbrellacost.io/api/v1'); const authResult = await dualAuth.authenticate({ username, password }); // Get auth headers which includes the token and api key const authHeaders = dualAuth.getAuthHeaders(); // Parse the API key to extract user, account, and division let userKey = ''; let accountKey = ''; let divisionId = 0; if (authHeaders.apikey) { const parts = authHeaders.apikey.split(':'); if (parts.length >= 2) { userKey = parts[0]; accountKey = parts[1]; if (parts.length >= 3) { divisionId = parseInt(parts[2]) || 0; } } } // Get user management info for realm const userManagementInfo = dualAuth.getUserManagementInfo(); const realm = userManagementInfo?.realm || 'default'; const isMSP = username.includes('+') && !username.includes('@'); // Create session const sessionResult = this.sessionManager.createSession( username, userKey, accountKey, divisionId, authHeaders.Authorization, realm, isMSP ); // Return OAuth 2.1 compliant response res.json({ access_token: sessionResult.bearerToken, token_type: 'Bearer', expires_in: 86400, // 24 hours in seconds scope: 'mcp:access', session_id: sessionResult.sessionId }); } catch (error) { console.error('[OAUTH] Authentication error:', error); res.status(500).json({ error: 'server_error', error_description: 'Internal authentication error' }); } }); this.app.post('/oauth/revoke', this.authenticateOAuth, (req: AuthenticatedRequest, res) => { if (req.session) { this.sessionManager.removeSession(req.session.sessionId); } res.json({ success: true }); }); // MCP JSON-RPC endpoint this.app.post('/mcp', this.authenticateOAuth, async (req: AuthenticatedRequest, res) => { try { const { session } = req; if (!session) { return res.status(401).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Unauthorized' }, id: req.body.id || null }); } // Set the session context for the MCP server const context = { username: session.username, userKey: session.userKey, accountKey: session.accountKey, divisionId: session.divisionId, token: session.token, realm: session.realm, isMSP: session.isMSP, transport: 'https' }; // Process the JSON-RPC request through the MCP server // This requires modifying the server to accept context const response = await this.handleMcpRequest(req.body, context); res.json(response); } catch (error) { console.error('[HTTPS] MCP request error:', error); res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error' }, id: req.body.id || null }); } }); // Session info endpoint this.app.get('/session', this.authenticateOAuth, (req: AuthenticatedRequest, res) => { const { session } = req; if (!session) { return res.status(401).json({ error: 'No session' }); } res.json({ sessionId: session.sessionId, username: session.username, expiresAt: session.expiresAt, isMSP: session.isMSP, realm: session.realm }); }); } /** * Handle MCP request with session context */ private async handleMcpRequest(request: any, context: any): Promise<any> { // Use the server's HTTPS request handler const server = this.mcpServer as any; if (server.handleHttpsRequest) { return await server.handleHttpsRequest(request, context); } else { console.error('[HTTPS] Server does not support HTTPS requests'); return { jsonrpc: '2.0', error: { code: -32603, message: 'Server does not support HTTPS requests' }, id: request.id }; } } /** * Get authentication URL */ private getAuthUrl(): string { const baseUrl = process.env.MCP_BASE_URL || `https://${this.host}:${this.port}`; return `${baseUrl}/oauth/authorize`; } /** * Get simple authentication page HTML */ private getSimpleAuthPage(): string { return `<!DOCTYPE html> <html> <head> <title>Umbrella MCP Authentication</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } .auth-container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; max-width: 400px; } h2 { color: #333; margin-bottom: 1.5rem; text-align: center; } .form-group { margin-bottom: 1rem; } label { display: block; margin-bottom: 0.5rem; color: #555; font-weight: 500; } input { width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; box-sizing: border-box; } input:focus { outline: none; border-color: #667eea; } button { width: 100%; padding: 0.75rem; background: #667eea; color: white; border: none; border-radius: 4px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: background 0.2s; } button:hover { background: #5a67d8; } .error { color: #e53e3e; margin-top: 1rem; text-align: center; } .success { color: #38a169; margin-top: 1rem; text-align: center; } </style> </head> <body> <div class="auth-container"> <h2>🔐 Umbrella MCP Authentication</h2> <form id="authForm"> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required placeholder="e.g., david+saola@umbrellacost.com"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required> </div> <button type="submit">Authenticate</button> </form> <div id="message"></div> </div> <script> document.getElementById('authForm').addEventListener('submit', async (e) => { e.preventDefault(); const messageEl = document.getElementById('message'); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const response = await fetch('/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (response.ok) { messageEl.className = 'success'; messageEl.textContent = 'Authentication successful! You can now use the MCP server.'; // Store token for the user if (data.access_token) { messageEl.innerHTML += '<br><br>Your bearer token:<br><code style="word-break: break-all; font-size: 0.8em;">' + data.access_token + '</code>'; } } else { messageEl.className = 'error'; messageEl.textContent = data.error_description || 'Authentication failed'; } } catch (error) { messageEl.className = 'error'; messageEl.textContent = 'Connection error: ' + error.message; } }); </script> </body> </html>`; } /** * Start HTTP/HTTPS server */ async start(): Promise<void> { if (this.useSSL) { // HTTPS mode - load SSL certificates const certPath = path.join(process.cwd(), 'certs', 'server.crt'); const keyPath = path.join(process.cwd(), 'certs', 'server.key'); // Check if certificates exist if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) { throw new Error('SSL certificates not found. Please run generate-ssl-certs.sh first or set MCP_USE_SSL=false for HTTP mode'); } const httpsOptions = { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) }; this.server = https.createServer(httpsOptions, this.app); return new Promise((resolve) => { this.server!.listen(this.port, this.host, () => { console.error(`[HTTPS] MCP server listening on https://${this.host}:${this.port}`); console.error(`[HTTPS] Authentication URL: https://${this.host}:${this.port}/oauth/authorize`); resolve(); }); }); } else { // HTTP mode - no SSL certificates needed (for ALB/proxy deployments) console.error('[HTTP] Running in HTTP mode (SSL termination expected at load balancer)'); this.server = http.createServer(this.app); return new Promise((resolve) => { this.server!.listen(this.port, this.host, () => { console.error(`[HTTP] MCP server listening on http://${this.host}:${this.port}`); console.error(`[HTTP] Authentication URL: http://${this.host}:${this.port}/oauth/authorize`); console.error(`[HTTP] ⚠️ SSL termination should be handled by your load balancer/proxy`); resolve(); }); }); } } /** * Stop HTTP/HTTPS server */ async stop(): Promise<void> { if (this.server) { return new Promise((resolve) => { this.server!.close(() => { console.error(`[${this.useSSL ? 'HTTPS' : 'HTTP'}] Server stopped`); resolve(); }); }); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/daviddraiumbrella/invoice-monitoring'

If you have feedback or need assistance with the MCP directory API, please join our Discord server