Skip to main content
Glama
http.ts13.4 kB
/** * db-mcp - HTTP Transport * * Streamable HTTP transport with OAuth 2.0 integration. * Provides a secure HTTP endpoint for MCP communication. */ import express, { type Express, type RequestHandler } from 'express'; import cors from 'cors'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { OAuthResourceServer } from '../auth/OAuthResourceServer.js'; import { AuthorizationServerDiscovery } from '../auth/AuthorizationServerDiscovery.js'; import { TokenValidator } from '../auth/TokenValidator.js'; import { createAuthMiddleware, oauthErrorHandler } from '../auth/middleware.js'; import { SUPPORTED_SCOPES } from '../auth/scopes.js'; import { createModuleLogger, ERROR_CODES } from '../utils/logger.js'; import type http from 'node:http'; const logger = createModuleLogger('HTTP'); // ============================================================================= // Types // ============================================================================= /** * HTTP transport configuration */ export interface HttpTransportConfig { /** Port to listen on */ port: number; /** Host to bind to (default: '0.0.0.0') */ host?: string; /** OAuth configuration */ oauth: { /** Enable OAuth authentication */ enabled: boolean; /** Authorization server URL */ authorizationServerUrl: string; /** Expected audience in tokens */ audience: string; /** Expected issuer (defaults to authorizationServerUrl) */ issuer?: string; /** JWKS URI (auto-discovered if not provided) */ jwksUri?: string; /** Clock tolerance in seconds (default: 60) */ clockTolerance?: number; /** Paths that bypass authentication */ publicPaths?: string[]; }; /** CORS configuration */ cors?: { /** Allowed origins */ origin?: string | string[] | boolean; /** Allowed methods */ methods?: string[]; /** Allowed headers */ allowedHeaders?: string[]; /** Exposed headers */ exposedHeaders?: string[]; /** Allow credentials */ credentials?: boolean; }; /** Resource URI (defaults to http://localhost:{port}) */ resourceUri?: string; } // ============================================================================= // HTTP Transport // ============================================================================= /** * HTTP Transport for MCP with OAuth 2.0 integration */ export class HttpTransport { private readonly config: HttpTransportConfig; private app: Express | null = null; private server: http.Server | null = null; private mcpTransport: StreamableHTTPServerTransport | null = null; private resourceServer: OAuthResourceServer | null = null; private authServerDiscovery: AuthorizationServerDiscovery | null = null; private tokenValidator: TokenValidator | null = null; constructor(config: HttpTransportConfig) { this.config = { ...config, host: config.host ?? '0.0.0.0' }; } /** * Initialize the transport * * Sets up Express app, OAuth components, and MCP transport. * * @returns The MCP StreamableHTTPServerTransport instance */ async initialize(): Promise<StreamableHTTPServerTransport> { logger.info('INIT', 'Initializing HTTP transport...'); // Create Express app this.app = express(); // Configure CORS if (this.config.cors) { this.app.use(cors(this.config.cors) as RequestHandler); } else { // Default CORS for development this.app.use(cors({ origin: true, methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Authorization', 'Content-Type'], credentials: true }) as RequestHandler); } // Parse JSON bodies this.app.use(express.json()); // Determine resource URI const resourceUri = this.config.resourceUri ?? `http://localhost:${String(this.config.port)}`; // Set up OAuth if enabled if (this.config.oauth.enabled) { await this.setupOAuth(resourceUri); } // Health check endpoint (always public) this.app.get('/health', (_req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), oauth: this.config.oauth.enabled }); }); // Create MCP transport this.mcpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() }); // Set up MCP endpoint this.setupMcpEndpoint(); // Error handler this.app.use(oauthErrorHandler); logger.info('INIT_COMPLETE', 'HTTP transport initialized', { context: { port: this.config.port, oauth: this.config.oauth.enabled, resourceUri } }); return this.mcpTransport; } /** * Set up OAuth 2.0 components */ private async setupOAuth(resourceUri: string): Promise<void> { logger.info('OAUTH_SETUP', 'Setting up OAuth 2.0...'); // Create Resource Server this.resourceServer = new OAuthResourceServer({ resource: resourceUri, authorizationServers: [this.config.oauth.authorizationServerUrl], scopesSupported: [...SUPPORTED_SCOPES] }); // Serve Protected Resource Metadata endpoint this.app?.get( '/.well-known/oauth-protected-resource', this.resourceServer.getMetadataHandler() ); // Discover authorization server metadata this.authServerDiscovery = new AuthorizationServerDiscovery({ authServerUrl: this.config.oauth.authorizationServerUrl }); try { const metadata = await this.authServerDiscovery.discover(); // Create Token Validator this.tokenValidator = new TokenValidator({ jwksUri: this.config.oauth.jwksUri ?? metadata.jwks_uri ?? '', issuer: this.config.oauth.issuer ?? metadata.issuer, audience: this.config.oauth.audience, clockTolerance: this.config.oauth.clockTolerance }); logger.info('OAUTH_READY', 'OAuth 2.0 setup complete'); } catch (error) { // If discovery fails, we can still start without OAuth validation // This allows the server to start and return proper errors to clients logger.warning( ERROR_CODES.AUTH.DISCOVERY_FAILED, 'Authorization server discovery failed. OAuth validation disabled.', { error: error instanceof Error ? error : undefined } ); // Create a dummy token validator that always fails // This ensures requests still get proper 401 responses if (!this.config.oauth.jwksUri) { logger.error( ERROR_CODES.AUTH.DISCOVERY_FAILED, 'No JWKS URI available. Please provide oauth.jwksUri in config.', {} ); throw error; } this.tokenValidator = new TokenValidator({ jwksUri: this.config.oauth.jwksUri, issuer: this.config.oauth.issuer ?? this.config.oauth.authorizationServerUrl, audience: this.config.oauth.audience, clockTolerance: this.config.oauth.clockTolerance }); } } /** * Set up the MCP endpoint with authentication */ private setupMcpEndpoint(): void { if (!this.app || !this.mcpTransport) { throw new Error('Transport not initialized'); } // Apply auth middleware if OAuth is enabled if (this.config.oauth.enabled && this.tokenValidator && this.resourceServer) { const authMiddleware = createAuthMiddleware({ tokenValidator: this.tokenValidator, resourceServer: this.resourceServer, publicPaths: [ '/health', ...(this.config.oauth.publicPaths ?? []) ] }); this.app.use(authMiddleware); } // MCP message endpoint // Note: We use type assertion here because Express extends IncomingMessage // but the SDK's type definition is stricter about the auth property this.app.post('/mcp', async (req, res) => { if (!this.mcpTransport) { res.status(500).json({ error: 'Transport not initialized' }); return; } try { // Cast to unknown first to avoid direct type incompatibility await this.mcpTransport.handleRequest( req as unknown as Parameters<typeof this.mcpTransport.handleRequest>[0], res, req.body as unknown ); } catch (error) { logger.error( ERROR_CODES.SERVER.TRANSPORT_ERROR, 'Error handling MCP request', { error: error instanceof Error ? error : undefined } ); if (!res.headersSent) { res.status(500).json({ error: 'internal_error', error_description: 'Failed to process MCP request' }); } } }); // Handle DELETE for session cleanup (if needed by StreamableHTTP) this.app.delete('/mcp', async (req, res) => { if (!this.mcpTransport) { res.status(500).json({ error: 'Transport not initialized' }); return; } try { await this.mcpTransport.handleRequest( req as unknown as Parameters<typeof this.mcpTransport.handleRequest>[0], res, req.body as unknown ); } catch (error) { logger.error( ERROR_CODES.SERVER.TRANSPORT_ERROR, 'Error handling MCP DELETE request', { error: error instanceof Error ? error : undefined } ); if (!res.headersSent) { res.status(500).json({ error: 'internal_error', error_description: 'Failed to process MCP request' }); } } }); } /** * Start listening on the configured port */ async start(): Promise<void> { if (!this.app) { throw new Error('Transport not initialized. Call initialize() first.'); } return new Promise((resolve, reject) => { try { this.server = this.app?.listen(this.config.port, this.config.host ?? '0.0.0.0', () => { logger.info('SERVER_STARTED', `HTTP server listening on ${this.config.host ?? '0.0.0.0'}:${String(this.config.port)}`); resolve(); }) ?? null; this.server?.on('error', (error) => { logger.error( ERROR_CODES.SERVER.START_FAILED, 'Failed to start HTTP server', { error } ); reject(error); }); } catch (error) { reject(error instanceof Error ? error : new Error(String(error))); } }); } /** * Stop the server */ async stop(): Promise<void> { return new Promise((resolve, reject) => { if (!this.server) { resolve(); return; } this.server.close((error) => { if (error) { logger.error( ERROR_CODES.SERVER.SHUTDOWN_FAILED, 'Error stopping HTTP server', { error } ); reject(error); } else { logger.info('SERVER_STOPPED', 'HTTP server stopped'); resolve(); } }); }); } /** * Get the underlying Express app */ getApp(): Express | null { return this.app; } /** * Get the MCP transport */ getMcpTransport(): StreamableHTTPServerTransport | null { return this.mcpTransport; } /** * Get the OAuth Resource Server */ getResourceServer(): OAuthResourceServer | null { return this.resourceServer; } } // ============================================================================= // Factory Function // ============================================================================= /** * Create an HTTP transport instance */ export function createHttpTransport(config: HttpTransportConfig): HttpTransport { return new HttpTransport(config); }

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/neverinfamous/db-mcp'

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