/**
* 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();
});
});
}
}
}