@qe-mcp/server-ena 0.1.4 → 0.1.6
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 +18 -1
- package/manifest.json +60 -0
- package/package.json +1 -1
- package/scripts/build-mcpb.sh +62 -0
- package/server.js +17 -1
package/README.md
CHANGED
|
@@ -30,7 +30,24 @@ Gives Claude a set of ENA tools that run **locally on your machine**. The WASM m
|
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
### Recommended — one-click desktop extension (`.mcpb`)
|
|
34
|
+
|
|
35
|
+
The easiest install, no terminal or config files. Download `server-ena.mcpb`
|
|
36
|
+
from the [releases](https://qe-libs.org/mcp/qe-mcp-server-ena/), then in
|
|
37
|
+
Claude Desktop go to **Settings → Extensions** and drag the file in (or
|
|
38
|
+
double-click it). Click Install and restart. Node.js ships inside Claude
|
|
39
|
+
Desktop, so nothing else is required.
|
|
40
|
+
|
|
41
|
+
The bundle is pure JS/WASM and cross-platform. It produces SVG (`svg_plot`)
|
|
42
|
+
and interactive HTML (`interactive_plot`) plots; inline PNG rendering is
|
|
43
|
+
omitted from the bundle (it requires a native module) — open the SVG or HTML
|
|
44
|
+
to view the network.
|
|
45
|
+
|
|
46
|
+
To build the bundle yourself: `bash scripts/build-mcpb.sh` → `dist/server-ena.mcpb`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
The package is also published to the public npm registry — no registry configuration needed.
|
|
34
51
|
|
|
35
52
|
### Option A — npx (no install needed)
|
|
36
53
|
|
package/manifest.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "0.2",
|
|
3
|
+
"name": "server-ena",
|
|
4
|
+
"display_name": "ENA — Epistemic Network Analysis",
|
|
5
|
+
"version": "0.1.6",
|
|
6
|
+
"description": "Epistemic Network Analysis on tabular coded data. Runs entirely on your machine — data never leaves your computer.",
|
|
7
|
+
"long_description": "Model patterns of co-occurrence between coded events (discourse moves, behaviors, features) within conversations. The full ENA pipeline — accumulate, normalize, rotate, project, node positions — runs locally in WebAssembly; only results and plots are returned. Provides ena_profile (privacy-safe column metadata), ena_inspect (triage), ena_fit, ena_compare_groups (mean-rotation group comparison), ena_plot (rendered network views), and ena_accumulate. Prefer a file path over pasted data so raw rows stay on your machine.",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Cody L Marquart",
|
|
10
|
+
"email": "clmarquart@wisc.edu"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://qe-libs.org/mcp/qe-mcp-server-ena/",
|
|
13
|
+
"documentation": "https://qe-libs.org/mcp/qe-mcp-server-ena/",
|
|
14
|
+
"license": "GPL-3.0-only",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ena",
|
|
17
|
+
"epistemic-network-analysis",
|
|
18
|
+
"quantitative-ethnography",
|
|
19
|
+
"discourse-analysis",
|
|
20
|
+
"learning-analytics",
|
|
21
|
+
"wasm"
|
|
22
|
+
],
|
|
23
|
+
"server": {
|
|
24
|
+
"type": "node",
|
|
25
|
+
"entry_point": "server.js",
|
|
26
|
+
"mcp_config": {
|
|
27
|
+
"command": "node",
|
|
28
|
+
"args": [
|
|
29
|
+
"${__dirname}/server.js"
|
|
30
|
+
],
|
|
31
|
+
"env": {}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"tools": [
|
|
35
|
+
{
|
|
36
|
+
"name": "ena_profile",
|
|
37
|
+
"description": "Privacy-safe column metadata for a local CSV — no raw data transmitted."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "ena_inspect",
|
|
41
|
+
"description": "Triage a dataset for ENA suitability; suggests codes, units, and conversations."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "ena_fit",
|
|
45
|
+
"description": "Fit a full ENA model; returns a reusable model_id and a network plot."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "ena_compare_groups",
|
|
49
|
+
"description": "Mean-rotation comparison of two groups; returns a model_id and plots."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "ena_plot",
|
|
53
|
+
"description": "Render a network view (all, group, unit, comparison, subtraction) from a cached model_id."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "ena_accumulate",
|
|
57
|
+
"description": "Accumulation step only — raw per-unit co-occurrence networks."
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# build-mcpb.sh — Produce a cross-platform .mcpb desktop-extension bundle.
|
|
3
|
+
#
|
|
4
|
+
# Stages only the runtime files plus a pure JS/WASM node_modules (optional deps
|
|
5
|
+
# omitted, so the native @resvg/resvg-js is excluded and the bundle works on any
|
|
6
|
+
# platform). PNG output is disabled in the bundle; SVG + interactive HTML remain.
|
|
7
|
+
#
|
|
8
|
+
# Usage: bash scripts/build-mcpb.sh
|
|
9
|
+
# Output: dist/server-ena.mcpb
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
14
|
+
STAGE="$(mktemp -d)/server-ena"
|
|
15
|
+
OUT="${ROOT}/dist"
|
|
16
|
+
|
|
17
|
+
echo "Staging runtime files → ${STAGE}"
|
|
18
|
+
mkdir -p "${STAGE}" "${OUT}"
|
|
19
|
+
|
|
20
|
+
# Runtime files only (server.js resolves qeviz from node_modules, so the
|
|
21
|
+
# repo-root qeviz-*.js/html dev artifacts and tests are not needed).
|
|
22
|
+
cp "${ROOT}/server.js" "${STAGE}/"
|
|
23
|
+
cp "${ROOT}/manifest.json" "${STAGE}/"
|
|
24
|
+
cp "${ROOT}/package.json" "${STAGE}/"
|
|
25
|
+
|
|
26
|
+
# Keep the bundle's manifest version in lockstep with package.json.
|
|
27
|
+
VERSION="$(node -e "process.stdout.write(require('${ROOT}/package.json').version)")"
|
|
28
|
+
node -e "
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const p = '${STAGE}/manifest.json';
|
|
31
|
+
const m = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
32
|
+
m.version = '${VERSION}';
|
|
33
|
+
fs.writeFileSync(p, JSON.stringify(m, null, 2) + '\n');
|
|
34
|
+
"
|
|
35
|
+
echo "Manifest version synced to ${VERSION}"
|
|
36
|
+
cp "${ROOT}/package-lock.json" "${STAGE}/" 2>/dev/null || true
|
|
37
|
+
[ -f "${ROOT}/README.md" ] && cp "${ROOT}/README.md" "${STAGE}/"
|
|
38
|
+
[ -f "${ROOT}/.npmrc" ] && cp "${ROOT}/.npmrc" "${STAGE}/"
|
|
39
|
+
|
|
40
|
+
echo "Installing production deps without optional (pure JS/WASM)…"
|
|
41
|
+
( cd "${STAGE}" && npm install --omit=optional --omit=dev --no-audit --no-fund --silent )
|
|
42
|
+
|
|
43
|
+
# Drop the .npmrc so the registry token/config isn't shipped in the bundle.
|
|
44
|
+
rm -f "${STAGE}/.npmrc"
|
|
45
|
+
|
|
46
|
+
# Some npm versions (e.g. 9.x) still materialize root-level optionalDependencies
|
|
47
|
+
# despite --omit=optional. Remove the native rasterizer explicitly so the bundle
|
|
48
|
+
# stays pure JS/WASM and cross-platform. The server degrades gracefully without it.
|
|
49
|
+
rm -rf "${STAGE}"/node_modules/@resvg
|
|
50
|
+
|
|
51
|
+
# Sanity: the native rasterizer must NOT be present in the bundle.
|
|
52
|
+
if [ -d "${STAGE}/node_modules/@resvg" ]; then
|
|
53
|
+
echo "ERROR: @resvg present in bundle — not cross-platform. Aborting." >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "Packing bundle…"
|
|
58
|
+
mcpb pack "${STAGE}" "${OUT}/server-ena.mcpb"
|
|
59
|
+
|
|
60
|
+
echo ""
|
|
61
|
+
echo "Built: ${OUT}/server-ena.mcpb"
|
|
62
|
+
mcpb info "${OUT}/server-ena.mcpb" 2>/dev/null | head -20 || true
|
package/server.js
CHANGED
|
@@ -303,7 +303,23 @@ async function renderSVG(modelData, opts = {}) {
|
|
|
303
303
|
if (!svgHtml.includes('xmlns=')) {
|
|
304
304
|
svgHtml = svgHtml.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
305
305
|
}
|
|
306
|
-
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);
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
// ── Model / accumulation cache ────────────────────────────────────────────────
|