@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
251 lines • 9.23 kB
JavaScript
#!/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;
const sessionId = String((_sessionId = 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