@barivia/barsom-mcp 0.17.0 → 0.18.0

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
@@ -43,7 +43,7 @@ MCP clients typically run it with **`npx`** (downloads on first use):
43
43
  | `BARIVIA_API_URL` | No | `https://api.barivia.se` | API base URL |
44
44
  | `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. |
45
45
  | `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). |
46
- | `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`; set a fixed port if you need stable bookmark URLs. |
46
+ | `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. |
47
47
  | `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`. |
48
48
 
49
49
  Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also accepted as fallbacks.
@@ -182,6 +182,10 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
182
182
 
183
183
  ### Migration notes
184
184
 
185
+ - **Fixes (0.17.1, non-breaking):**
186
+ - `datasets(action=list/get)` no longer prints `ingest_error=missing` for healthy datasets (a SQL `NULL` was serialized as the literal `"missing"`; fixed server-side, and the proxy now also ignores the `missing`/`null` sentinels).
187
+ - `file_path` upload help now states that **relative** paths resolve against the MCP **workspace root** — in Cursor/IDE clients that root is often the MCP install dir, so set `BARIVIA_WORKSPACE_ROOT` (or pass an absolute path) if a relative path is "not accessible".
188
+ - Preprocessing failures now name the **offending column(s)** (e.g. missing cells in a `train(action=map)` column) instead of a generic error.
185
189
  - **Surface cleanup (0.17.0, breaking):**
186
190
  - `train(action=baseline_study)` **removed** — use `train(action=map, preset=quick)` (the `baseline_explore` preset in `training_guidance`).
187
191
  - `results(action=transition_flow)` **removed** — it is now **`inference(action=transition_flow)`** (a frozen-map operation). Read the resulting figure with `results(action=get/download)` on the returned job_id.
@@ -224,6 +228,8 @@ MCP Client (Cursor/Claude) ←stdio→ @barivia/barsom-mcp ←HTTPS→ api.bariv
224
228
  | `Path must be within the workspace` / upload can’t find file | Set `BARIVIA_WORKSPACE_ROOT` to your project directory, or use an absolute path / `file:///...` URI. |
225
229
  | Job stuck “running” | Poll `jobs(action=status)` every 10–15s; large grids or FLooP-SIOM can take several minutes—not an MCP error. Staged datasets enqueue **`prepare_training_matrix`** (or impute prepare) first — train submit returns `prepare_job_id`; MCP auto-polls prepare when present. CFD mesh submit may return `prepare_job_id` for **`cfd_prepare`** (barmesh auto-polls). |
226
230
  | `429` | Rate limit—wait and retry. |
231
+ | Standalone `…/viz/…` page stuck at "running 0%" or "Loading…" | The link is stale: the viz port is assigned per MCP session and changes when the proxy restarts. Re-run `training_monitor` / `results_explorer` for a fresh URL, or set `BARIVIA_VIZ_PORT` for a persistent port. Confirm with `GET /api/health?job_id=<id>`. |
232
+ | Embedded results explorer: switching figures shows nothing | Fixed in `0.18.0` — earlier versions pointed embedded figures at a localhost URL the webview can't load. Update the package; the embedded view now lazy-loads figures over the MCP App bridge. |
227
233
  | Malformed MCP / client errors | Ensure nothing writes to **stdout** except MCP JSON-RPC (the proxy logs API traffic to **stderr** only). |
228
234
 
229
235
  ## Development
package/dist/shared.js CHANGED
@@ -26,7 +26,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
26
26
  * X-Barsom-Client-Version so the server can annotate tool guidance with the
27
27
  * wrapper version each action requires. Keep in sync with package.json on bump.
28
28
  */
29
- export const CLIENT_VERSION = "0.17.0";
29
+ export const CLIENT_VERSION = "0.18.0";
30
30
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
31
31
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
32
32
  /** Self-serve account dashboard (manage plan, billing, and API keys). */
@@ -6,6 +6,17 @@ import { z } from "zod";
6
6
  import { registerAuditedTool } from "../audit.js";
7
7
  import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, POLL_DERIVE_MAX_MS, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, } from "../shared.js";
8
8
  import { GZIP_UPLOAD_HINT } from "../job_status_format.js";
9
+ /**
10
+ * Normalize a nullable string field from the API. Returns "" for absent values,
11
+ * empty strings, and the literal SQL/serialization sentinels "missing"/"null"
12
+ * (older backends serialized a NULL ingest_error as the string "missing").
13
+ */
14
+ function cleanNullable(v) {
15
+ if (v == null)
16
+ return "";
17
+ const s = String(v).trim();
18
+ return s === "" || s.toLowerCase() === "missing" || s.toLowerCase() === "null" ? "" : s;
19
+ }
9
20
  /**
10
21
  * Format a dataset analyze result (from either the async job summary or the
11
22
  * legacy sync GET response) into the human-readable text block.
@@ -165,7 +176,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
165
176
  .enum(["upload", "preview", "analyze", "list", "get", "subset", "delete", "add_expression", "reduce_spectral"])
166
177
  .describe("upload: add CSV or .csv.gz; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; get: fetch one dataset metadata (status/staging); subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression (or, with project_onto_job, project the expression onto a trained map as a component plane); reduce_spectral: collapse a long ordered numeric block (e.g. spectrum, time series) into a small per-row feature set via PCA / log_sample / uniform_sample / stats"),
167
178
  name: z.string().optional().describe("Dataset name (required for action=upload and subset)"),
168
- file_path: z.string().optional().describe("Path to local CSV or .csv.gz (PREFERRED): absolute path, file:// URI, or relative to workspace root. Token-efficient; server reads file. Use .csv.gz for large scientific tables."),
179
+ file_path: z.string().optional().describe("Path to local CSV or .csv.gz (PREFERRED): absolute path, file:// URI, or path relative to the workspace root. Token-efficient; server reads file. NOTE: relative paths resolve against the MCP workspace root — in Cursor/IDE clients that root is often the MCP install dir, not your project, so set BARIVIA_WORKSPACE_ROOT in the MCP config env (or just pass an absolute path) if a relative path is 'not accessible'. Use .csv.gz for large scientific tables."),
169
180
  csv_data: z.string().optional().describe("Inline CSV string for small pastes only (<10KB). Avoid for large files — use file_path instead to avoid token explosion."),
170
181
  dataset_id: z.string().optional().describe("Dataset ID (required for preview, get, subset, and delete)"),
171
182
  n_rows: z.number().int().optional().default(5).describe("Sample rows to return (preview only)"),
@@ -632,9 +643,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
632
643
  const cols = ds.cols != null ? Number(ds.cols) : "?";
633
644
  const st = ds.status != null ? String(ds.status) : "ready";
634
645
  const statusBit = st !== "ready" ? ` | status=${st}` : "";
635
- const err = ds.ingest_error != null && String(ds.ingest_error) !== ""
636
- ? ` | ingest_error=${String(ds.ingest_error)}`
637
- : "";
646
+ const ingestErr = cleanNullable(ds.ingest_error);
647
+ const err = ingestErr ? ` | ingest_error=${ingestErr}` : "";
638
648
  return `${name} (${id}) — ${rows}×${cols}${statusBit}${err}`;
639
649
  });
640
650
  return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No datasets." }] };
@@ -653,9 +663,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
653
663
  ds.staged_prefix != null ? `Staged prefix: ${String(ds.staged_prefix)}` : "",
654
664
  ds.staged_version != null ? `Staged version: ${String(ds.staged_version)}` : "",
655
665
  ds.stage_job_id != null ? `Stage job: ${String(ds.stage_job_id)} (poll jobs(action=status))` : "",
656
- ds.ingest_error != null && String(ds.ingest_error) !== ""
657
- ? `Ingest error: ${String(ds.ingest_error)}`
658
- : "",
666
+ cleanNullable(ds.ingest_error) ? `Ingest error: ${cleanNullable(ds.ingest_error)}` : "",
659
667
  ds.created_at != null ? `Created: ${String(ds.created_at)}` : "",
660
668
  ].filter(Boolean);
661
669
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -26,7 +26,12 @@ function buildAvailableFigures(jobId, summary) {
26
26
  const files = (summary.files ?? []).filter((file) => /\.(png|pdf|svg)$/i.test(file));
27
27
  const fallback = getResultsImagesToFetch(jobType, summary, "default", false);
28
28
  const figureFiles = files.length > 0 ? files : fallback;
29
- const port = getVizPort();
29
+ // NOTE: this payload feeds the *embedded* MCP App view, which runs in a
30
+ // sandboxed webview that cannot load a localhost http asset. So we never set
31
+ // a `url` here — the App lazy-loads each raster as base64 through the
32
+ // `_fetch_figure` server-tool bridge (the default figure also arrives as a
33
+ // base64 image via the tool result). The standalone browser page builds its
34
+ // own relative-URL payload separately (see views/results-explorer/app.ts).
30
35
  return figureFiles.map((filename) => {
31
36
  const base = filename.replace(/\.(png|pdf|svg)$/i, "");
32
37
  const featureName = base.startsWith("component_")
@@ -40,7 +45,6 @@ function buildAvailableFigures(jobId, summary) {
40
45
  kind: base.startsWith("component_") ? "component" : base === "combined" ? "summary" : "diagnostic",
41
46
  featureName,
42
47
  caption: getCaptionForImage(filename) || (!isRaster ? "This figure is available for download, but it is not inline-displayable in this first iteration." : undefined),
43
- url: port && isRaster ? `http://localhost:${port}/api/results/${jobId}/image/${filename}` : undefined,
44
48
  };
45
49
  });
46
50
  }
@@ -96,7 +100,7 @@ function buildMetrics(summary) {
96
100
  { label: "Features", value: features },
97
101
  ];
98
102
  }
99
- function buildPayload(jobId, data) {
103
+ export function buildPayload(jobId, data) {
100
104
  const summary = (data.summary ?? {});
101
105
  const figures = buildAvailableFigures(jobId, summary);
102
106
  const grid = summary.grid ?? [];
@@ -117,9 +121,7 @@ function buildPayload(jobId, data) {
117
121
  metrics,
118
122
  highlights,
119
123
  availableFigures: figures,
120
- defaultFigureKey: figures.find((figure) => figure.key === "combined" && figure.url)?.key
121
- ?? figures.find((figure) => Boolean(figure.url))?.key
122
- ?? figures.find((figure) => figure.key === "combined")?.key
124
+ defaultFigureKey: figures.find((figure) => figure.key === "combined")?.key
123
125
  ?? figures[0]?.key,
124
126
  standaloneUrl: port ? `http://localhost:${port}/viz/results-explorer?mode=standalone&job_id=${jobId}` : undefined,
125
127
  };
@@ -150,7 +152,9 @@ async function handleResultsExplorer(job_id) {
150
152
  if (!getClientSupportsMcpApps() && payload.standaloneUrl) {
151
153
  content.push({
152
154
  type: "text",
153
- text: `Interactive visualization: ${payload.standaloneUrl}\nOpen this URL in your browser for a richer results exploration experience.`,
155
+ text: `Interactive visualization: ${payload.standaloneUrl}\n` +
156
+ `Open this URL in your browser for a richer results exploration experience. ` +
157
+ `This localhost port is assigned per MCP session and changes if the proxy restarts — if the page won't load, re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.`,
154
158
  });
155
159
  }
156
160
  return structuredTextResult(payload, undefined, content);
@@ -54,7 +54,7 @@ async function enrichWithTrainingLog(job_id, data) {
54
54
  export function registerTrainingMonitorTool(server) {
55
55
  registerAppTool(server, "training_monitor", {
56
56
  title: "Training Monitor",
57
- description: "Optional embedded or standalone view of training progress for a job_id. When embedded in a client that supports MCP Apps, the panel auto-refreshes every 5s until the job completes: live QE/TE learning curves plus a per-epoch hit-distribution heatmap (sampled BMU counts across the map, showing where observations are landing as training proceeds). A standalone browser URL is also available. The same scalar fields are available via jobs(action=status)—not required to complete training or read results.",
57
+ description: "Optional embedded or standalone view of training progress for a job_id. When embedded in a client that supports MCP Apps, the panel auto-refreshes every 5s until the job completes: live QE/TE learning curves plus a per-epoch hit-distribution heatmap (sampled BMU counts across the map, showing where observations are landing as training proceeds). A standalone browser URL is also available — its localhost port is per MCP session and goes stale if the proxy restarts (set BARIVIA_VIZ_PORT for a persistent port; re-run this tool for a fresh link). The same scalar fields are available via jobs(action=status)—not required to complete training or read results.",
58
58
  inputSchema: {
59
59
  job_id: z.string().describe("Training job ID to monitor"),
60
60
  fetch_training_log: z
@@ -96,7 +96,8 @@ export function registerTrainingMonitorTool(server) {
96
96
  if (!getClientSupportsMcpApps() && standaloneUrl) {
97
97
  content.push({
98
98
  type: "text",
99
- text: `Open in browser: ${standaloneUrl}`,
99
+ text: `Open in browser (best kept open during training): ${standaloneUrl}\n` +
100
+ `This localhost port is assigned per MCP session and changes if the proxy restarts — if the page stays at "running 0%" or stops updating, the link is stale: re-run training_monitor for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port. Health check: http://localhost:${port}/api/health?job_id=${encodeURIComponent(job_id)}`,
100
101
  });
101
102
  }
102
103
  return structuredTextResult(structuredContent, text, content);