UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

251 lines 9.31 kB
#!/usr/bin/env node import { randomUUID, timingSafeEqual } from 'node:crypto'; import path from 'node:path'; import process from 'node:process'; import express from 'express'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { SERVER_INFO, createDocsTools, registerDocsTools, validateDocsRoot } from "./mcp-docs-server.js"; function createLogger(silent) { return (...args) => { if (!silent) { console.error(...args); } }; } function parseAllowedHosts() { const raw = process.env.MCP_ALLOWED_HOSTS; if (!raw || raw.trim() === '' || raw.trim() === '*') { return undefined; } return raw.split(',').map(s => s.trim()).filter(Boolean); } function buildMcpServer(options = {}) { const tools = createDocsTools(options); const server = new McpServer(SERVER_INFO); registerDocsTools(server, tools); return { server, docsRoot: tools.docsRoot }; } function safeEqual(a, b) { const encoder = new TextEncoder(); const bufA = encoder.encode(a); const bufB = encoder.encode(b); if (bufA.length !== bufB.length) { return false; } return timingSafeEqual(bufA, bufB); } function authMiddleware(token) { if (!token) { return (_req, _res, next) => next(); } const expected = `Bearer ${token}`; return (req, res, next) => { var _req$headers$authoriz; const header = String((_req$headers$authoriz = req.headers['authorization']) !== null && _req$headers$authoriz !== void 0 ? _req$headers$authoriz : ''); if (safeEqual(header, expected)) { next(); return; } res.status(401).set('WWW-Authenticate', 'Bearer realm="eufemia-mcp"').json({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null }); }; } function hostAllowlistMiddleware(allowed, logErr) { if (!allowed || allowed.length === 0) { return (_req, _res, next) => next(); } const set = new Set(allowed.map(h => h.toLowerCase())); return (req, res, next) => { var _req$headers$host; const host = String((_req$headers$host = req.headers['host']) !== null && _req$headers$host !== void 0 ? _req$headers$host : '').split(':')[0].toLowerCase(); if (set.has(host)) { next(); return; } logErr(`[eufemia] rejected Host header: ${host}`); res.status(403).json({ jsonrpc: '2.0', error: { code: -32002, message: 'Host not allowed' }, id: null }); }; } export async function startHttpServer(options = {}) { var _options$port, _process$env$PORT, _ref, _options$host, _options$allowedHosts, _options$authToken, _options$silent, _ref3, _options$docsRoot; const port = (_options$port = options.port) !== null && _options$port !== void 0 ? _options$port : Number((_process$env$PORT = process.env.PORT) !== null && _process$env$PORT !== void 0 ? _process$env$PORT : 8787); const host = (_ref = (_options$host = options.host) !== null && _options$host !== void 0 ? _options$host : process.env.HOST) !== null && _ref !== void 0 ? _ref : '0.0.0.0'; const allowedHosts = (_options$allowedHosts = options.allowedHosts) !== null && _options$allowedHosts !== void 0 ? _options$allowedHosts : parseAllowedHosts(); const authToken = (_options$authToken = options.authToken) !== null && _options$authToken !== void 0 ? _options$authToken : process.env.MCP_AUTH_TOKEN; const logErr = createLogger((_options$silent = options.silent) !== null && _options$silent !== void 0 ? _options$silent : false); const app = express(); app.disable('x-powered-by'); app.use(express.json({ limit: '4mb' })); app.use(hostAllowlistMiddleware(allowedHosts, logErr)); app.get('/healthz', (_req, res) => { res.json({ ok: true, name: SERVER_INFO.name, version: SERVER_INFO.version, transports: ['streamable-http', 'sse'] }); }); const streamableTransports = new Map(); const handleStreamable = async (req, res) => { try { var _ref2; const sessionId = (_ref2 = typeof req.header === 'function' ? req.header('mcp-session-id') : req.headers['mcp-session-id']) !== null && _ref2 !== void 0 ? _ref2 : undefined; const body = req.body; let transport = sessionId ? streamableTransports.get(sessionId) : undefined; if (!transport) { const isInit = req.method === 'POST' && body && isInitializeRequest(body); if (req.method !== 'POST' || !isInit) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: no valid session id and not an initialize request.' }, id: null }); return; } transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); const localTransport = transport; transport.onclose = () => { const sid = localTransport.sessionId; if (sid) { streamableTransports.delete(sid); } }; const { server } = buildMcpServer({ docsRoot: options.docsRoot }); await server.connect(transport); } await transport.handleRequest(req, res, body); const sid = transport.sessionId; if (sid && !streamableTransports.has(sid)) { streamableTransports.set(sid, transport); } } catch (e) { logErr('[eufemia] streamable error:', e); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null }); } } }; app.post('/mcp', authMiddleware(authToken), handleStreamable); app.get('/mcp', authMiddleware(authToken), handleStreamable); app.delete('/mcp', authMiddleware(authToken), handleStreamable); const sseTransports = new Map(); app.get('/sse', authMiddleware(authToken), async (req, res) => { try { const transport = new SSEServerTransport('/messages', res); sseTransports.set(transport.sessionId, transport); transport.onclose = () => { sseTransports.delete(transport.sessionId); }; const { server } = buildMcpServer({ docsRoot: options.docsRoot }); await server.connect(transport); logErr(`[eufemia] sse connected: ${transport.sessionId}`); req.on('close', () => { sseTransports.delete(transport.sessionId); }); } catch (e) { logErr('[eufemia] sse start error:', e); if (!res.headersSent) { res.status(500).end('Failed to start SSE'); } } }); app.post('/messages', authMiddleware(authToken), async (req, res) => { var _sessionId, _req$query; const sessionId = String((_sessionId = (_req$query = req.query) === null || _req$query === void 0 ? void 0 : _req$query.sessionId) !== null && _sessionId !== void 0 ? _sessionId : ''); const transport = sessionId ? sseTransports.get(sessionId) : undefined; if (!transport) { res.status(404).json({ jsonrpc: '2.0', error: { code: -32004, message: 'Unknown sessionId' }, id: null }); return; } await transport.handlePostMessage(req, res, req.body); }); const docsRootAbs = path.resolve((_ref3 = (_options$docsRoot = options.docsRoot) !== null && _options$docsRoot !== void 0 ? _options$docsRoot : process.env.EUFEMIA_DOCS_ROOT) !== null && _ref3 !== void 0 ? _ref3 : './docs'); await validateDocsRoot(docsRootAbs); const httpServer = await new Promise((resolve, reject) => { const s = app.listen(port, host, () => resolve(s)); s.on('error', reject); }); const addr = httpServer.address(); const boundPort = typeof addr === 'object' && addr ? addr.port : Number(port); const url = `http://${host}:${boundPort}`; logErr(`[eufemia] http listening on ${url} (streamable: /mcp, sse: /sse, post: /messages)`); logErr(`[eufemia] docsRoot: ${docsRootAbs}`); return { url, port: boundPort, host, docsRoot: docsRootAbs, close: () => new Promise((resolve, reject) => { streamableTransports.forEach(t => { void t.close().catch(() => undefined); }); sseTransports.forEach(t => { void t.close().catch(() => undefined); }); streamableTransports.clear(); sseTransports.clear(); httpServer.close(err => err ? reject(err) : resolve()); }) }; } const shouldRun = (() => { const entryPath = process.argv[1] ? path.resolve(process.argv[1]) : ''; const entryName = entryPath ? path.basename(entryPath) : ''; const allowed = new Set(['mcp-http-server.js', 'mcp-http-server.mjs', 'mcp-http-server.cjs', 'mcp-http-server.ts', 'mcp-http-server.mts']); return entryName ? allowed.has(entryName) : false; })(); if (shouldRun) { startHttpServer().catch(e => { console.error('[eufemia] fatal:', e); process.exit(1); }); } //# sourceMappingURL=mcp-http-server.js.map