Skip to main content
Glama

Smart EHR MCP Server

by jmandel
server.ts17 kB
/** * MCP Intra‑Browser Proxy – Bun‑powered Express version * * • 401 + WWW‑Authenticate on the SSE route * • OAuth code grant (one tab bounce) — no device flow * • A *single* tool iframe at a time → simpler stream routing * • Full duplex streaming between MCP Streamable HTTP and WebSocket (browser UI) */ import express, { Request, Response, NextFunction } from "express"; import cors from "cors"; import { randomUUID } from "crypto"; import { WebSocketServer, WebSocket } from "ws"; import { createServer, IncomingMessage } from "http"; import path from "node:path"; import { parse } from "node:url"; /* ------------------------------------------------------------------ */ /* 1 – in‑memory session registry */ /* ------------------------------------------------------------------ */ // --- Type helper for JSON RPC Messages --- interface JsonRpcMessage { jsonrpc: "2.0"; id?: string | number; method?: string; params?: any; result?: any; error?: any; } interface Session { token: string; code?: string; sse?: Response; ws?: WebSocket; toSSE: string[]; // queued → Client (via GET /mcp) toWS: string[]; // queued → UI (via WebSocket) postResponses: Map<string | number, Response>; } const sessions = new Map<string, Session>(); // Helper: ensure session exists for a given config key function ensureSession(config: string): Session { let sess = sessions.get(config); if (!sess) { sess = { token: config, toSSE: [], toWS: [], postResponses: new Map() } as Session; sessions.set(config, sess); } return sess; } /* ------------------------------------------------------------------ */ /* 2 – express on Bun */ /* ------------------------------------------------------------------ */ // Define handler type explicitly to satisfy linter // Make return type `any` to accommodate various ways handlers can end type ExpressHandler = (req: Request, res: Response, next?: NextFunction) => any; const app = express(); app.use(cors({ origin: true, credentials: true })); app.use(express.json({ limit: "5mb" })); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.resolve("public"))); // serves index.html + js // Detailed Request Logging Middleware const loggingMiddleware: ExpressHandler = (req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; console.log( `-------------------------------------------------- [REQ LOG] ${new Date().toISOString()} Method: ${req.method} URL: ${req.originalUrl} Status: ${res.statusCode} IP: ${req.ip || req.socket.remoteAddress} Headers: ${JSON.stringify(req.headers, null, 2)} Query: ${JSON.stringify(req.query, null, 2)} Body: ${req.body ? JSON.stringify(req.body, null, 2) : 'N/A'} Duration: ${duration}ms --------------------------------------------------` ); }); if (next) { next(); } }; app.use(loggingMiddleware); const http = createServer(app); /* ---------- OAuth, DCR, Well-Known (Explicit Handler Types) ----------- */ // Fix linter errors by explicitly typing handlers const authorizeHandler: ExpressHandler = (req, res) => { const { client_id, redirect_uri, state } = req.query; if (!client_id || !redirect_uri) return res.sendStatus(400); const sess = ensureSession(req.params.config || "global"); const code = randomUUID(); sess.code = code; // Store parameters for the script const toolUrl = `/?token=${sess.token}`; const redirectUriString = String(redirect_uri); // Ensure it's a string const stateString = state !== undefined ? String(state) : undefined; res.type("html").send(/* html */` <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MCP Proxy Authorization</title> <style> body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; flex-direction: column; } button { padding: 10px 20px; font-size: 16px; cursor: pointer; } </style> </head> <body> <h1>Authorize Access</h1> <p>Click the button below to authorize the client and open the MCP tool interface.</p> <button id="authorizeBtn">Authorize and Open Tool</button> <script> const toolUrl = ${JSON.stringify(toolUrl)}; const redirectUriBase = ${JSON.stringify(redirectUriString)}; const code = ${JSON.stringify(code)}; const state = ${JSON.stringify(stateString)}; // Will be null if undefined document.getElementById('authorizeBtn').addEventListener('click', () => { // 1. Open the tool tab window.open(toolUrl, '_blank', 'popup'); // 2. Construct the redirect URL const redirectUrl = new URL(redirectUriBase, window.location.origin); redirectUrl.searchParams.set('code', code); if (state !== null && state !== undefined) { redirectUrl.searchParams.set('state', state); } // 3. Redirect the current tab to complete the OAuth flow window.location.href = redirectUrl.toString(); }); </script> </body> </html>`); }; app.get("/oauth/authorize", authorizeHandler); const tokenHandler: ExpressHandler = (req, res) => { const { code, grant_type } = req.body; if (grant_type !== "authorization_code") return res.sendStatus(400); const token = ensureSession(req.params.config || "global").code; if (!token) return res.status(400).json({ error: "invalid_grant" }); ensureSession(req.params.config || "global").code = undefined; res.json({ access_token: token, token_type: "Bearer", expires_in: 3600 }); }; app.post("/oauth/token", tokenHandler); const dcrHandler: ExpressHandler = (req, res) => { const { client_name, redirect_uris, grant_types, response_types, token_endpoint_auth_method, scope } = req.body; console.log("[DCR] Received registration request:", req.body); const clientId = `client-${randomUUID()}`; const responseBody = { client_id: clientId, client_id_issued_at: Math.floor(Date.now() / 1000), ...(redirect_uris && { redirect_uris }), ...(grant_types && { grant_types }), ...(response_types && { response_types }), ...(token_endpoint_auth_method && { token_endpoint_auth_method }), ...(scope && { scope }), registration_access_token: `reg-${randomUUID()}`, registration_client_uri: `/register/${clientId}`, }; console.log("[DCR] Responding with:", responseBody); res.status(201).json(responseBody); }; app.post("/register", dcrHandler); const wellKnownHandler: ExpressHandler = (req, res) => { const protocol = req.headers['x-forwarded-proto'] || req.protocol; const host = req.headers.host; const baseUrl = `${protocol}://${host}`; const metadata = { issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/authorize`, token_endpoint: `${baseUrl}/oauth/token`, registration_endpoint: `${baseUrl}/register`, scopes_supported: ["mcp"], response_types_supported: ["code"], grant_types_supported: ["authorization_code"], token_endpoint_auth_methods_supported: ["none"], code_challenge_methods_supported: ["S256"], }; res.json(metadata); }; app.get("/.well-known/oauth-authorization-server", wellKnownHandler); /* ---------- MCP Streamable HTTP Endpoint (/mcp) ----------------- */ // Derive config key from request params or query (default "global") function extractConfig(req: Request): string { return (req.params as any)?.config || (req.query?.config as string) || "global"; } // GET /mcp or /:config/mcp : Establishes the persistent stream const mcpGetHandler: ExpressHandler = (req, res) => { const configKey = extractConfig(req); const sess = ensureSession(configKey); console.log(`[GET /mcp] Request for config '${configKey}'`); if (sess.sse) { try { sess.sse.end(); } catch {} sess.sse = undefined; console.log(`[GET /mcp] Replaced existing SSE stream for config '${configKey}'.`); } console.log(`[GET /mcp] Establishing SSE stream for config '${configKey}'.`); res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); res.write(":\n\n"); sess.sse = res; console.log(`[GET /mcp] SSE connection stored for session. Flushing ${sess.toSSE.length} queued messages.`); sess.toSSE.splice(0).forEach(m => { console.log(`[GET /mcp] Writing queued message to SSE:`, m.substring(0,100)); res.write(`data: ${m}\n\n`); }); res.on("close", () => { console.log(`[GET /mcp] SSE connection closed for token ${configKey.substring(0, 6)}...`); const currentSess = sessions.get(configKey); if (currentSess && currentSess.sse === res) { currentSess.sse = undefined; } }); }; app.get("/mcp", mcpGetHandler); app.get("/:config/mcp", mcpGetHandler); // POST /mcp : Receives client-to-server messages const mcpPostHandler: ExpressHandler = (req, res) => { const configKey = extractConfig(req); const sess = ensureSession(configKey); console.log(`[POST /mcp] Request for config '${configKey}'`); if (!sess) { console.log(`[POST /mcp] Session not found for config.`); res.setHeader("WWW-Authenticate", 'Bearer realm="mcp-proxy", scope="mcp",' + ' authorization_uri="/oauth/authorize", token_uri="/oauth/token"'); console.log(`[POST /mcp] Sending 401 Unauthorized response.`); return res.status(401).json({ error: "invalid_token", error_description: "Invalid or expired token." }); } const messages: JsonRpcMessage[] = Array.isArray(req.body) ? req.body : [req.body]; const requestMessages = messages.filter(msg => msg && msg.id !== undefined && msg.method !== undefined); const hasRequests = requestMessages.length > 0; const requestIds = requestMessages.map(msg => msg.id); console.log(`[POST /mcp] Received ${messages.length} message(s). Contains requests: ${hasRequests}`); const payload = JSON.stringify(req.body); console.log(`[POST /mcp] Forwarding payload to WS if connected:`, payload.substring(0, 200) + (payload.length > 200 ? '...' : '')); if (sess.ws && sess.ws.readyState === WebSocket.OPEN) { sess.ws.send(payload); } else { console.log(`[POST /mcp] WebSocket not open, queueing payload for UI (toWS).`); sess.toWS.push(payload); } if (hasRequests && sess.ws && sess.ws.readyState === WebSocket.OPEN) { // WS available → keep POST open as SSE for direct response streaming console.log(`[POST /mcp] WS open. Setting up SSE response stream for IDs: ${requestIds.join(', ')}`); res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); res.write(":\n\n"); for (const id of requestIds) { if (id !== undefined) sess.postResponses.set(id, res); } res.on('close', () => { console.log(`[POST /mcp] POST SSE closed for IDs: ${requestIds.join(', ')}`); const currentSess = sessions.get(configKey); if (currentSess) { requestIds.forEach(id => { if (id !== undefined && currentSess.postResponses.get(id) === res) { currentSess.postResponses.delete(id); } }); } }); } else { // No WS yet → queue requests; responses will go out via GET stream later console.log(`[POST /mcp] WS not open or no requests. Immediate 202 Accepted.`); res.status(202).send(); } }; app.post("/mcp", mcpPostHandler); app.post("/:config/mcp", mcpPostHandler); /* ---------- WebSocket bridge (Handles messages FROM browser UI) --- */ const wss = new WebSocketServer({ noServer: true }); http.on("upgrade", (req, sock, head) => { const url = new URL(req.url ?? "", `http://${req.headers.host}`); if (url.pathname !== "/ws") { console.log("[Upgrade] Ignoring non-/ws upgrade request."); return sock.destroy(); } console.log("[Upgrade] Handling upgrade request for /ws"); wss.handleUpgrade(req, sock, head, ws => wss.emit("connection", ws, req)); }); wss.on("connection", (ws: WebSocket, req: IncomingMessage) => { // Determine config key from query string (?config=foo) – default "global" const parsedUrl = parse(req.url ?? "", true); const configQuery = parsedUrl.query.config; const configKey = Array.isArray(configQuery) ? configQuery[0] : (configQuery as string) || "global"; const sess = ensureSession(configKey); console.log(`[WS Connect] Connection attempt for config '${configKey}'`); if (!sess) { console.log(`[WS Connect] Session not found for token. Closing WS.`); return ws.close(4001, "session unavailable"); } console.log(`[WS Connect] WebSocket connected – replacing any existing WS for config '${configKey}'`); // If an old WebSocket exists, close it (last window wins) if (sess.ws && sess.ws.readyState === WebSocket.OPEN) { try { sess.ws.close(4000, "replaced by new window"); } catch {} } sess.ws = ws; console.log(`[WS Connect] Flushing ${sess.toWS.length} queued messages (from POST /mcp).`); sess.toWS.splice(0).forEach(m => ws.send(m)); ws.on("message", (data) => { const messageString = String(data); console.log(`[WS Message] Received message from WS (Browser UI)`, messageString.substring(0, 200) + (messageString.length > 200 ? '...' : '')); let rpcMessage: JsonRpcMessage | null = null; try { rpcMessage = JSON.parse(messageString); } catch (e) { console.warn("[WS Message] Received non-JSON message from WS, cannot route precisely.", e); if (sess.sse && !sess.sse.closed) { console.log(`[WS Message] Forwarding non-JSON message to GET /mcp stream.`); sess.sse.write(`data: ${messageString}\n\n`); } else { console.log(`[WS Message] GET /mcp stream closed, queueing non-JSON message for Client (toSSE).`); sess.toSSE.push(messageString); } return; } if (rpcMessage && rpcMessage.id !== undefined && rpcMessage.method === undefined) { const responseId = rpcMessage.id; const postResponseStream = sess.postResponses.get(responseId); if (postResponseStream && !postResponseStream.closed) { console.log(`[WS Message] Routing response ID ${responseId} to its specific POST /mcp stream.`); postResponseStream.write(`data: ${messageString}\n\n`); sess.postResponses.delete(responseId); console.log(`[WS Message] Removed ID ${responseId} from postResponses map.`); } else { console.log(`[WS Message] Response ID ${responseId} has no matching active POST stream or stream is closed. Routing to GET /mcp stream (if available).`); if (sess.sse && !sess.sse.closed) { sess.sse.write(`data: ${messageString}\n\n`); } else { console.log(`[WS Message] GET /mcp stream closed, queueing unmatched response for Client (toSSE).`); sess.toSSE.push(messageString); } } } else if (rpcMessage && rpcMessage.method !== undefined) { console.log(`[WS Message] Routing notification/request ('${rpcMessage.method}') to GET /mcp stream (if available).`); if (sess.sse && !sess.sse.closed) { sess.sse.write(`data: ${messageString}\n\n`); } else { console.log(`[WS Message] GET /mcp stream closed, queueing notification/request for Client (toSSE).`); sess.toSSE.push(messageString); } } else { console.warn("[WS Message] Received message from WS is not a valid JSON-RPC response or notification/request:", rpcMessage); } }); ws.on("close", () => { console.log(`[WS Close] WebSocket closed for token ${configKey.substring(0, 6)}...`); const currentSess = sessions.get(configKey); if (currentSess && currentSess.ws === ws) { currentSess.ws = undefined; } }); ws.on("error", (error) => { console.error(`[WS Error] WebSocket error for token ${configKey.substring(0, 6)}...:`, error); const currentSess = sessions.get(configKey); if (currentSess && currentSess.ws === ws) { currentSess.ws = undefined; } }); }); /* ------------------------------------------------------------------ */ /* 3 – start Bun‑powered server */ /* ------------------------------------------------------------------ */ const PORT = Number(process.env.PORT ?? 8787); http.listen(PORT, () => console.log(`➡ MCP proxy running on http://localhost:${PORT}`));

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/jmandel/health-record-mcp'

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