@qe-mcp/server-ena 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -9
- package/package.json +4 -2
- package/server.js +56 -16
package/README.md
CHANGED
|
@@ -217,15 +217,25 @@ These use `ena_plot` with the cached `model_id` — no refitting.
|
|
|
217
217
|
|
|
218
218
|
Each fitting or plotting call returns:
|
|
219
219
|
|
|
220
|
-
-
|
|
221
|
-
- **`interactive_plot`** — path to a self-contained HTML file with the interactive qeviz visualization, openable in any browser.
|
|
222
|
-
- **`
|
|
223
|
-
-
|
|
224
|
-
|
|
225
|
-
The
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
- **`svg_plot`** — path to the rendered SVG (vector; embeddable). Always produced.
|
|
221
|
+
- **`interactive_plot`** — path to a self-contained HTML file with the interactive qeviz visualization, openable in any browser. Always produced.
|
|
222
|
+
- **`png_plot`** — path to the rendered PNG (static raster). Only when the optional PNG rasterizer is installed.
|
|
223
|
+
- **Inline PNG** — an image content block rendered directly in clients that support it (Claude Desktop, Claude web). Only when the optional PNG rasterizer is installed.
|
|
224
|
+
|
|
225
|
+
The file-path outputs let clients that don't render inline image blocks
|
|
226
|
+
(Claude Code, Claude Science) still open or embed the real ENA network
|
|
227
|
+
plot. All files are written to the OS temp dir under `ena-viz/` and
|
|
228
|
+
persist until the next system restart.
|
|
229
|
+
|
|
230
|
+
### Optional PNG rendering
|
|
231
|
+
|
|
232
|
+
PNG output (`png_plot` and the inline image block) is powered by
|
|
233
|
+
[`@resvg/resvg-js`](https://www.npmjs.com/package/@resvg/resvg-js), a
|
|
234
|
+
native module listed under `optionalDependencies`. It is installed
|
|
235
|
+
automatically by `npm install` on supported platforms. When it is absent
|
|
236
|
+
— e.g. a cross-platform bundle built without optional dependencies — the
|
|
237
|
+
server logs a notice and produces `svg_plot` + `interactive_plot` only;
|
|
238
|
+
no error, no crash.
|
|
229
239
|
|
|
230
240
|
---
|
|
231
241
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qe-mcp/server-ena",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "MCP server for Epistemic Network Analysis — WASM backend, zero Python dependency",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
"@modelcontextprotocol/sdk": "^1.4.0",
|
|
15
15
|
"@qe-libs/qeviz": "^0.1.0",
|
|
16
16
|
"@qe-libs/rena-wasm": "^0.1.0",
|
|
17
|
-
"@resvg/resvg-js": "^2.6.2",
|
|
18
17
|
"jsdom": "^25.0.1"
|
|
19
18
|
},
|
|
19
|
+
"optionalDependencies": {
|
|
20
|
+
"@resvg/resvg-js": "^2.6.2"
|
|
21
|
+
},
|
|
20
22
|
"keywords": [
|
|
21
23
|
"mcp",
|
|
22
24
|
"ena",
|
package/server.js
CHANGED
|
@@ -243,7 +243,27 @@ function writeVizFile(content, name, ext = 'html') {
|
|
|
243
243
|
// ── jsdom SVG renderer ────────────────────────────────────────────────────────
|
|
244
244
|
|
|
245
245
|
import { JSDOM } from 'jsdom';
|
|
246
|
-
|
|
246
|
+
|
|
247
|
+
// @resvg/resvg-js is a native (platform-specific) addon and is an OPTIONAL
|
|
248
|
+
// dependency: it powers the inline PNG image block and the png_plot file. When
|
|
249
|
+
// it is absent — e.g. a cross-platform .mcpb bundle built without optional deps,
|
|
250
|
+
// or a platform with no prebuilt binary — the server still produces the SVG and
|
|
251
|
+
// interactive HTML outputs. Loaded lazily so its absence never breaks startup.
|
|
252
|
+
let _Resvg; // undefined = not yet attempted; null = unavailable
|
|
253
|
+
async function getResvg() {
|
|
254
|
+
if (_Resvg === undefined) {
|
|
255
|
+
try {
|
|
256
|
+
({ Resvg: _Resvg } = await import('@resvg/resvg-js'));
|
|
257
|
+
} catch {
|
|
258
|
+
_Resvg = null;
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
'[ena-mcp] @resvg/resvg-js not available — inline PNG and png_plot ' +
|
|
261
|
+
'disabled; svg_plot and interactive_plot are still produced.\n'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return _Resvg;
|
|
266
|
+
}
|
|
247
267
|
|
|
248
268
|
const SVG_SIZE = 600;
|
|
249
269
|
|
|
@@ -283,7 +303,23 @@ async function renderSVG(modelData, opts = {}) {
|
|
|
283
303
|
if (!svgHtml.includes('xmlns=')) {
|
|
284
304
|
svgHtml = svgHtml.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
285
305
|
}
|
|
286
|
-
return svgHtml;
|
|
306
|
+
return withWhiteBackground(svgHtml);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// The qeviz SVG has a transparent background, which makes the plot invisible in
|
|
310
|
+
// dark-mode viewers. Insert an opaque white rect as the first child (behind all
|
|
311
|
+
// content), sized to the SVG's viewBox so it covers the full canvas.
|
|
312
|
+
function withWhiteBackground(svgHtml) {
|
|
313
|
+
const openTagMatch = svgHtml.match(/<svg[^>]*>/);
|
|
314
|
+
if (!openTagMatch) return svgHtml;
|
|
315
|
+
const openTag = openTagMatch[0];
|
|
316
|
+
|
|
317
|
+
const vb = openTag.match(/viewBox\s*=\s*["']\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*["']/);
|
|
318
|
+
const rect = vb
|
|
319
|
+
? `<rect x="${vb[1]}" y="${vb[2]}" width="${vb[3]}" height="${vb[4]}" fill="#ffffff"/>`
|
|
320
|
+
: `<rect x="0" y="0" width="100%" height="100%" fill="#ffffff"/>`;
|
|
321
|
+
|
|
322
|
+
return svgHtml.replace(openTag, openTag + rect);
|
|
287
323
|
}
|
|
288
324
|
|
|
289
325
|
// ── Model / accumulation cache ────────────────────────────────────────────────
|
|
@@ -1057,7 +1093,7 @@ const TOOLS = [
|
|
|
1057
1093
|
'Returns a model_id that can be passed to ena_plot for additional views (group mean, unit, subtraction, comparison) without re-fitting. ' +
|
|
1058
1094
|
'Repeated calls with identical parameters return the cached model instantly. ' +
|
|
1059
1095
|
'Supports rotation="svd" (default) or rotation="mean" (requires group_col, group1_value, group2_value). ' +
|
|
1060
|
-
'
|
|
1096
|
+
'The ENA network plot is written to disk as SVG (svg_plot) and interactive HTML (interactive_plot), and — when the optional PNG rasterizer is installed — also as PNG (png_plot) plus an inline image block. Use these paths to show the plot; do not generate your own visualization. ' +
|
|
1061
1097
|
'PRIVACY: use data_path (file stays local, only model results reach Claude) rather than data_csv (raw data passes through conversation). ' +
|
|
1062
1098
|
'The WASM model runs entirely on the user\'s machine regardless of which input method is used.',
|
|
1063
1099
|
inputSchema: {
|
|
@@ -1092,7 +1128,7 @@ const TOOLS = [
|
|
|
1092
1128
|
'Axis 1 passes through the mean difference between groups. ' +
|
|
1093
1129
|
'Returns a model_id for use with ena_plot (group, subtraction, comparison, unit views). ' +
|
|
1094
1130
|
'Repeated calls with identical parameters return the cached model instantly. ' +
|
|
1095
|
-
'
|
|
1131
|
+
'The ENA network plot is written to disk as SVG (svg_plot) and interactive HTML (interactive_plot), and — when the optional PNG rasterizer is installed — also as PNG (png_plot) plus an inline image block. Use these paths to show the plot; do not generate your own visualization. ' +
|
|
1096
1132
|
'PRIVACY: use data_path (file stays local, only model results reach Claude) rather than data_csv (raw data passes through conversation). ' +
|
|
1097
1133
|
'The WASM model runs entirely on the user\'s machine regardless of which input method is used.',
|
|
1098
1134
|
inputSchema: {
|
|
@@ -1118,7 +1154,7 @@ const TOOLS = [
|
|
|
1118
1154
|
'"comparison" — overlay of two group mean networks (requires group1, group2); ' +
|
|
1119
1155
|
'"subtraction" — difference network g1 − g2 (requires group1, group2, and group_col on the source model); ' +
|
|
1120
1156
|
'"unit" — individual unit network (requires unit_label). ' +
|
|
1121
|
-
'
|
|
1157
|
+
'The ENA network plot is written to disk as SVG (svg_plot) and interactive HTML (interactive_plot), plus PNG (png_plot) and an inline image when the optional PNG rasterizer is installed.',
|
|
1122
1158
|
inputSchema: {
|
|
1123
1159
|
type: 'object',
|
|
1124
1160
|
properties: {
|
|
@@ -1245,19 +1281,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1245
1281
|
try {
|
|
1246
1282
|
svg = await renderSVG(_vizData, _vizOpts ?? {});
|
|
1247
1283
|
if (svg) {
|
|
1248
|
-
|
|
1249
|
-
// Inline image block — rendered by clients that support it (Claude Desktop / web).
|
|
1250
|
-
content.unshift({
|
|
1251
|
-
type: 'image',
|
|
1252
|
-
data: png.toString('base64'),
|
|
1253
|
-
mimeType: 'image/png',
|
|
1254
|
-
});
|
|
1255
|
-
// Also write SVG + PNG files for clients that don't render inline images
|
|
1256
|
-
// (Claude Code, Claude Science) so the real plot can be opened/embedded.
|
|
1284
|
+
// SVG file — always available (pure JS/WASM), vector, embeddable.
|
|
1257
1285
|
try { artifacts.svg_plot = writeVizFile(svg, slug, 'svg'); }
|
|
1258
1286
|
catch (e) { process.stderr.write(`[ena-mcp] SVG write failed: ${e.message}\n`); }
|
|
1259
|
-
|
|
1260
|
-
|
|
1287
|
+
|
|
1288
|
+
// PNG (inline image block + png_plot file) only when the optional
|
|
1289
|
+
// native rasterizer is present. Absent in cross-platform bundles.
|
|
1290
|
+
const Resvg = await getResvg();
|
|
1291
|
+
if (Resvg) {
|
|
1292
|
+
const png = new Resvg(svg, { fitTo: { mode: 'width', value: SVG_SIZE }, background: '#ffffff' }).render().asPng();
|
|
1293
|
+
content.unshift({
|
|
1294
|
+
type: 'image',
|
|
1295
|
+
data: png.toString('base64'),
|
|
1296
|
+
mimeType: 'image/png',
|
|
1297
|
+
});
|
|
1298
|
+
try { artifacts.png_plot = writeVizFile(png, slug, 'png'); }
|
|
1299
|
+
catch (e) { process.stderr.write(`[ena-mcp] PNG write failed: ${e.message}\n`); }
|
|
1300
|
+
}
|
|
1261
1301
|
} else {
|
|
1262
1302
|
renderError = 'renderSVG returned null (qe-graph svg_element not found — jsdom/qeviz may have failed silently)';
|
|
1263
1303
|
}
|