@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
258 lines (257 loc) • 9.83 kB
JavaScript
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.startHttpServer = startHttpServer;
var _nodeCrypto = require("node:crypto");
var _nodePath = _interopRequireDefault(require("node:path"));
var _nodeProcess = _interopRequireDefault(require("node:process"));
var _express = _interopRequireDefault(require("express"));
var _mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
var _sse = require("@modelcontextprotocol/sdk/server/sse.js");
var _streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
var _types = require("@modelcontextprotocol/sdk/types.js");
var _mcpDocsServer = require("./mcp-docs-server.js");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function createLogger(silent) {
return (...args) => {
if (!silent) {
console.error(...args);
}
};
}
function parseAllowedHosts() {
const raw = _nodeProcess.default.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 = (0, _mcpDocsServer.createDocsTools)(options);
const server = new _mcp.McpServer(_mcpDocsServer.SERVER_INFO);
(0, _mcpDocsServer.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 (0, _nodeCrypto.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
});
};
}
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 = _nodeProcess.default.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 : _nodeProcess.default.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 : _nodeProcess.default.env.MCP_AUTH_TOKEN;
const logErr = createLogger((_options$silent = options.silent) !== null && _options$silent !== void 0 ? _options$silent : false);
const app = (0, _express.default)();
app.disable('x-powered-by');
app.use(_express.default.json({
limit: '4mb'
}));
app.use(hostAllowlistMiddleware(allowedHosts, logErr));
app.get('/healthz', (_req, res) => {
res.json({
ok: true,
name: _mcpDocsServer.SERVER_INFO.name,
version: _mcpDocsServer.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 && (0, _types.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 _streamableHttp.StreamableHTTPServerTransport({
sessionIdGenerator: () => (0, _nodeCrypto.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 _sse.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 = _nodePath.default.resolve((_ref3 = (_options$docsRoot = options.docsRoot) !== null && _options$docsRoot !== void 0 ? _options$docsRoot : _nodeProcess.default.env.EUFEMIA_DOCS_ROOT) !== null && _ref3 !== void 0 ? _ref3 : './docs');
await (0, _mcpDocsServer.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 = _nodeProcess.default.argv[1] ? _nodePath.default.resolve(_nodeProcess.default.argv[1]) : '';
const entryName = entryPath ? _nodePath.default.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);
_nodeProcess.default.exit(1);
});
}
//# sourceMappingURL=mcp-http-server.js.map