/**
* mysql-mcp - HTTP Transport
*
* HTTP/SSE transport with OAuth 2.0 support.
*/
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import type { OAuthResourceServer } from '../auth/OAuthResourceServer.js';
import type { TokenValidator } from '../auth/TokenValidator.js';
import { validateAuth, formatOAuthError } from '../auth/middleware.js';
import { logger } from '../utils/logger.js';
/**
* HTTP transport configuration
*/
export interface HttpTransportConfig {
/** Port to listen on */
port: number;
/** Host to bind to (default: localhost) */
host?: string;
/** OAuth resource server (optional) */
resourceServer?: OAuthResourceServer;
/** Token validator (optional, required if resourceServer is provided) */
tokenValidator?: TokenValidator;
/** CORS allowed origins (default: none) */
corsOrigins?: string[];
}
/**
* HTTP Transport for MCP
*/
export class HttpTransport {
private server: ReturnType<typeof createServer> | null = null;
private readonly config: HttpTransportConfig;
private transport: StreamableHTTPServerTransport | null = null;
private readonly onConnect?: (transport: StreamableHTTPServerTransport) => void;
constructor(config: HttpTransportConfig, onConnect?: (transport: StreamableHTTPServerTransport) => void) {
this.config = {
...config,
host: config.host ?? 'localhost'
};
this.onConnect = onConnect;
}
/**
* Start the HTTP server
*/
async start(): Promise<void> {
return new Promise((resolve, reject) => {
this.server = createServer((req, res) => {
this.handleRequest(req, res).catch((error: unknown) => {
logger.error('HTTP request handler error', { error: String(error) });
if (!res.headersSent) {
res.writeHead(500);
res.end(JSON.stringify({ error: 'Internal server error' }));
}
});
});
this.server.on('error', reject);
this.server.listen(this.config.port, this.config.host, () => {
logger.info(`HTTP transport listening on ${this.config.host}:${this.config.port}`);
resolve();
});
});
}
/**
* Stop the HTTP server
*/
async stop(): Promise<void> {
return new Promise((resolve) => {
if (this.server) {
this.server.close(() => {
logger.info('HTTP transport stopped');
resolve();
});
} else {
resolve();
}
});
}
/**
* Handle incoming HTTP request
*/
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
// Set security headers for all responses
this.setSecurityHeaders(res);
// Set CORS headers
this.setCorsHeaders(req, res);
// Handle preflight
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
// Handle well-known endpoints
if (url.pathname === '/.well-known/oauth-protected-resource') {
this.handleProtectedResourceMetadata(res);
return;
}
// Health check
if (url.pathname === '/health') {
this.handleHealthCheck(res);
return;
}
// Authenticate if OAuth is configured
// Note: For SSE connection (/sse), we authenticate via query param or header
if (this.config.resourceServer && this.config.tokenValidator) {
try {
// For regular requests
await validateAuth(req.headers.authorization, {
tokenValidator: this.config.tokenValidator,
required: true
});
} catch (error) {
const { status, body } = formatOAuthError(error);
res.writeHead(status, {
'Content-Type': 'application/json',
'WWW-Authenticate': 'Bearer'
});
res.end(JSON.stringify(body));
return;
}
}
// Handle MCP requests
if (url.pathname === '/sse') {
await this.handleSSERequest(req, res);
return;
}
if (url.pathname === '/messages') {
await this.handleMessageRequest(req, res);
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
}
/**
* Handle SSE connection request
*/
private async handleSSERequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
// StreamableHTTPServerTransport usage guided by type feedback and introspection
const transport = new StreamableHTTPServerTransport();
this.transport = transport;
await transport.start();
if (this.onConnect) {
this.onConnect(transport);
}
// Handle the request (keeps connection open for SSE)
await transport.handleRequest(req, res);
}
/**
* Handle MCP message request
*/
private async handleMessageRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
if (!this.transport) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'No active connection' }));
return;
}
await this.transport.handleRequest(req, res);
}
/**
* Handle protected resource metadata endpoint
*/
private handleProtectedResourceMetadata(res: ServerResponse): void {
if (!this.config.resourceServer) {
res.writeHead(404);
res.end(JSON.stringify({ error: 'OAuth not configured' }));
return;
}
const metadata = this.config.resourceServer.getMetadata();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(metadata));
}
/**
* Handle health check endpoint
*/
private handleHealthCheck(res: ServerResponse): void {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'healthy', timestamp: new Date().toISOString() }));
}
/**
* Set security headers for all responses
*/
private setSecurityHeaders(res: ServerResponse): void {
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Enable XSS filtering
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevent caching of API responses
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
// Content Security Policy - API server has no content to load
res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'");
}
/**
* Set CORS headers
*/
private setCorsHeaders(req: IncomingMessage, res: ServerResponse): void {
const origin = req.headers.origin;
// Only allow configured origins
if (origin && this.config.corsOrigins?.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
}
}
}
/**
* Create an HTTP transport instance
*/
export function createHttpTransport(config: HttpTransportConfig, onConnect?: (transport: StreamableHTTPServerTransport) => void): HttpTransport {
return new HttpTransport(config, onConnect);
}