@barivia/barsom-mcp 0.22.4 → 0.23.2

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 CHANGED
@@ -54,7 +54,8 @@ MCP clients typically run it with **`npx`** (downloads on first use):
54
54
  | `BARIVIA_API_URL` | No | `https://api.barivia.se` | API base URL |
55
55
  | `BARIVIA_WORKSPACE_ROOT` | No | `process.cwd()` or `PWD` | Directory for relative `file_path` and `save_to_disk`. In Cursor MCP, `process.cwd()` is often the MCP install dir — add `BARIVIA_WORKSPACE_ROOT` to your MCP config `env` with your project path (e.g. `/home/user/myproject`). Absolute paths and `file://` URIs work without it. |
56
56
  | `BARIVIA_FETCH_TIMEOUT_MS` | No | `60000` | Per-request HTTP timeout (ms) to the API. Increase (e.g. `120000`) on slow networks or when the API does long-running work. Large dataset uploads use a separate longer timeout internally, and `datasets(analyze)` runs asynchronously (auto-polled) so it is not bound by this. A timeout is not auto-retried (re-firing slow work would only multiply load / risk duplicates). |
57
- | `BARIVIA_VIZ_PORT` | No | OS-assigned | When the client has no MCP Apps support, the proxy may start a local viz server on `127.0.0.1`. The port is **assigned per MCP session and changes when the proxy restarts**, so standalone `…/viz/…` URLs from a previous session go stale (the page then can't reach `/api/*` and shows a "stale session — re-run the tool" message). Set a fixed port for stable bookmark URLs. Diagnostic: `GET http://127.0.0.1:<port>/api/health?job_id=<id>` reports the port, API reachability, and the job's status. |
57
+ | `BARIVIA_VIZ_PORT` | No | OS-assigned | Local viz server on `127.0.0.1`. Port is **per MCP session** unless pinned standalone `…/viz/…` URLs go stale after proxy restart. Set a fixed port for stable bookmark URLs. Diagnostic: `GET http://127.0.0.1:<port>/api/health?job_id=<id>`. |
58
+ | `BARIVIA_UI_DELIVERY` | No | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline`. Tool responses always lead with a standalone localhost URL (hosts may list `ui://` without mounting widgets) and include a structured `ui_delivery` block. `account(status)` reports detected Apps support, viz port, and active tier. |
58
59
  | `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | No | `1` (enabled) | File uploads are constrained to the MCP workspace root by default. Set to `0` or `false` to allow absolute paths anywhere on the machine (high trust). **Changed in v0.x:** previously defaulted to off; now on for security. If uploads fail with "paths outside the workspace are disabled", either set `BARIVIA_WORKSPACE_ROOT` to cover your data directory, or opt out with `BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0`. |
59
60
 
60
61
  Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also accepted as fallbacks.
@@ -67,7 +68,7 @@ Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also ac
67
68
 
68
69
  **Supported stack:** Node **18+** (see `engines` in `package.json`). Built with `@modelcontextprotocol/sdk` **^1.x** — if a major MCP client upgrade breaks tools, check SDK release notes alongside this package version.
69
70
 
70
- **Local viz fallback:** If the IDE does not advertise MCP Apps, the proxy starts an HTTP server bound to **127.0.0.1** only (not exposed to the LAN). It serves built-in HTML for training prep / monitor / results explorer and proxies read-only API calls with your existing API key. Responses may include `http://127.0.0.1:<port>/viz/...` links. Browser `Access-Control-Allow-Origin: *` applies only to that localhost origin so the embedded pages can load data.
71
+ **Local viz fallback:** The proxy starts an HTTP server bound to **127.0.0.1** only (not exposed to the LAN). It serves built-in HTML for training monitor / results explorer and proxies read-only API calls with your existing API key. **`training_monitor` / `results_explorer` always lead with a standalone `http://127.0.0.1:<port>/viz/...` URL** (even when the host advertises MCP Apps — some Cursor builds list `ui://` but never mount widgets). Browser `Access-Control-Allow-Origin: *` applies only to that localhost origin so the embedded pages can load data.
71
72
 
72
73
  ## Tools and prompts (15 tools + 2 prompts)
73
74
 
@@ -102,7 +103,7 @@ Call at the **start of mapping work** (or when the user asks what the MCP can do
102
103
  | `analyze` | Pre-training recommendations (correlation, columns to consider dropping, etc.) |
103
104
  | `list` | Finding dataset IDs |
104
105
  | `get` | One dataset by id — status, staging fields, `stage_job_id`, ingest errors |
105
- | `subset` | Creating a filtered/sliced copy (row_range, filter conditions) |
106
+ | `subset` | Creating a filtered/sliced copy (row_range, filter, `sample_n`) — **only when the user asks**; agents must not auto-downsample before train/analyze |
106
107
  | `add_expression` | Add a derived column from an expression (formula → new column on the dataset) |
107
108
  | `reduce_spectral` | Pre-training reducer for long ordered numeric blocks (spectra, time series, sensor fingerprints, gene panels). Methods: **pca** (top-k principal components), **log_sample** (k columns at log-spaced indices — scattering, audio bands), **uniform_sample** (k columns at evenly-spaced indices — regularly-sampled time series), **stats** (6 fixed per-row summary columns). All produce one feature vector per row; appends derived columns to the dataset. |
108
109
  | `delete` | Removing a dataset |
@@ -172,11 +173,11 @@ Capacity is shared LOCAL/GKE worker pools (no per-key cloud burst lease).
172
173
 
173
174
  | Tool | Role |
174
175
  |------|------|
175
- | `training_monitor` | Live QE + **panel** topographic-error curves (ordering/convergence, dual y-axis); **Panel TE** and **Map TE** scalars at kernel end; FLooP step progress when applicable; optional `jobs(action=status)` is enough |
176
- | `results_explorer` | Browse metrics and figures after training completes |
176
+ | `training_monitor` | Live QE + **panel** TE curves; auto-opened from `train()` submit when the host mounts Apps; cross-link to results explorer when finalize completes; always returns a standalone URL + `ui_delivery` |
177
+ | `results_explorer` | Browse metrics and figures after training completes; cross-link back to training monitor; always returns a standalone URL + `ui_delivery` |
177
178
  | `_fetch_figure` | **Host / App only** — Results Explorer invokes this for one raster figure; not for agent chat |
178
179
 
179
- When the client does not advertise MCP Apps support, the proxy starts **`viz-server`** on **`127.0.0.1`** (localhost-only). Tool responses can include `http://127.0.0.1:PORT/viz/...` links. See **Local viz fallback** under Environment Variables.
180
+ Standalone localhost URLs are always first in tool content (see `BARIVIA_UI_DELIVERY` / **Local viz fallback**). Pages cross-link monitor results for the same `job_id`.
180
181
 
181
182
  #### Choosing where to view results
182
183
 
@@ -190,6 +191,7 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
190
191
 
191
192
  ### Migration notes
192
193
 
194
+ - **UI delivery + train auto-open (0.22.4+):** `train()` returns `training_monitor` App structured content so supporting hosts open the monitor in the same turn. App tools emit structured `ui_delivery` (`delivery`, `urls`, `hint_for_agent`) and always put the standalone viz URL first. Env: `BARIVIA_UI_DELIVERY`, `BARIVIA_VIZ_PORT`. Cyclic cos/sin component PNGs are omitted from default `summary.files` / `figures=all` (encoding for training; panels still appear in `combined` / recolor); opt in with train param `viz_upload_cyclic_components=true`.
193
195
  - **Account surface (0.22.0, breaking):** `account(request_compute|compute_status|release_compute)` removed — AWS cloud burst leases are gone. Use `account(status|history|add_funds)`. Capacity is shared LOCAL/GKE pools.
194
196
  - **Structured results + train hints (0.21.x, non-breaking):**
195
197
  - **`results(action=get)`** returns **`structuredContent`** (job id/type, label, summary metrics, file list) alongside the text summary — same pattern as `training_monitor` / `results_explorer`. Agent hosts can read fields without parsing prose.
@@ -229,7 +231,7 @@ When adding or refining tools, follow [MCP best practices](https://modelcontextp
229
231
 
230
232
  ## Data preparation
231
233
 
232
- To train on a subset of your data (e.g. first 2000 rows, or rows where region=Europe) without re-uploading: use **datasets(action=subset)** with `row_range` and/or `filter` to create a new dataset, then train with **train(action=map, dataset_id=...)**, **train(action=siom_map, dataset_id=...)**, or **train(action=floop_siom, dataset_id=...)** on the new dataset_id; or pass **row_range** in the training job params for a one-off slice.
234
+ To train on a subset of your data (e.g. first 2000 rows, or rows where region=Europe) without re-uploading: use **datasets(action=subset)** with `row_range` and/or `filter` (or `sample_n`) to create a new dataset, then train with **train(action=map, dataset_id=...)** on the new dataset_id; or pass **row_range** in the training job params for a one-off slice. **Agents must not call subset/`sample_n` unless the user explicitly asks** — train on the full uploaded table by default.
233
235
 
234
236
  ## How It Works
235
237
 
package/dist/audit.js CHANGED
@@ -110,10 +110,12 @@ function stripAuditMeta(result) {
110
110
  function errorCodeFrom(err) {
111
111
  if (err && typeof err === "object") {
112
112
  const e = err;
113
+ if (e.errorCode)
114
+ return e.errorCode;
113
115
  if (e.httpStatus !== undefined)
114
116
  return `http_${e.httpStatus}`;
115
117
  const m = e.message ?? "";
116
- const codeMatch = m.match(/error_code:\s*(\w+)/);
118
+ const codeMatch = m.match(/error_code[=:]\s*(\w+)/);
117
119
  if (codeMatch)
118
120
  return codeMatch[1];
119
121
  }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Local offline queue for send_feedback when the API or object storage is down.
3
+ * Persists under ~/.barivia/deferred-feedback (or BARIVIA_FEEDBACK_QUEUE_DIR).
4
+ * Proxies flush the queue on the next successful feedback submission.
5
+ */
6
+ import fs from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { randomUUID } from "node:crypto";
10
+ export function deferredFeedbackQueueDir() {
11
+ const override = process.env.BARIVIA_FEEDBACK_QUEUE_DIR?.trim();
12
+ if (override)
13
+ return path.resolve(override);
14
+ return path.join(os.homedir(), ".barivia", "deferred-feedback");
15
+ }
16
+ async function ensureQueueDir() {
17
+ const dir = deferredFeedbackQueueDir();
18
+ await fs.mkdir(dir, { recursive: true });
19
+ return dir;
20
+ }
21
+ function entryPath(dir, id) {
22
+ return path.join(dir, `${id}.json`);
23
+ }
24
+ export async function enqueueDeferredFeedback(product, body, lastError) {
25
+ const dir = await ensureQueueDir();
26
+ const id = randomUUID();
27
+ const entry = {
28
+ id,
29
+ product,
30
+ created_at: new Date().toISOString(),
31
+ body,
32
+ ...(lastError ? { last_error: lastError } : {}),
33
+ };
34
+ const filePath = entryPath(dir, id);
35
+ await fs.writeFile(filePath, JSON.stringify(entry, null, 2), "utf8");
36
+ return { id, path: filePath };
37
+ }
38
+ export async function listDeferredFeedback(product) {
39
+ const dir = deferredFeedbackQueueDir();
40
+ let names;
41
+ try {
42
+ names = await fs.readdir(dir);
43
+ }
44
+ catch (err) {
45
+ if (err.code === "ENOENT")
46
+ return [];
47
+ throw err;
48
+ }
49
+ const out = [];
50
+ for (const name of names) {
51
+ if (!name.endsWith(".json"))
52
+ continue;
53
+ try {
54
+ const raw = await fs.readFile(path.join(dir, name), "utf8");
55
+ const entry = JSON.parse(raw);
56
+ if (product && entry.product !== product)
57
+ continue;
58
+ out.push(entry);
59
+ }
60
+ catch {
61
+ /* skip corrupt */
62
+ }
63
+ }
64
+ out.sort((a, b) => a.created_at.localeCompare(b.created_at));
65
+ return out;
66
+ }
67
+ /**
68
+ * Attempt to submit each queued entry via `submit`. Successful entries are deleted.
69
+ * Failures leave the file in place for a later retry.
70
+ */
71
+ export async function flushDeferredFeedback(product, submit) {
72
+ const entries = await listDeferredFeedback(product);
73
+ let flushed = 0;
74
+ const errors = [];
75
+ const dir = deferredFeedbackQueueDir();
76
+ for (const entry of entries) {
77
+ try {
78
+ await submit(entry.body);
79
+ await fs.unlink(entryPath(dir, entry.id)).catch(() => undefined);
80
+ flushed += 1;
81
+ }
82
+ catch (err) {
83
+ errors.push(err instanceof Error ? err.message : String(err));
84
+ }
85
+ }
86
+ const remaining = (await listDeferredFeedback(product)).length;
87
+ return { flushed, remaining, errors };
88
+ }
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as h,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as k}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as I}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const M=new e({name:"analytics-engine",version:m,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list, compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, history, add_funds |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Constraints\n\n- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks for a filter, slice, or random sample. Train and analyze on the uploaded `dataset_id` at full row count — GPU staging/prepare handles hundreds of thousands of rows quickly; silent downsampling hides real prepare/train behavior and wastes scale demos. Use small named fixtures (e.g. sample.csv, periodic_load) only when the user or runbook names them.\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});s(M,w,w,{mimeType:n},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(M,I,I,{mimeType:n},async()=>{const e=await c("training-monitor");return{contents:[{uri:I,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(M),y(M),A(M),f(M),g(M),h(M,_),b(M),v(M),j(M),k(M),P(M),M.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),M.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const T=new t;(async function(){try{const e=await a(l,p,c);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=M.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(n))},await M.connect(T)})().catch(console.error);
2
+ import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as s,registerAppResource as r,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as h}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerExploreMapTool as y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as k}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as I}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:m,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list (slim + limit/cursor/status/job_type/has_results), update (label/description/tags), compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs. Results catalog = `jobs(list, has_results=true)` |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, history, **inventory** (markdown datasets+jobs overview), add_funds, **health** (API liveness via GET /health — no R2) |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees; queues locally if API/storage is down |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Org inventory recipe\n\nTo rebuild a durable markdown overview of an org (datasets + jobs):\n1. `account(action=inventory)` — preferred one-shot (datasets name/id/rows/cols/description/tags + slim recent jobs).\n2. Or compose: `datasets(list)` + `jobs(action=list, limit=50)` (page with `cursor` / filter with `status` / `job_type` / `has_results=true` for a results catalog).\n3. Set `description` + `tags` on `datasets(upload|update)` and `train(...)` / `jobs(update)` so duplicates stay navigable.\n\n## API briefly unavailable (pause, do not crash)\nIf tools return `api_unreachable` / gateway HTML / slim `error_code` lines: summarize in **one calm sentence** — never paste Cloudflare HTML. The proxy already burst-retried (**3** tries, ~**2s** apart); honor `retry_after_sec` (~**30**) before another round. Use `account(action=health)` (no R2). Local downloads and prior results stay usable; `send_feedback` queues under `~/.barivia/deferred-feedback` and flushes when the API returns. Do not start large uploads/trains until `account(status)` or health looks healthy again.\n\n## Constraints\n\n- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks. On CPU, `preset=quick` (15×15) is typically very fast up to ~100k rows — do **not** downsample by default when exploring below 100k; only suggest `sample_n` when n>100k or the user asks. Train/analyze the full `dataset_id`. Use small named fixtures only when the user or runbook names them.\n- **Small-n before train:** If preview/analyze shows n<1000 (especially n<100), heed the JUMPBACK/warning — omit `grid_x`/`grid_y` for auto (~sqrt(5·√n)), avoid oversized presets on tiny tables, drop redundant correlated geo features, and check `active_node_fraction`/TE before narrating map insights.\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});r(T,w,w,{mimeType:n},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),r(T,I,I,{mimeType:n},async()=>{const e=await c("training-monitor");return{contents:[{uri:I,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),k(T),y(T),A(T),f(T),h(T),g(T,b),_(T),v(T),j(T),x(T),P(T),T.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset below ~100k rows (CPU quick 15×15 is fast); never call `datasets(subset)` / `sample_n` unless the user asks or n>100k exploratory. Small-n (<1000): prefer auto grid, not oversized presets. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),T.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const M=new t;(async function(){try{const e=await a(l,p,c);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=s(t);d(!!o?.mimeTypes?.includes(n))},await T.connect(M)})().catch(console.error);
@@ -0,0 +1,69 @@
1
+ /** Shared markdown inventory formatting for account(inventory) / jobs list. */
2
+ export function formatTags(tags) {
3
+ if (!Array.isArray(tags) || tags.length === 0)
4
+ return "";
5
+ return tags.map((t) => String(t)).filter(Boolean).join(", ");
6
+ }
7
+ export function formatDatasetInventoryLine(ds) {
8
+ const id = String(ds.id ?? "");
9
+ const name = String(ds.name ?? "");
10
+ const rows = ds.rows != null ? Number(ds.rows) : "?";
11
+ const cols = ds.cols != null ? Number(ds.cols) : "?";
12
+ const st = ds.status != null ? String(ds.status) : "ready";
13
+ const statusBit = st !== "ready" ? ` | status=${st}` : "";
14
+ const desc = ds.description != null && String(ds.description).trim() !== ""
15
+ ? ` — ${String(ds.description).trim()}`
16
+ : "";
17
+ const tags = formatTags(ds.tags);
18
+ const tagsBit = tags ? ` [${tags}]` : "";
19
+ return `${name} (${id}) — ${rows}×${cols}${statusBit}${tagsBit}${desc}`;
20
+ }
21
+ export function formatJobInventoryLine(job) {
22
+ const id = String(job.id ?? "");
23
+ const st = String(job.status ?? "");
24
+ const label = job.label != null && job.label !== "" ? String(job.label) : null;
25
+ const jt = job.job_type != null ? String(job.job_type) : "?";
26
+ const ds = job.dataset_id != null ? String(job.dataset_id) : "";
27
+ const created = job.created_at != null ? String(job.created_at) : "";
28
+ const result = job.result_ref != null && String(job.result_ref) !== "" ? "yes" : "no";
29
+ const tags = formatTags(job.tags);
30
+ const tagsBit = tags ? ` [${tags}]` : "";
31
+ const desc = job.description != null && String(job.description).trim() !== ""
32
+ ? ` — ${String(job.description).trim()}`
33
+ : "";
34
+ const head = label ? `${label} (id: ${id})` : `id: ${id}`;
35
+ return `${head} — ${st} | type=${jt} | dataset=${ds} | created=${created} | result=${result}${tagsBit}${desc}`;
36
+ }
37
+ export function extractJobsList(data) {
38
+ if (Array.isArray(data)) {
39
+ return { jobs: data, next_cursor: null };
40
+ }
41
+ if (data && typeof data === "object") {
42
+ const obj = data;
43
+ const jobs = Array.isArray(obj.jobs) ? obj.jobs : [];
44
+ const next = obj.next_cursor != null && String(obj.next_cursor) !== ""
45
+ ? String(obj.next_cursor)
46
+ : null;
47
+ return { jobs, next_cursor: next };
48
+ }
49
+ return { jobs: [], next_cursor: null };
50
+ }
51
+ export function buildJobsListQuery(opts) {
52
+ const qs = new URLSearchParams();
53
+ if (opts.dataset_id)
54
+ qs.set("dataset_id", opts.dataset_id);
55
+ if (opts.status)
56
+ qs.set("status", opts.status);
57
+ if (opts.job_type)
58
+ qs.set("job_type", opts.job_type);
59
+ if (opts.has_results)
60
+ qs.set("has_results", "true");
61
+ if (opts.limit != null)
62
+ qs.set("limit", String(opts.limit));
63
+ if (opts.cursor)
64
+ qs.set("cursor", opts.cursor);
65
+ if (opts.fields)
66
+ qs.set("fields", opts.fields);
67
+ const s = qs.toString();
68
+ return s ? `/v1/jobs?${s}` : "/v1/jobs";
69
+ }
@@ -1,8 +1,9 @@
1
1
  import { apiCall } from "./shared.js";
2
- export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07-offline";
2
+ export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07f-offline";
3
3
  const FALLBACK_LINES = [
4
4
  "Before upload: for train(action=map) ensure training columns have no NaNs; for train(action=impute) missing cells in training columns are OK (plain numeric only).",
5
- "Do not auto-subset: never call datasets(subset) or sample_n unless the user explicitly asks — train on the full uploaded dataset_id.",
5
+ "Do not auto-subset: never call datasets(subset) or sample_n unless the user explicitly asks — on CPU, preset=quick 15×15 is typically very fast up to ~100k rows; only suggest sample_n when n>100k or the user asks. Train on the full uploaded dataset_id.",
6
+ "Small-n: if preview/analyze shows n<1000 (especially n<100), omit grid_x/grid_y for auto (~sqrt(5·√n)); avoid preset=quick/standard on tiny tables (dead nodes / TE≈1). Drop redundant correlated geo features before train.",
6
7
  'Please run datasets(action=preview, dataset_id="{id}") to inspect columns, then datasets(action=analyze, dataset_id="{id}") to see which columns and temporal periods are most informative.',
7
8
  "Choose the training path:",
8
9
  '- train(action=map, dataset_id="{id}", ...) for complete-case data',