Skip to main content
Glama
middleware.ts8.96 kB
/** * Express Middleware for MCP Streamable HTTP Server * * This module contains reusable middleware functions for: * - CORS headers * - Protocol version validation * - Accept header normalization * - Authentication * - Static file serving for .well-known endpoints */ import express, { RequestHandler } from 'express'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { randomUUID, timingSafeEqual } from 'crypto'; import { debugLog } from './debug-log.js'; import { MCP_PROTOCOL_VERSION, SUPPORTED_MCP_PROTOCOL_VERSIONS, MCP_SESSION_ID_HEADER, MCP_PROTOCOL_VERSION_HEADER, JSON_RPC_ERROR_CODES, MAX_SESSIONS, } from './constants.js'; /** * CORS middleware - allows cross-origin requests * Exposes custom MCP headers to clients per spec * * Security Note: CORS wildcard (*) is intentionally used here because: * 1. This is a public MCP discovery API (/.well-known endpoints) * 2. Authentication is handled separately via Bearer tokens (not cookies) * 3. No sensitive data is exposed in unauthenticated responses * 4. Discovery endpoints (tools/list, resources/list) are intentionally public * 5. Sensitive operations (tools/call, resources/read) require API authentication * * This follows MCP specification best practices for public discovery endpoints. * For production deployments requiring stricter CORS, configure a reverse proxy * (e.g., nginx) to override these headers with specific allowed origins. */ export const corsMiddleware: RequestHandler = (req: any, res: any, next: any) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.header( 'Access-Control-Allow-Headers', `Content-Type, Authorization, ${MCP_SESSION_ID_HEADER}, ${MCP_PROTOCOL_VERSION_HEADER}` ); // MCP spec: Expose custom headers so clients can read them res.header( 'Access-Control-Expose-Headers', `${MCP_SESSION_ID_HEADER}, ${MCP_PROTOCOL_VERSION_HEADER}` ); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }; /** * Protocol version validation middleware * Validates and sets MCP protocol version headers * Supports multiple protocol versions for backward compatibility */ export const versionMiddleware: RequestHandler = (req: any, res: any, next: any) => { // Only validate on MCP endpoint requests if ((req.path === '/mcp' || req.path === '/') && req.method === 'POST') { const version = req.headers['mcp-protocol-version']; // Protocol version is optional but if provided, validate it against supported versions if (version && !SUPPORTED_MCP_PROTOCOL_VERSIONS.includes(version as any)) { return res.status(400).json({ jsonrpc: '2.0', error: { code: JSON_RPC_ERROR_CODES.INVALID_REQUEST, message: `Unsupported MCP protocol version: ${version}. Supported: ${SUPPORTED_MCP_PROTOCOL_VERSIONS.join(', ')}` }, id: null }); } // Always send latest protocol version in response to indicate server capabilities res.setHeader(MCP_PROTOCOL_VERSION_HEADER, MCP_PROTOCOL_VERSION); } next(); }; /** * Accept header normalization middleware * Ensures both application/json and text/event-stream are acceptable */ export const acceptMiddleware: RequestHandler = (req: any, _res: any, next: any) => { const raw = (req.headers['accept'] as string | undefined)?.trim() || ''; const parts = raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : []; const ensure = (mime: string) => { if (!parts.some((p) => p.includes(mime))) parts.push(mime); }; ensure('application/json'); ensure('text/event-stream'); req.headers['accept'] = parts.join(', '); next(); }; /** * Creates authentication middleware factory * @param requireApiAuth - Whether to require API authentication */ export function createAuthMiddleware(requireApiAuth: boolean): RequestHandler { return (req: any, res: any, next: any) => { // Lazy authentication - only check for tool invocations if (req.path === '/mcp' && requireApiAuth && req.method === 'POST') { // Parse the request body to check if it's a tool invocation const body = req.body; if (body && typeof body === 'object') { const method = body.method; // Only require auth for tool/resource calls, not for capability discovery const requiresAuth = method && ( method.startsWith('tools/') || method.startsWith('resources/') || method === 'tools/call' || method === 'resources/read' ); if (requiresAuth) { const authHeader = req.headers.authorization; const apiKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null; // Use timing-safe comparison to prevent timing attacks const expectedKey = process.env.PLUGGEDIN_API_KEY || ''; const isValid = apiKey && apiKey.length === expectedKey.length && timingSafeEqual(Buffer.from(apiKey), Buffer.from(expectedKey)); if (!isValid) { return res.status(401).json({ jsonrpc: '2.0', error: { code: JSON_RPC_ERROR_CODES.UNAUTHORIZED, message: 'Unauthorized: Invalid or missing API key' }, id: body.id || null }); } } } } next(); }; } /** * Creates a static file handler for .well-known endpoints * Sets proper Content-Type for mcp-config files */ export function createWellKnownHandler() { return express.static('.well-known', { setHeaders: (res, path) => { // Set proper Content-Type for mcp-config file if (path.endsWith('mcp-config')) { res.setHeader('Content-Type', 'application/json'); } } }); } // Session metadata interface (must match streamable-http.ts) interface SessionMetadata { transport: StreamableHTTPServerTransport; lastAccess: number; } /** * Evict oldest session when max sessions limit is reached (LRU eviction) */ function evictOldestSession(sessions: Map<string, SessionMetadata>): void { if (sessions.size === 0) return; let oldestSessionId: string | null = null; let oldestAccessTime = Infinity; // Find session with oldest access time for (const [sessionId, metadata] of sessions.entries()) { if (metadata.lastAccess < oldestAccessTime) { oldestAccessTime = metadata.lastAccess; oldestSessionId = sessionId; } } if (oldestSessionId) { const metadata = sessions.get(oldestSessionId); if (metadata) { try { metadata.transport.close().catch(() => {}); } catch {} sessions.delete(oldestSessionId); debugLog(`Evicted oldest session ${oldestSessionId} (LRU eviction)`); } } } /** * Resolves or creates a transport for the current request * Handles both stateful (session-based) and stateless modes * Implements LRU eviction when max sessions limit is reached * * @param req - Express request object * @param res - Express response object * @param server - MCP server instance * @param stateless - Whether to use stateless mode * @param sessions - Map of active sessions with metadata (for stateful mode) */ export async function resolveTransport( req: any, res: any, server: Server, stateless: boolean, sessions: Map<string, SessionMetadata> ): Promise<StreamableHTTPServerTransport> { if (stateless) { // Create a new transport for each request in stateless mode const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined // Disable session management in stateless mode }); await server.connect(transport); return transport; } // Use session-based transport management // MCP spec: Use title case for custom headers const sessionId = req.headers['mcp-session-id'] as string || randomUUID(); if (!sessions.has(sessionId)) { // Check if we need to evict a session (LRU eviction) if (sessions.size >= MAX_SESSIONS) { evictOldestSession(sessions); } // Create a new transport for this session const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId, onsessioninitialized: (id) => { debugLog(`Session initialized: ${id}`); } }); const metadata: SessionMetadata = { transport, lastAccess: Date.now() }; sessions.set(sessionId, metadata); await server.connect(transport); // Set session ID in response header (title case per MCP spec) res.setHeader(MCP_SESSION_ID_HEADER, sessionId); } else { // Update last access time for existing session const metadata = sessions.get(sessionId)!; metadata.lastAccess = Date.now(); } return sessions.get(sessionId)!.transport; }

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/VeriTeknik/pluggedin-mcp'

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