@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
90 lines • 12.2 kB
JavaScript
/**
* Metrics API routes for the unified dev console.
*
* Mounts on the existing Express app to provide:
* - GET /api/metrics — JSON query (delegates to MemoryMetricsSink)
* - GET /api/metrics/stream — SSE endpoint, pushes each new snapshot
*/
import { Router } from 'express';
import { UnicodeValidator } from '../../security/validators/unicodeValidator.js';
function normalizedStringParam(query, key) {
const val = query[key];
if (typeof val === 'string' && val) {
return UnicodeValidator.normalize(val).normalizedContent;
}
return undefined;
}
function parseMetricsQueryOptions(query) {
const options = {};
const names = normalizedStringParam(query, 'names');
if (names)
options['names'] = names.split(',').map(s => s.trim());
const source = normalizedStringParam(query, 'source');
if (source)
options['source'] = source;
const type = normalizedStringParam(query, 'type');
if (type)
options['type'] = type;
for (const field of ['since', 'until']) {
if (typeof query[field] === 'string' && query[field])
options[field] = query[field];
}
if (typeof query['latest'] === 'string') {
options['latest'] = query['latest'] !== 'false';
}
for (const field of ['limit', 'offset']) {
if (typeof query[field] === 'string') {
const parsed = Number.parseInt(query[field], 10);
if (!Number.isNaN(parsed))
options[field] = parsed;
}
}
return options;
}
export function createMetricsRoutes(metricsSink) {
const router = Router();
const clients = new Set();
// GET /api/metrics — JSON query
router.get('/metrics', (req, res) => {
const result = metricsSink.query(parseMetricsQueryOptions(req.query));
res.json(result);
});
// GET /api/metrics/stream — SSE endpoint for real-time metric snapshots.
// Currently unused by the built-in web console (which polls /api/metrics).
// Retained for third-party consumers (Prometheus, Grafana agent, custom dashboards)
// that benefit from push-based metric delivery.
router.get('/metrics/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
res.write(':connected\n\n');
const client = { res };
clients.add(client);
// Keep-alive heartbeat — prevents proxies from closing idle connections
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30_000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(client);
});
});
function onSnapshot(snapshot) {
const data = JSON.stringify(snapshot);
for (const client of clients) {
if (client.res.destroyed) {
clients.delete(client);
continue;
}
client.res.write(`data: ${data}\n\n`);
}
}
return {
router,
onSnapshot,
clientCount: () => clients.size,
};
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljc1JvdXRlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy93ZWIvcm91dGVzL21ldHJpY3NSb3V0ZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7OztHQU1HO0FBRUgsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUlqQyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSwrQ0FBK0MsQ0FBQztBQVlqRixTQUFTLHFCQUFxQixDQUFDLEtBQXVCLEVBQUUsR0FBVztJQUNqRSxNQUFNLEdBQUcsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDdkIsSUFBSSxPQUFPLEdBQUcsS0FBSyxRQUFRLElBQUksR0FBRyxFQUFFLENBQUM7UUFDbkMsT0FBTyxnQkFBZ0IsQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUMsaUJBQWlCLENBQUM7SUFDM0QsQ0FBQztJQUNELE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUM7QUFFRCxTQUFTLHdCQUF3QixDQUFDLEtBQXVCO0lBQ3ZELE1BQU0sT0FBTyxHQUE0QixFQUFFLENBQUM7SUFFNUMsTUFBTSxLQUFLLEdBQUcscUJBQXFCLENBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3BELElBQUksS0FBSztRQUFFLE9BQU8sQ0FBQyxPQUFPLENBQUMsR0FBRyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBRWxFLE1BQU0sTUFBTSxHQUFHLHFCQUFxQixDQUFDLEtBQUssRUFBRSxRQUFRLENBQUMsQ0FBQztJQUN0RCxJQUFJLE1BQU07UUFBRSxPQUFPLENBQUMsUUFBUSxDQUFDLEdBQUcsTUFBTSxDQUFDO0lBRXZDLE1BQU0sSUFBSSxHQUFHLHFCQUFxQixDQUFDLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FB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QU0sTUFBTSxJQUFJLE9BQU8sRUFBRSxDQUFDO1lBQzdCLElBQUksTUFBTSxDQUFDLEdBQUcsQ0FBQyxTQUFTLEVBQUUsQ0FBQztnQkFBQyxPQUFPLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDO2dCQUFDLFNBQVM7WUFBQyxDQUFDO1lBQy9ELE1BQU0sQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLFNBQVMsSUFBSSxNQUFNLENBQUMsQ0FBQztRQUN4QyxDQUFDO0lBQ0gsQ0FBQztJQUVELE9BQU87UUFDTCxNQUFNO1FBQ04sVUFBVTtRQUNWLFdBQVcsRUFBRSxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsSUFBSTtLQUNoQyxDQUFDO0FBQ0osQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogTWV0cmljcyBBUEkgcm91dGVzIGZvciB0aGUgdW5pZmllZCBkZXYgY29uc29sZS5cbiAqXG4gKiBNb3VudHMgb24gdGhlIGV4aXN0aW5nIEV4cHJlc3MgYXBwIHRvIHByb3ZpZGU6XG4gKiAtIEdFVCAvYXBpL21ldHJpY3MgICAgICAgIOKAlCBKU09OIHF1ZXJ5IChkZWxlZ2F0ZXMgdG8gTWVtb3J5TWV0cmljc1NpbmspXG4gKiAtIEdFVCAvYXBpL21ldHJpY3Mvc3RyZWFtIOKAlCBTU0UgZW5kcG9pbnQsIHB1c2hlcyBlYWNoIG5ldyBzbmFwc2hvdFxuICovXG5cbmltcG9ydCB7IFJvdXRlciB9IGZyb20gJ2V4cHJlc3MnO1xuaW1wb3J0IHR5cGUgeyBSZXF1ZXN0LCBSZXNwb25zZSB9IGZyb20gJ2V4cHJlc3MnO1xuaW1wb3J0IHR5cGUgeyBNZW1vcnlNZXRyaWNzU2luayB9IGZyb20gJy4uLy4uL21ldHJpY3Mvc2lua3MvTWVtb3J5TWV0cmljc1NpbmsuanMnO1xuaW1wb3J0IHR5cGUgeyBNZXRyaWNTbmFwc2hvdCwgTWV0cmljVHlwZSB9IGZyb20gJy4uLy4uL21ldHJpY3MvdHlwZXMuanMnO1xuaW1wb3J0IHsgVW5pY29kZVZhbGlkYXRvciB9IGZyb20gJy4uLy4uL3NlY3VyaXR5L3ZhbGlkYXRvcnMvdW5pY29kZVZhbGlkYXRvci5qcyc7XG5cbmludGVyZmFjZSBTU0VDbGllbnQge1xuICByZXM6IFJlc3BvbnNlO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1ldHJpY3NSb3V0ZXNSZXN1bHQge1xuICByb3V0ZXI6IFJvdXRlcjtcbiAgb25TbmFwc2hvdDogKHNuYXBzaG90OiBNZXRyaWNTbmFwc2hvdCkgPT4gdm9pZDtcbiAgY2xpZW50Q291bnQ6ICgpID0+IG51bWJlcjtcbn1cblxuZnVuY3Rpb24gbm9ybWFsaXplZFN0cmluZ1BhcmFtKHF1ZXJ5OiBSZXF1ZXN0WydxdWVyeSddLCBrZXk6IHN0cmluZyk6IHN0cmluZyB8IHVuZGVmaW5lZCB7XG4gIGNvbnN0IHZhbCA9IHF1ZXJ5W2tleV07XG4gIGlmICh0eXBlb2YgdmFsID09PSAnc3RyaW5nJyAmJiB2YWwpIHtcbiAgICByZXR1cm4gVW5pY29kZVZhbGlkYXRvci5ub3JtYWxpemUodmFsKS5ub3JtYWxpemVkQ29udGVudDtcbiAgfVxuICByZXR1cm4gdW5kZWZpbmVkO1xufVxuXG5mdW5jdGlvbiBwYXJzZU1ldHJpY3NRdWVyeU9wdGlvbnMocXVlcnk6IFJlcXVlc3RbJ3F1ZXJ5J10pOiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPiB7XG4gIGNvbnN0IG9wdGlvbnM6IFJlY29yZDxzdHJpbmcsIHVua25vd24+ID0ge307XG5cbiAgY29uc3QgbmFtZXMgPSBub3JtYWxpemVkU3RyaW5nUGFyYW0ocXVlcnksICduYW1lcycpO1xuICBpZiAobmFtZXMpIG9wdGlvbnNbJ25hbWVzJ10gPSBuYW1lcy5zcGxpdCgnLCcpLm1hcChzID0+IHMudHJpbSgpKTtcblxuICBjb25zdCBzb3VyY2UgPSBub3JtYWxpemVkU3RyaW5nUGFyYW0ocXVlcnksICdzb3VyY2UnKTtcbiAgaWYgKHNvdXJjZSkgb3B0aW9uc1snc291cmNlJ10gPSBzb3VyY2U7XG5cbiAgY29uc3QgdHlwZSA9IG5vcm1hbGl6ZWRTdHJpbmdQYXJhbShxdWVyeSwgJ3R5cGUnKTtcbiAgaWYgKHR5cGUpIG9wdGlvbnNbJ3R5cGUnXSA9IHR5cGUgYXMgTWV0cmljVHlwZTtcblxuICBmb3IgKGNvbnN0IGZpZWxkIG9mIFsnc2luY2UnLCAndW50aWwnXSBhcyBjb25zdCkge1xuICAgIGlmICh0eXBlb2YgcXVlcnlbZmllbGRdID09PSAnc3RyaW5nJyAmJiBxdWVyeVtmaWVsZF0pIG9wdGlvbnNbZmllbGRdID0gcXVlcnlbZmllbGRdO1xuICB9XG4gIGlmICh0eXBlb2YgcXVlcnlbJ2xhdGVzdCddID09PSAnc3RyaW5nJykge1xuICAgIG9wdGlvbnNbJ2xhdGVzdCddID0gcXVlcnlbJ2xhdGVzdCddICE9PSAnZmFsc2UnO1xuICB9XG4gIGZvciAoY29uc3QgZmllbGQgb2YgWydsaW1pdCcsICdvZmZzZXQnXSBhcyBjb25zdCkge1xuICAgIGlmICh0eXBlb2YgcXVlcnlbZmllbGRdID09PSAnc3RyaW5nJykge1xuICAgICAgY29uc3QgcGFyc2VkID0gTnVtYmVyLnBhcnNlSW50KHF1ZXJ5W2ZpZWxkXSwgMTApO1xuICAgICAgaWYgKCFOdW1iZXIuaXNOYU4ocGFyc2VkKSkgb3B0aW9uc1tmaWVsZF0gPSBwYXJzZWQ7XG4gICAgfVxuICB9XG4gIHJldHVybiBvcHRpb25zO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlTWV0cmljc1JvdXRlcyhtZXRyaWNzU2luazogTWVtb3J5TWV0cmljc1NpbmspOiBNZXRyaWNzUm91dGVzUmVzdWx0IHtcbiAgY29uc3Qgcm91dGVyID0gUm91dGVyKCk7XG4gIGNvbnN0IGNsaWVudHMgPSBuZXcgU2V0PFNTRUNsaWVudD4oKTtcblxuICAvLyBHRVQgL2FwaS9tZXRyaWNzIOKAlCBKU09OIHF1ZXJ5XG4gIHJvdXRlci5nZXQoJy9tZXRyaWNzJywgKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSkgPT4ge1xuICAgIGNvbnN0IHJlc3VsdCA9IG1ldHJpY3NTaW5rLnF1ZXJ5KHBhcnNlTWV0cmljc1F1ZXJ5T3B0aW9ucyhyZXEucXVlcnkpKTtcbiAgICByZXMuanNvbihyZXN1bHQpO1xuICB9KTtcblxuICAvLyBHRVQgL2FwaS9tZXRyaWNzL3N0cmVhbSDigJQgU1NFIGVuZHBvaW50IGZvciByZWFsLXRpbWUgbWV0cmljIHNuYXBzaG90cy5cbiAgLy8gQ3VycmVudGx5IHVudXNlZCBieSB0aGUgYnVpbHQtaW4gd2ViIGNvbnNvbGUgKHdoaWNoIHBvbGxzIC9hcGkvbWV0cmljcykuXG4gIC8vIFJldGFpbmVkIGZvciB0aGlyZC1wYXJ0eSBjb25zdW1lcnMgKFByb21ldGhldXMsIEdyYWZhbmEgYWdlbnQsIGN1c3RvbSBkYXNoYm9hcmRzKVxuICAvLyB0aGF0IGJlbmVmaXQgZnJvbSBwdXNoLWJhc2VkIG1ldHJpYyBkZWxpdmVyeS5cbiAgcm91dGVyLmdldCgnL21ldHJpY3Mvc3RyZWFtJywgKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSkgPT4ge1xuICAgIHJlcy53cml0ZUhlYWQoMjAwLCB7XG4gICAgICAnQ29udGVudC1UeXBlJzogJ3RleHQvZXZlbnQtc3RyZWFtJyxcbiAgICAgICdDYWNoZS1Db250cm9sJzogJ25vLWNhY2hlJyxcbiAgICAgICdDb25uZWN0aW9uJzogJ2tlZXAtYWxpdmUnLFxuICAgIH0pO1xuICAgIHJlcy53cml0ZSgnOmNvbm5lY3RlZFxcblxcbicpO1xuXG4gICAgY29uc3QgY2xpZW50OiBTU0VDbGllbnQgPSB7IHJlcyB9O1xuICAgIGNsaWVudHMuYWRkKGNsaWVudCk7XG5cbiAgICAvLyBLZWVwLWFsaXZlIGhlYXJ0YmVhdCDigJQgcHJldmVudHMgcHJveGllcyBmcm9tIGNsb3NpbmcgaWRsZSBjb25uZWN0aW9uc1xuICAgIGNvbnN0IGhlYXJ0YmVhdCA9IHNldEludGVydmFsKCgpID0+IHtcbiAgICAgIHJlcy53cml0ZSgnOiBoZWFydGJlYXRcXG5cXG4nKTtcbiAgICB9LCAzMF8wMDApO1xuXG4gICAgcmVxLm9uKCdjbG9zZScsICgpID0+IHtcbiAgICAgIGNsZWFySW50ZXJ2YWwoaGVhcnRiZWF0KTtcbiAgICAgIGNsaWVudHMuZGVsZXRlKGNsaWVudCk7XG4gICAgfSk7XG4gIH0pO1xuXG4gIGZ1bmN0aW9uIG9uU25hcHNob3Qoc25hcHNob3Q6IE1ldHJpY1NuYXBzaG90KTogdm9pZCB7XG4gICAgY29uc3QgZGF0YSA9IEpTT04uc3RyaW5naWZ5KHNuYXBzaG90KTtcbiAgICBmb3IgKGNvbnN0IGNsaWVudCBvZiBjbGllbnRzKSB7XG4gICAgICBpZiAoY2xpZW50LnJlcy5kZXN0cm95ZWQpIHsgY2xpZW50cy5kZWxldGUoY2xpZW50KTsgY29udGludWU7IH1cbiAgICAgIGNsaWVudC5yZXMud3JpdGUoYGRhdGE6ICR7ZGF0YX1cXG5cXG5gKTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4ge1xuICAgIHJvdXRlcixcbiAgICBvblNuYXBzaG90LFxuICAgIGNsaWVudENvdW50OiAoKSA9PiBjbGllbnRzLnNpemUsXG4gIH07XG59XG4iXX0=