import express, { Request, Response } from 'express';
import https from 'https';
import http from 'http';
import fs from 'fs';
import path from 'path';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { ConfigLoader } from '../config/config-loader.js';
import { OAuthServer } from '../auth/oauth-server.js';
import { TokenManager } from '../auth/token-manager.js';
import { UmbrellaMcpServer } from '../server.js';
import { debugLog } from '../utils/debug-logger.js';
export class HttpsTransport {
private config = ConfigLoader.getInstance();
private oauthServer: OAuthServer;
private tokenManager: TokenManager;
private mcpServer: UmbrellaMcpServer;
private app: express.Application;
constructor(mcpServer: UmbrellaMcpServer) {
this.mcpServer = mcpServer;
this.oauthServer = new OAuthServer();
this.tokenManager = new TokenManager();
this.app = this.oauthServer.getApp();
this.setupStaticFiles();
this.setupMcpEndpoints();
}
private setupStaticFiles(): void {
// Serve static assets from public directory
const publicPath = path.join(process.cwd(), 'public');
this.app.use('/assets', express.static(path.join(publicPath, 'assets')));
}
private setupMcpEndpoints(): void {
// Protected MCP endpoint - HEAD request for capability check
this.app.head('/mcp', (req, res) => {
if (!req.header('authorization')) {
this.writeBearerChallenge(res);
return res.status(401).end();
}
return res.status(200).end();
});
// Unified MCP endpoint handler for both GET (SSE) and POST (JSON-RPC)
const mcpHandler = async (req: Request, res: Response) => {
const auth = req.header('authorization');
if (!auth) {
this.writeBearerChallenge(res);
if (req.method === 'POST') {
const issuer = this.config.getConfig().auth.issuer;
return res.status(401).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Authentication required. Please authorize via OAuth.',
data: {
authorization_url: new URL('authorize', issuer).toString(),
token_endpoint: new URL('oauth/token', issuer).toString()
}
},
id: req.body?.id ?? null
});
}
return res.status(401).end();
}
try {
// Verify token and extract Umbrella credentials
const token = auth.replace(/^Bearer\s+/i, '');
const { payload, umbrellaAuth } = await this.tokenManager.verifyToken(token);
// Check if token is expired
if (this.tokenManager.isTokenExpired(payload.exp)) {
this.writeBearerChallenge(res);
if (req.method === 'POST') {
return res.status(401).json({
jsonrpc: '2.0',
error: {
code: -32001,
message: 'Token expired. Please re-authenticate.'
},
id: req.body?.id ?? null
});
}
return res.status(401).end();
}
// Set Umbrella auth for the MCP server session
if (this.config.isDebugMode()) {
console.error('[HTTPS-TRANSPORT] Setting auth from token for:', payload.sub);
}
const authResult = await this.mcpServer.getSessionManager().setAuthFromToken({
Authorization: umbrellaAuth.Authorization,
userEmail: payload.sub,
clientId: payload.clientId,
userManagementInfo: umbrellaAuth.userManagementInfo // Pass the stored auth method from token
});
if (this.config.isDebugMode()) {
console.error('[HTTPS-TRANSPORT] Auth set result:', authResult);
}
// Get the MCP server instance
const server = this.mcpServer.getServer();
// Create a new transport for this request
debugLog.logState("Transport", "Step 1: Creating new StreamableHTTPServerTransport");
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
// Connect the server to the transport
debugLog.logState("Transport", "Step 2: Connecting server to transport");
await server.connect(transport);
debugLog.logState("Transport", "Step 3: Server connected successfully");
// Let the transport handle the request (both GET/SSE and POST)
debugLog.logState("Transport", "Step 4: About to handle request");
await (transport as any).handleRequest(req, res, req.body);
debugLog.logState("Transport", "Step 5: Request handled successfully");
} catch (error: any) {
if (this.config.isDebugMode()) {
console.error('[MCP] Error:', error.message);
}
this.writeBearerChallenge(res);
if (req.method === 'POST') {
return res.status(401).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Invalid token'
},
id: req.body?.id ?? null
});
}
return res.status(401).end();
}
};
// Register both GET and POST handlers
this.app.get('/mcp', mcpHandler);
this.app.post('/mcp', mcpHandler);
// Add root path welcome page
this.app.get('/', (req, res) => {
const issuer = this.config.getConfig().auth.issuer;
// Read the welcome page content from file
const welcomePagePath = path.join(process.cwd(), 'welcome-page-preview.html');
try {
let htmlContent = fs.readFileSync(welcomePagePath, 'utf8');
// Replace placeholder URLs with dynamic ones
htmlContent = htmlContent.replace(/https:\/\/welfare-adaptive-jade-biggest\.trycloudflare\.com/g, issuer.replace(/\/$/, ''));
res.setHeader('Content-Type', 'text/html');
res.send(htmlContent);
} catch (error) {
// Fallback if file doesn't exist
const mcpEndpoint = new URL('mcp', issuer).toString();
res.setHeader('Content-Type', 'text/html');
res.send(`
<html>
<head><title>Umbrella MCP Server</title></head>
<body style="font-family: Arial, sans-serif; margin: 2rem; text-align: center;">
<h1>🌂 Umbrella MCP Server</h1>
<p>Welcome to the Umbrella MCP Server for Claude Desktop integration!</p>
<h2>MCP Endpoint:</h2>
<code style="background: #f0f0f0; padding: 0.5rem; border-radius: 4px;">${mcpEndpoint}</code>
</body>
</html>
`);
}
});
}
private writeBearerChallenge(res: Response): void {
const authConfig = this.config.getConfig().auth;
const issuer = authConfig.issuer;
const resourceUrl = new URL('mcp', issuer).toString();
const params = [
'realm="umbrella-mcp"',
'scope="openid profile email"',
'error="invalid_token"',
'error_description="No bearer token on request"',
`issuer="${issuer}"`,
`resource="${resourceUrl}"`,
`authorization_endpoint="${new URL('authorize', issuer)}"`,
`token_endpoint="${new URL('oauth/token', issuer)}"`,
`registration_endpoint="${new URL('register', issuer)}"`
];
res.setHeader('WWW-Authenticate', `Bearer ${params.join(', ')}`);
}
async start(): Promise<void> {
const serverConfig = this.config.getConfig().server;
const port = serverConfig.port;
// Check if SSL certificates exist
const hasSSLCerts = fs.existsSync(serverConfig.httpsOptions.keyPath) &&
fs.existsSync(serverConfig.httpsOptions.certPath);
let server: https.Server | http.Server;
let protocol: string;
if (hasSSLCerts) {
// Use HTTPS if certificates are available
server = https.createServer({
key: fs.readFileSync(serverConfig.httpsOptions.keyPath),
cert: fs.readFileSync(serverConfig.httpsOptions.certPath)
}, this.app);
protocol = 'HTTPS';
} else {
// Fallback to HTTP if no certificates
console.log('[HTTP] SSL certificates not found, running in HTTP mode');
console.log('[HTTP] For HTTPS mode, run: openssl req -x509 -newkey rsa:4096 -keyout certs/server-key.pem -out certs/server-cert.pem -days 365 -nodes -subj "/CN=localhost"');
server = http.createServer(this.app);
protocol = 'HTTP';
}
server.listen(port, () => {
const issuer = this.config.getConfig().auth.issuer;
const mode = serverConfig.production ? 'PRODUCTION' : 'DEVELOPMENT';
const scheme = hasSSLCerts ? 'https' : 'http';
console.log(`[${protocol}] MCP OAuth server running in ${mode} mode on port ${port} (${protocol})`);
console.log(`[${protocol}] Local server: ${scheme}://127.0.0.1:${port}/`);
if (this.config.getConfig().cloudflare.currentTunnelUrl) {
console.log(`[${protocol}] Tunnel URL: ${this.config.getConfig().cloudflare.currentTunnelUrl}`);
console.log(`[${protocol}] OAuth endpoints: ${issuer}`);
console.log(`[${protocol}] MCP endpoint: ${new URL('mcp', issuer)}`);
} else if (serverConfig.production) {
console.log(`[${protocol}] OAuth endpoints: ${issuer}`);
console.log(`[${protocol}] MCP endpoint: ${new URL('mcp', issuer)}`);
}
});
}
}