@barivia/barsom-mcp 0.23.10 → 0.23.11
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 +4 -3
- package/dist/audit.js +9 -3
- package/dist/index.js +1 -1
- package/dist/job_monitor.js +20 -1
- package/dist/shared.js +46 -9
- package/dist/tools/account.js +1 -1
- package/dist/tools/explore_map.js +1 -0
- package/dist/tools/feedback.js +6 -1
- package/dist/tools/jobs.js +2 -2
- package/dist/tools/results.js +9 -1
- package/dist/tools/train.js +1 -1
- package/dist/tools/training_monitor.js +21 -4
- package/dist/ui-delivery.js +44 -10
- package/dist/views/src/views/results-explorer/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @barivia/barsom-mcp
|
|
2
2
|
|
|
3
|
-
MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. The npm package is **`@barivia/barsom-mcp`**; many configs label the server **`analytics-engine`** (the MCP server name in the client JSON). **`guide_barsom_workflow`** is the canonical bootstrap: it loads plan-scoped workflow text from the Barivia API when online (tool map, async rules, training modes, SOP). If the API is unreachable, it returns a short offline stub. Core tools are `datasets`, `train` (`map`, `siom_map`, `impute`, `floop_siom` where entitled), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/export/download/recolor), and `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report). **Optional MCP App tools** (`training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results, though `results_explorer` is the preferred way to browse figures.
|
|
3
|
+
MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. The npm package is **`@barivia/barsom-mcp`**; many configs label the server **`analytics-engine`** (the MCP server name in the client JSON). **`guide_barsom_workflow`** is the canonical bootstrap: it loads plan-scoped workflow text from the Barivia API when online (tool map, async rules, training modes, SOP). If the API is unreachable, it returns a short **connectivity-only** offline stub. Core tools are `datasets`, `train` (`map`, `siom_map`, `impute`, `floop_siom` where entitled), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/export/download/recolor), and `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report). **Optional MCP App tools** (`training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results, though `results_explorer` is the preferred way to browse figures.
|
|
4
4
|
|
|
5
|
-
**Pre-training help (pick one):** **`prepare_training`** prompt = narrative checklist (tier-scoped from the API when online); **`training_guidance`** tool = structured presets and parameter hints (including
|
|
5
|
+
**Pre-training help (pick one):** **`prepare_training`** prompt = narrative checklist (tier-scoped from the API when online); **`training_guidance`** tool = structured presets, quality/timing heuristics, and parameter hints (including resolved per-column normalization). Review either, then submit with **`train`**. Parameter encyclopedias and quality targets are **API-gated** (valid key + plan); the published proxy catalog keeps short routing text only.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -56,7 +56,8 @@ MCP clients typically run it with **`npx`** (downloads on first use):
|
|
|
56
56
|
| `BARIVIA_WORKSPACE_EXTRA_ROOTS` | No | (empty) | Comma- or colon-separated absolute directories whose **realpath** targets are also allowed for uploads when the sandbox is on. Use for shared fixture trees reached via symlinks under the case workspace (e.g. `…/barivia-testing/data`). Does not weaken the default sandbox for paths outside these roots. |
|
|
57
57
|
| `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). |
|
|
58
58
|
| `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>`. |
|
|
59
|
-
| `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. |
|
|
59
|
+
| `BARIVIA_UI_DELIVERY` | No | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline` \| `text_only`. 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. |
|
|
60
|
+
| `BARIVIA_UI_PREFER_LOCALHOST` | No | (off) | Set `1` to force `localhost` under `auto` even when the host advertises MCP Apps (Cursor list-without-mount workaround). Default `auto` behavior unchanged. |
|
|
60
61
|
| `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | No | `1` (enabled) | File uploads are constrained to the MCP workspace root (and optional `BARIVIA_WORKSPACE_EXTRA_ROOTS`) by default. Set to `0` or `false` to allow absolute paths anywhere on the machine (high trust). If uploads fail with "symlink resolves outside workspace", either point `BARIVIA_WORKSPACE_ROOT` / `BARIVIA_WORKSPACE_EXTRA_ROOTS` at the real data dir, copy the file into the workspace, or opt out with `BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0`. |
|
|
61
62
|
|
|
62
63
|
Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also accepted as fallbacks.
|
package/dist/audit.js
CHANGED
|
@@ -161,14 +161,20 @@ export async function runMcpToolAudit(tool, action, args, handler) {
|
|
|
161
161
|
/**
|
|
162
162
|
* Register an MCP tool with structured audit logging on each invocation.
|
|
163
163
|
*/
|
|
164
|
-
export function registerAuditedTool(server, name, description, schema, handler) {
|
|
165
|
-
|
|
164
|
+
export function registerAuditedTool(server, name, description, schema, handler, annotations) {
|
|
165
|
+
const cb = (async (args) => {
|
|
166
166
|
const rec = args;
|
|
167
167
|
const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
|
|
168
168
|
// One trace per tool invocation: every apiCall inside this handler shares one
|
|
169
169
|
// trace_id, so the API + job chain reconstruct the whole logical action.
|
|
170
170
|
return runWithTrace(() => runMcpToolAudit(name, action, rec, () => handler(args)));
|
|
171
|
-
})
|
|
171
|
+
});
|
|
172
|
+
if (annotations) {
|
|
173
|
+
server.tool(name, description, schema, annotations, cb);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
server.tool(name, description, schema, cb);
|
|
177
|
+
}
|
|
172
178
|
}
|
|
173
179
|
/** Attach audit flags to a tool result (stripped before returning to MCP client). */
|
|
174
180
|
export function withAuditFlags(result, flags) {
|
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 c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as _}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as h}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}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 I}from"./tools/feedback.js";import{registerTrainingMonitorTool as
|
|
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 c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as _}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as h}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}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 I}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as P}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as S}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:d,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics via the Barivia API.\n\n## Bootstrap (do this first)\n\nCall `guide_barsom_workflow` for the plan-scoped tool map, training modes, async rules, and SOP (API when online). Call `training_guidance` / `prepare_training` for parameter and quality hints before guessing knobs.\n\n## Workflow (short)\n\n`datasets(upload)` → preview/analyze → `train(action=...)` (only modes your plan allows) → poll `jobs(status)` every 10–15s → `results(get)` then optional `results_explorer`. Scoring and frozen-map ops live in `inference`.\n\n## Tool taxonomy (one tool per stage)\n\n| Stage | Tool(s) |\n|-------|---------|\n| Data | `datasets` |\n| Train submit | `train` (map / siom_map / impute / floop_siom where entitled) |\n| Lifecycle | `jobs` (status/list/compare/cancel/delete — no train/score) |\n| Artifacts | `results` (get/export/download/recolor) |\n| Frozen-map ops | `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report) |\n| Account | `account` (status/inventory/health) |\n| Bootstrap | `guide_barsom_workflow`, `training_guidance`, `prepare_training` |\n| Explore (UI) | `results_explorer`, `training_monitor` |\n| Feedback | `send_feedback` (after user agrees) |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics across **training runs**. `inference(compare)` = density-diff vs a **second dataset**.\n- Post-training analysis is `inference`, not `results` (presentation/artifacts only).\n- Project columns → `inference(project_columns)`; formula → `datasets(add_expression, project_onto_job=...)`.\n\n## Async & constraints\n\n- Training returns `job_id` immediately — poll `jobs(status)` every 10–15s; running is not failed.\n- **Do not auto-subset** unless the user asks. Prefer `results(get, figures="none")` for sweeps.\n- Column names are case-sensitive. After recolor/transition_flow/project_columns, use the **new** job_id when returned.\n- On `api_unreachable`: one calm sentence, honor `retry_after_sec`; use `account(health)`. Timeouts are not auto-retried.\n\n## UI delivery\n\nTool results lead with a localhost viz URL and a structured `ui_delivery` block — follow `hint_for_agent`. Env: `BARIVIA_UI_DELIVERY` (auto|apps|localhost|inline|text_only), `BARIVIA_UI_PREFER_LOCALHOST=1` (opt-in localhost even when Apps advertised), `BARIVIA_VIZ_PORT`. Details: `account(status)`.'});s(T,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(T,P,P,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:P,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(T),w(T),A(T),f(T),_(T),g(T,h),b(T),v(T),j(T),k(T),I(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 with SIOM entitlement; floop_siom when entitled; 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")}}]})),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 S(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const O=new t;(async function(){try{const e=await a(l,c,p);m(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=r(t);u(!!o?.mimeTypes?.includes(n))},await T.connect(O)})().catch(console.error);
|
package/dist/job_monitor.js
CHANGED
|
@@ -10,6 +10,9 @@ export const DEFAULT_BLOCK_UNTIL_SEC = 900;
|
|
|
10
10
|
export const DEFAULT_POLL_INTERVAL_SEC = 5;
|
|
11
11
|
export const MIN_POLL_INTERVAL_SEC = 5;
|
|
12
12
|
export const HEARTBEAT_POLLS = 1;
|
|
13
|
+
/** After this many unchanged polls, gently raise interval (never below MIN_POLL). */
|
|
14
|
+
export const IDLE_BACKOFF_AFTER_POLLS = 8;
|
|
15
|
+
export const MAX_POLL_INTERVAL_SEC = 15;
|
|
13
16
|
function sleep(ms) {
|
|
14
17
|
return new Promise((r) => setTimeout(r, ms));
|
|
15
18
|
}
|
|
@@ -218,12 +221,22 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
218
221
|
let lastSnap = null;
|
|
219
222
|
let data = {};
|
|
220
223
|
let heartbeat = 0;
|
|
224
|
+
let idleStreak = 0;
|
|
221
225
|
while (Date.now() - start < blockMs) {
|
|
222
226
|
data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
223
227
|
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
224
228
|
const snap = snapshotFromJob(data, elapsedSec);
|
|
225
229
|
heartbeat += 1;
|
|
226
230
|
const heartbeatDue = heartbeat >= HEARTBEAT_POLLS;
|
|
231
|
+
const unchanged = lastSnap != null &&
|
|
232
|
+
lastSnap.status === snap.status &&
|
|
233
|
+
lastSnap.progress_pct === snap.progress_pct &&
|
|
234
|
+
lastSnap.phase === snap.phase &&
|
|
235
|
+
lastSnap.epoch === snap.epoch;
|
|
236
|
+
if (unchanged)
|
|
237
|
+
idleStreak += 1;
|
|
238
|
+
else
|
|
239
|
+
idleStreak = 0;
|
|
227
240
|
if (shouldRecordSnapshot(lastSnap, snap) || heartbeatDue) {
|
|
228
241
|
snapshots.push(snap);
|
|
229
242
|
lastSnap = snap;
|
|
@@ -233,7 +246,13 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
233
246
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
234
247
|
break;
|
|
235
248
|
}
|
|
236
|
-
|
|
249
|
+
// Gentle backoff on idle polls — raise interval, never below the 5s floor.
|
|
250
|
+
let waitMs = pollMs;
|
|
251
|
+
if (idleStreak >= IDLE_BACKOFF_AFTER_POLLS) {
|
|
252
|
+
const steps = Math.floor((idleStreak - IDLE_BACKOFF_AFTER_POLLS) / 2);
|
|
253
|
+
waitMs = Math.min(pollMs + steps * 5000, MAX_POLL_INTERVAL_SEC * 1000);
|
|
254
|
+
}
|
|
255
|
+
await sleep(Math.max(MIN_POLL_INTERVAL_SEC * 1000, waitMs));
|
|
237
256
|
}
|
|
238
257
|
const status = String(data.status ?? "");
|
|
239
258
|
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
package/dist/shared.js
CHANGED
|
@@ -27,10 +27,37 @@ export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "2000
|
|
|
27
27
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
28
28
|
/** Cool-down hint after the burst is exhausted — agents should wait before another round. */
|
|
29
29
|
export const UNAVAILABLE_RETRY_AFTER_SEC = 30;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Delay between burst retries. Keeps the flat RETRY_BASE_MS default (short gateway
|
|
32
|
+
* blips); when the server sends Retry-After / retry_after_sec, sleep at least that long.
|
|
33
|
+
*/
|
|
34
|
+
export function retryDelayMs(_attempt, retryAfterMs) {
|
|
35
|
+
if (retryAfterMs != null && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
|
|
36
|
+
return Math.max(RETRY_BASE_MS, Math.floor(retryAfterMs));
|
|
37
|
+
}
|
|
32
38
|
return RETRY_BASE_MS;
|
|
33
39
|
}
|
|
40
|
+
/** Parse Retry-After header (delta-seconds) and/or JSON body retry_after_sec → ms. */
|
|
41
|
+
export function parseRetryAfterMs(resp, bodyText) {
|
|
42
|
+
const header = resp.headers.get("Retry-After");
|
|
43
|
+
if (header) {
|
|
44
|
+
const sec = parseInt(header.trim(), 10);
|
|
45
|
+
if (!Number.isNaN(sec) && sec >= 0)
|
|
46
|
+
return sec * 1000;
|
|
47
|
+
}
|
|
48
|
+
if (bodyText) {
|
|
49
|
+
try {
|
|
50
|
+
const j = JSON.parse(bodyText);
|
|
51
|
+
const sec = Number(j.retry_after_sec);
|
|
52
|
+
if (Number.isFinite(sec) && sec >= 0)
|
|
53
|
+
return sec * 1000;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* not JSON */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
34
61
|
function newRequestId() {
|
|
35
62
|
return randomUUID();
|
|
36
63
|
}
|
|
@@ -39,7 +66,7 @@ function newRequestId() {
|
|
|
39
66
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
40
67
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
41
68
|
*/
|
|
42
|
-
export const CLIENT_VERSION = "0.23.
|
|
69
|
+
export const CLIENT_VERSION = "0.23.11";
|
|
43
70
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
44
71
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
45
72
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
@@ -244,7 +271,11 @@ export function isTransientError(err, status) {
|
|
|
244
271
|
return true; // network-level fetch failure
|
|
245
272
|
return false;
|
|
246
273
|
}
|
|
247
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* Streaming SHA-256 of a file without reading it into memory.
|
|
276
|
+
* Used by tools/datasets.ts upload as Idempotency-Key = sha256(name+body) — do not
|
|
277
|
+
* invent a second key path here.
|
|
278
|
+
*/
|
|
248
279
|
export async function streamFileSha256(srcPath) {
|
|
249
280
|
return new Promise((resolve, reject) => {
|
|
250
281
|
const h = createHash("sha256");
|
|
@@ -814,7 +845,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
814
845
|
const text = await resp.text();
|
|
815
846
|
if (!resp.ok) {
|
|
816
847
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
817
|
-
const delayMs = retryDelayMs(attempt);
|
|
848
|
+
const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
|
|
818
849
|
logWarn("API retry", {
|
|
819
850
|
rid: requestId,
|
|
820
851
|
method,
|
|
@@ -845,7 +876,10 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
845
876
|
catch (err) {
|
|
846
877
|
lastError = err;
|
|
847
878
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
848
|
-
const
|
|
879
|
+
const errRetryMs = typeof err.retryAfterSec === "number"
|
|
880
|
+
? err.retryAfterSec * 1000
|
|
881
|
+
: undefined;
|
|
882
|
+
const delayMs = retryDelayMs(attempt, errRetryMs);
|
|
849
883
|
logWarn("API retry", {
|
|
850
884
|
rid: requestId,
|
|
851
885
|
method,
|
|
@@ -889,8 +923,9 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
889
923
|
},
|
|
890
924
|
}, effectiveTimeout);
|
|
891
925
|
if (!resp.ok) {
|
|
926
|
+
const text = await resp.text();
|
|
892
927
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
893
|
-
const delayMs = retryDelayMs(attempt);
|
|
928
|
+
const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
|
|
894
929
|
logWarn("API retry", {
|
|
895
930
|
rid: requestId,
|
|
896
931
|
path,
|
|
@@ -902,7 +937,6 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
902
937
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
903
938
|
continue;
|
|
904
939
|
}
|
|
905
|
-
const text = await resp.text();
|
|
906
940
|
throwApiError(resp.status, text, requestId);
|
|
907
941
|
}
|
|
908
942
|
const arrayBuf = await resp.arrayBuffer();
|
|
@@ -914,7 +948,10 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
914
948
|
catch (err) {
|
|
915
949
|
lastError = err;
|
|
916
950
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
917
|
-
const
|
|
951
|
+
const errRetryMs = typeof err.retryAfterSec === "number"
|
|
952
|
+
? err.retryAfterSec * 1000
|
|
953
|
+
: undefined;
|
|
954
|
+
const delayMs = retryDelayMs(attempt, errRetryMs);
|
|
918
955
|
logWarn("API retry", {
|
|
919
956
|
rid: requestId,
|
|
920
957
|
path,
|
package/dist/tools/account.js
CHANGED
|
@@ -46,7 +46,7 @@ export function registerAccountTool(server) {
|
|
|
46
46
|
action=status: Returns plan tier, compute class (CPU/GPU), usage limits, live queue state, training time estimates, and credit balance.
|
|
47
47
|
Use BEFORE large jobs to check GPU availability and estimate wait time.
|
|
48
48
|
When the API/gateway is down, returns structured \`{ok:false, retry_after_sec, …}\` — treat as a pause, not a crash.
|
|
49
|
-
action=inventory:
|
|
49
|
+
action=inventory: One-shot markdown overview — all datasets (slim) plus the latest N jobs (limit default 50). Not paginated for datasets; for more jobs use jobs(action=list, cursor=next_cursor, limit=…). Prefer this over hand-assembling datasets(list)+jobs(list).
|
|
50
50
|
action=health: Lightweight liveness/readiness (no R2). Use when tools return api_unreachable or before retrying deferred send_feedback.
|
|
51
51
|
Capacity is shared LOCAL/GKE worker pools — there is no per-key cloud burst lease.
|
|
52
52
|
NOT FOR: Training itself — use train(action=map). This tool only manages the account.`, {
|
|
@@ -164,6 +164,7 @@ async function handleResultsExplorer(job_id) {
|
|
|
164
164
|
jobId: job_id,
|
|
165
165
|
jobStatus: "completed",
|
|
166
166
|
inlineImageAttached,
|
|
167
|
+
uiResourceUri: RESULTS_EXPLORER_URI,
|
|
167
168
|
});
|
|
168
169
|
appendUiDeliveryContent(content, uiDelivery, {
|
|
169
170
|
linkLabel: "Open results explorer",
|
package/dist/tools/feedback.js
CHANGED
|
@@ -23,7 +23,7 @@ export function registerFeedbackTool(server) {
|
|
|
23
23
|
|
|
24
24
|
For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note.
|
|
25
25
|
|
|
26
|
-
If the API or object storage is unavailable, this tool queues feedback locally (~/.barivia/deferred-feedback) and returns deferred=true with request_id / api_health — call again later to flush. Probe API liveness with account(action=health) (GET /health; does not touch R2).`, {
|
|
26
|
+
If the API or object storage is unavailable, this tool queues feedback locally (~/.barivia/deferred-feedback) and returns deferred=true with request_id / api_health — call again later to flush (idempotent enqueue of the same draft). Probe API liveness with account(action=health) (GET /health; does not touch R2).`, {
|
|
27
27
|
feedback: z.string().max(1400).optional().describe("Single feedback note (max 1400 characters). Use feedback_items instead for a multi-part report."),
|
|
28
28
|
feedback_items: z.array(z.string().max(1400)).max(10).optional().describe("Several feedback instances (each max 1400 characters, up to 10), stored together as one batch. Prefer this for a detailed issue: split it into focused parts (symptoms, reproduction, environment, asks)."),
|
|
29
29
|
}, async ({ feedback, feedback_items }) => {
|
|
@@ -104,5 +104,10 @@ If the API or object storage is unavailable, this tool queues feedback locally (
|
|
|
104
104
|
}
|
|
105
105
|
throw err;
|
|
106
106
|
}
|
|
107
|
+
}, {
|
|
108
|
+
readOnlyHint: false,
|
|
109
|
+
destructiveHint: false,
|
|
110
|
+
idempotentHint: true,
|
|
111
|
+
openWorldHint: true,
|
|
107
112
|
});
|
|
108
113
|
}
|
package/dist/tools/jobs.js
CHANGED
|
@@ -24,13 +24,13 @@ ASYNC POLLING PROTOCOL (action=monitor):
|
|
|
24
24
|
- Optional UI: training_monitor(job_id) for embedded charts; jobs(action=monitor) is the headless equivalent.
|
|
25
25
|
|
|
26
26
|
ASYNC POLLING PROTOCOL (action=status):
|
|
27
|
-
- One-shot check only. If not using monitor, poll every 10-15 seconds manually.
|
|
27
|
+
- One-shot check only. If not using monitor, poll every 10-15 seconds manually — do not give up after one or two polls; large jobs need patience.
|
|
28
28
|
- Progress increases through phases (ordering then convergence for map training); other job types differ.
|
|
29
29
|
- For large grids (40×40+), do not assume failure before 3 minutes on CPU.
|
|
30
30
|
- Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min.
|
|
31
31
|
- completed → results(action=get); failed → read error + optional failure_stage (preprocessing/training/metrics/visualization/upload): memory error → smaller grid/batch and retrain; column missing → datasets(action=preview); NaN on a standard map → train(action=impute) for sparse columns or clean the CSV.
|
|
32
32
|
|
|
33
|
-
action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
|
|
33
|
+
action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page; when the response includes next_cursor, call again with cursor=that value. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
|
|
34
34
|
action=update: PATCH label/description/tags (pass at train submit when possible).
|
|
35
35
|
action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown (including SIOM knobs gamma/gamma_f/siom_decay/siom_penalty/reset_per_epoch when they vary); pass label="…" at train time for readable rows.
|
|
36
36
|
action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
|
package/dist/tools/results.js
CHANGED
|
@@ -19,7 +19,8 @@ Temporal state-transition arrows moved to **inference(action=transition_flow)**
|
|
|
19
19
|
ONLY call this after jobs(action=status) returns "completed".
|
|
20
20
|
ESCALATION: If job not found, verify job_id. If "job not complete", poll with jobs(action=status).
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Job states (for async parents): pending → running → completed | failed | cancelled (finalize_training may run after compute completes — wait until status=completed before get).
|
|
23
|
+
action=get: Returns text summary with quality metrics and inline images; structuredContent includes a slim verification block (status, file_count, has_summary).
|
|
23
24
|
- figures: omit = combined only. Prefer string "all" / "none" (not an array). "all" = all published plots (excludes unpublished cyclic cos/sin expansions unless train used viz_upload_cyclic_components=true). "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, divergence_kl, component_1..N) or download filenames. Hosts that stringify arrays are coerced. Request unpublished cyclic planes via results(recolor, figures=[component_N, ...]).
|
|
24
25
|
- include_individual: if true (and figures omitted), inlines every component plane.
|
|
25
26
|
- After recolor or project, use the job_id returned by that operation to get the new artifacts, not the original training job_id.
|
|
@@ -478,6 +479,13 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
478
479
|
summary,
|
|
479
480
|
files,
|
|
480
481
|
figures_inlined: [...inlinedImages],
|
|
482
|
+
verification: {
|
|
483
|
+
status: "completed",
|
|
484
|
+
has_summary: Object.keys(summary).length > 0,
|
|
485
|
+
file_count: files.length,
|
|
486
|
+
figures_inlined_count: inlinedImages.size,
|
|
487
|
+
job_type: jobType2,
|
|
488
|
+
},
|
|
481
489
|
};
|
|
482
490
|
const summaryText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n\n");
|
|
483
491
|
return structuredTextResult(structuredPayload, summaryText, content);
|
package/dist/tools/train.js
CHANGED
|
@@ -15,7 +15,7 @@ export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Ret
|
|
|
15
15
|
|
|
16
16
|
Call **guide_barsom_workflow**, **training_guidance**, and/or the **prepare_training** prompt for plan-scoped parameter details, timing heuristics, quality.csv semantics, SIOM/FLooP knobs, and impute constraints — do not guess. Backend: "cpu" | "gpu" | "gpu_graphs" (on CPU-only hosts pass backend=cpu).
|
|
17
17
|
|
|
18
|
-
ASYNC: every action returns a job_id immediately.
|
|
18
|
+
ASYNC: every action returns a job_id immediately. Prefer jobs(action=monitor) to block with snapshots, or poll jobs(action=status) every 10–15s (not faster) — do not abandon after one poll. When completed, call results(action=get). Timing and quality heuristics: **training_guidance**.
|
|
19
19
|
NOT FOR: lifecycle → jobs; scoring → inference.
|
|
20
20
|
Rank transform (map/siom/floop only) is train-only — see training_guidance.`;
|
|
21
21
|
function assertImputeTrainingArgs(args) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { runMcpToolAudit } from "../audit.js";
|
|
4
|
+
import { logWarn } from "../logger.js";
|
|
4
5
|
import { apiCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
|
|
6
|
+
/** Loose shape check — warn + pass through on failure (never drop a successful monitor payload). */
|
|
7
|
+
const TrainingMonitorStructuredSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
type: z.literal("training-monitor"),
|
|
10
|
+
jobId: z.string(),
|
|
11
|
+
refresh_interval_ms: z.number(),
|
|
12
|
+
})
|
|
13
|
+
.passthrough();
|
|
5
14
|
import { appendUiDeliveryContent, beginInlineFallback, resolveUiDelivery, tryAttachLearningCurve, } from "../ui-delivery.js";
|
|
6
15
|
import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
|
|
7
16
|
import { buildVizStandaloneUrl, resultsExplorerStandaloneLinkText, } from "../viz_links.js";
|
|
@@ -114,8 +123,8 @@ export async function buildTrainingMonitorResult(job_id, opts) {
|
|
|
114
123
|
timingParts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
115
124
|
const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
|
|
116
125
|
const text = (preamble ? `${preamble}\n\n` : "") +
|
|
117
|
-
`Training monitor (
|
|
118
|
-
`
|
|
126
|
+
`Training monitor (standalone URL + optional App panel; refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s when mounted): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
|
|
127
|
+
`For headless progress use jobs(action=status); this tool is optional.`;
|
|
119
128
|
const content = [{ type: "text", text }];
|
|
120
129
|
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
|
121
130
|
let inlineImageAttached = false;
|
|
@@ -127,6 +136,7 @@ export async function buildTrainingMonitorResult(job_id, opts) {
|
|
|
127
136
|
jobId: job_id,
|
|
128
137
|
jobStatus: status,
|
|
129
138
|
inlineImageAttached,
|
|
139
|
+
uiResourceUri: TRAINING_MONITOR_URI,
|
|
130
140
|
});
|
|
131
141
|
appendUiDeliveryContent(content, uiDelivery, {
|
|
132
142
|
linkLabel: "Open training monitor",
|
|
@@ -142,6 +152,13 @@ export async function buildTrainingMonitorResult(job_id, opts) {
|
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
structuredContent.ui_delivery = uiDelivery;
|
|
155
|
+
const parsed = TrainingMonitorStructuredSchema.safeParse(structuredContent);
|
|
156
|
+
if (!parsed.success) {
|
|
157
|
+
logWarn("training_monitor structuredContent validation failed — passing through", {
|
|
158
|
+
job_id,
|
|
159
|
+
issues: parsed.error.issues.slice(0, 5).map((i) => i.message),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
145
162
|
return { structuredContent, content, text };
|
|
146
163
|
}
|
|
147
164
|
/** After terminal + finalize, append results explorer open hint (monitor completion path). */
|
|
@@ -158,13 +175,13 @@ export function appendResultsExplorerCompletionHint(content, job_id) {
|
|
|
158
175
|
export function registerTrainingMonitorTool(server) {
|
|
159
176
|
registerAppTool(server, "training_monitor", {
|
|
160
177
|
title: "Training Monitor",
|
|
161
|
-
description: "Optional
|
|
178
|
+
description: "Optional view of training progress for a job_id. Prefer the standalone localhost URL in the tool result (always surfaced when the viz server is up) — some hosts list MCP Apps without mounting panels. When a host mounts the App, the panel auto-refreshes every 5s: QE/TE curves (≤1000 batch samples per phase) plus a hit-distribution heatmap. Set BARIVIA_VIZ_PORT for a stable port; re-run for a fresh link after proxy restart. Headless: jobs(action=status) has the same scalars — not required to finish training or read results.",
|
|
162
179
|
inputSchema: {
|
|
163
180
|
job_id: z.string().describe("Training job ID to monitor"),
|
|
164
181
|
fetch_training_log: z
|
|
165
182
|
.boolean()
|
|
166
183
|
.optional()
|
|
167
|
-
.describe("
|
|
184
|
+
.describe("[INTERNAL — App/refresh path] Merge completed-job training-log arrays when live progress is empty. Agents should omit this."),
|
|
168
185
|
},
|
|
169
186
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
170
187
|
}, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
|
package/dist/ui-delivery.js
CHANGED
|
@@ -6,11 +6,20 @@ import { apiRawCall, getClientSupportsMcpApps, getVizPort, mimeForFilename, rese
|
|
|
6
6
|
import { buildVizStandaloneUrl } from "./viz_links.js";
|
|
7
7
|
export function parseUiDeliveryOverride() {
|
|
8
8
|
const raw = (process.env.BARIVIA_UI_DELIVERY ?? "auto").trim().toLowerCase();
|
|
9
|
-
if (raw === "apps" ||
|
|
9
|
+
if (raw === "apps" ||
|
|
10
|
+
raw === "localhost" ||
|
|
11
|
+
raw === "inline" ||
|
|
12
|
+
raw === "text_only" ||
|
|
13
|
+
raw === "auto") {
|
|
10
14
|
return raw;
|
|
11
15
|
}
|
|
12
16
|
return "auto";
|
|
13
17
|
}
|
|
18
|
+
/** Opt-in: force localhost under auto even when the host advertises MCP Apps (e.g. Cursor list-without-mount). */
|
|
19
|
+
export function preferLocalhostUi() {
|
|
20
|
+
const v = (process.env.BARIVIA_UI_PREFER_LOCALHOST ?? "").trim().toLowerCase();
|
|
21
|
+
return v === "1" || v === "true" || v === "yes";
|
|
22
|
+
}
|
|
14
23
|
function vizPortPinned() {
|
|
15
24
|
return Boolean(process.env.BARIVIA_VIZ_PORT?.trim());
|
|
16
25
|
}
|
|
@@ -30,17 +39,26 @@ export function buildHealthUrl(jobId) {
|
|
|
30
39
|
}
|
|
31
40
|
function hintForTier(tier, tool, opts) {
|
|
32
41
|
const label = tool === "training_monitor" ? "training monitor" : "results explorer";
|
|
42
|
+
const linkFirst = opts.standaloneUrl
|
|
43
|
+
? `AGENT: Post a clickable markdown link first (e.g. "[Open ${label}](${opts.standaloneUrl})") — do not bury it after metrics.`
|
|
44
|
+
: undefined;
|
|
33
45
|
switch (tier) {
|
|
34
46
|
case "embedded":
|
|
35
|
-
return
|
|
47
|
+
return linkFirst
|
|
48
|
+
? `${linkFirst} Embedded MCP App panel if the host mounts it; some hosts list ui:// without mounting (use the URL).`
|
|
49
|
+
: `The ${label} MCP App panel may embed. Use jobs(status) / results(get) if no panel or URL appears.`;
|
|
36
50
|
case "localhost":
|
|
37
|
-
return
|
|
38
|
-
?
|
|
51
|
+
return linkFirst
|
|
52
|
+
? `${linkFirst} Re-post after the job completes if status was running. Set BARIVIA_VIZ_PORT for a stable port.`
|
|
39
53
|
: `AGENT: Viz server unavailable — use jobs(status) and results(get) for progress and figures.`;
|
|
40
54
|
case "inline_image":
|
|
41
|
-
return
|
|
55
|
+
return linkFirst
|
|
56
|
+
? `${linkFirst} Inline raster figure(s) may also be attached; summarize metrics; offer results_explorer or results(get) for more.`
|
|
57
|
+
: `AGENT: Inline raster figure(s) are attached below. Summarize metrics; offer results_explorer or results(get) for additional figures.`;
|
|
42
58
|
case "text_only":
|
|
43
|
-
return
|
|
59
|
+
return linkFirst
|
|
60
|
+
? `${linkFirst} Otherwise use jobs(status) for progress and results(get) for figures.`
|
|
61
|
+
: `AGENT: No embedded panel or localhost viz — use jobs(status) for training progress and results(get) or results_explorer for figures.`;
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
64
|
export function resolveUiDelivery(options) {
|
|
@@ -58,9 +76,15 @@ export function resolveUiDelivery(options) {
|
|
|
58
76
|
else if (override === "inline") {
|
|
59
77
|
delivery = options.inlineImageAttached ? "inline_image" : "text_only";
|
|
60
78
|
}
|
|
79
|
+
else if (override === "text_only") {
|
|
80
|
+
delivery = "text_only";
|
|
81
|
+
}
|
|
61
82
|
else {
|
|
62
|
-
// auto
|
|
63
|
-
if (
|
|
83
|
+
// auto — default unchanged: prefer embedded when Apps advertised unless opt-in prefer-localhost
|
|
84
|
+
if (preferLocalhostUi() && port) {
|
|
85
|
+
delivery = "localhost";
|
|
86
|
+
}
|
|
87
|
+
else if (mcpApps) {
|
|
64
88
|
delivery = "embedded";
|
|
65
89
|
}
|
|
66
90
|
else if (port) {
|
|
@@ -73,12 +97,13 @@ export function resolveUiDelivery(options) {
|
|
|
73
97
|
delivery = "text_only";
|
|
74
98
|
}
|
|
75
99
|
}
|
|
100
|
+
// Always include standalone URL when the viz port exists (all tiers) — hosts may not mount Apps.
|
|
76
101
|
const urls = [];
|
|
77
|
-
if (standaloneUrl
|
|
102
|
+
if (standaloneUrl) {
|
|
78
103
|
urls.push(standaloneUrl);
|
|
79
104
|
}
|
|
80
105
|
const healthUrl = buildHealthUrl(options.jobId);
|
|
81
|
-
if (healthUrl && delivery === "localhost") {
|
|
106
|
+
if (healthUrl && (delivery === "localhost" || preferLocalhostUi())) {
|
|
82
107
|
urls.push(healthUrl);
|
|
83
108
|
}
|
|
84
109
|
const crossLink = options.tool === "training_monitor" && options.jobStatus === "completed"
|
|
@@ -97,6 +122,7 @@ export function resolveUiDelivery(options) {
|
|
|
97
122
|
viz_port: port,
|
|
98
123
|
port_pinned: vizPortPinned(),
|
|
99
124
|
override,
|
|
125
|
+
...(options.uiResourceUri ? { ui_resource_uri: options.uiResourceUri } : {}),
|
|
100
126
|
};
|
|
101
127
|
}
|
|
102
128
|
/**
|
|
@@ -154,6 +180,7 @@ export function getMcpClientDiagnostics() {
|
|
|
154
180
|
const override = parseUiDeliveryOverride();
|
|
155
181
|
const mcpApps = getClientSupportsMcpApps();
|
|
156
182
|
const port = getVizPort() || null;
|
|
183
|
+
const preferLocal = preferLocalhostUi();
|
|
157
184
|
let activeTier = "text_only";
|
|
158
185
|
if (override === "apps") {
|
|
159
186
|
activeTier = mcpApps ? "embedded" : port ? "localhost" : "text_only";
|
|
@@ -164,6 +191,12 @@ export function getMcpClientDiagnostics() {
|
|
|
164
191
|
else if (override === "inline") {
|
|
165
192
|
activeTier = "inline_image";
|
|
166
193
|
}
|
|
194
|
+
else if (override === "text_only") {
|
|
195
|
+
activeTier = "text_only";
|
|
196
|
+
}
|
|
197
|
+
else if (preferLocal && port) {
|
|
198
|
+
activeTier = "localhost";
|
|
199
|
+
}
|
|
167
200
|
else if (mcpApps) {
|
|
168
201
|
activeTier = "embedded";
|
|
169
202
|
}
|
|
@@ -175,6 +208,7 @@ export function getMcpClientDiagnostics() {
|
|
|
175
208
|
viz_port: port,
|
|
176
209
|
viz_port_pinned: vizPortPinned(),
|
|
177
210
|
ui_delivery_override: override,
|
|
211
|
+
ui_prefer_localhost: preferLocal,
|
|
178
212
|
active_delivery_tier: activeTier,
|
|
179
213
|
health_url: buildHealthUrl(),
|
|
180
214
|
};
|
|
@@ -305,7 +305,7 @@ Note: This type uses \`Record<K, string | undefined>\` rather than \`Partial<Rec
|
|
|
305
305
|
for compatibility with Zod schema generation. Both are functionally equivalent for validation.`);c.object({method:c.literal("ui/open-link"),params:c.object({url:c.string().describe("URL to open in the host's browser")})});var lI=c.object({isError:c.boolean().optional().describe("True if the host failed to open the URL (e.g., due to security policy).")}).passthrough(),cI=c.object({isError:c.boolean().optional().describe("True if the host rejected or failed to deliver the message.")}).passthrough();c.object({method:c.literal("ui/notifications/sandbox-proxy-ready"),params:c.object({})});var oo=c.object({connectDomains:c.array(c.string()).optional().describe("Origins for network requests (fetch/XHR/WebSocket)."),resourceDomains:c.array(c.string()).optional().describe("Origins for static resources (scripts, images, styles, fonts)."),frameDomains:c.array(c.string()).optional().describe("Origins for nested iframes (frame-src directive)."),baseUriDomains:c.array(c.string()).optional().describe("Allowed base URIs for the document (base-uri directive).")}),so=c.object({camera:c.object({}).optional().describe("Request camera access (Permission Policy `camera` feature)."),microphone:c.object({}).optional().describe("Request microphone access (Permission Policy `microphone` feature)."),geolocation:c.object({}).optional().describe("Request geolocation access (Permission Policy `geolocation` feature)."),clipboardWrite:c.object({}).optional().describe("Request clipboard write access (Permission Policy `clipboard-write` feature).")});c.object({method:c.literal("ui/notifications/size-changed"),params:c.object({width:c.number().optional().describe("New width in pixels."),height:c.number().optional().describe("New height in pixels.")})});var dI=c.object({method:c.literal("ui/notifications/tool-input"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Complete tool call arguments as key-value pairs.")).optional().describe("Complete tool call arguments as key-value pairs.")})}),mI=c.object({method:c.literal("ui/notifications/tool-input-partial"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Partial tool call arguments (incomplete, may change).")).optional().describe("Partial tool call arguments (incomplete, may change).")})}),fI=c.object({method:c.literal("ui/notifications/tool-cancelled"),params:c.object({reason:c.string().optional().describe('Optional reason for the cancellation (e.g., "user action", "timeout").')})}),pI=c.object({fonts:c.string().optional()}),vI=c.object({variables:uI.optional().describe("CSS variables for theming the app."),css:pI.optional().describe("CSS blocks that apps can inject.")}),hI=c.object({method:c.literal("ui/resource-teardown"),params:c.object({})});c.record(c.string(),c.unknown());var Go=c.object({text:c.object({}).optional().describe("Host supports text content blocks."),image:c.object({}).optional().describe("Host supports image content blocks."),audio:c.object({}).optional().describe("Host supports audio content blocks."),resource:c.object({}).optional().describe("Host supports resource content blocks."),resourceLink:c.object({}).optional().describe("Host supports resource link content blocks."),structuredContent:c.object({}).optional().describe("Host supports structured content.")}),gI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),openLinks:c.object({}).optional().describe("Host supports opening external URLs."),serverTools:c.object({listChanged:c.boolean().optional().describe("Host supports tools/list_changed notifications.")}).optional().describe("Host can proxy tool calls to the MCP server."),serverResources:c.object({listChanged:c.boolean().optional().describe("Host supports resources/list_changed notifications.")}).optional().describe("Host can proxy resource reads to the MCP server."),logging:c.object({}).optional().describe("Host accepts log messages."),sandbox:c.object({permissions:so.optional().describe("Permissions granted by the host (camera, microphone, geolocation)."),csp:oo.optional().describe("CSP domains approved by the host.")}).optional().describe("Sandbox configuration applied by the host."),updateModelContext:Go.optional().describe("Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns."),message:Go.optional().describe("Host supports receiving content messages (ui/message) from the view.")}),_I=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),tools:c.object({listChanged:c.boolean().optional().describe("App supports tools/list_changed notifications.")}).optional().describe("App exposes MCP-style tools that the host can call."),availableDisplayModes:c.array($t).optional().describe("Display modes the app supports.")});c.object({method:c.literal("ui/notifications/initialized"),params:c.object({}).optional()});c.object({csp:oo.optional().describe("Content Security Policy configuration."),permissions:so.optional().describe("Sandbox permissions requested by the UI."),domain:c.string().optional().describe("Dedicated origin for view sandbox."),prefersBorder:c.boolean().optional().describe("Visual boundary preference - true if UI prefers a visible border.")});c.object({method:c.literal("ui/request-display-mode"),params:c.object({mode:$t.describe("The display mode being requested.")})});var $I=c.object({mode:$t.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),bI=c.union([c.literal("model"),c.literal("app")]).describe("Tool visibility scope - who can access the tool.");c.object({resourceUri:c.string().optional(),visibility:c.array(bI).optional().describe(`Who can access this tool. Default: ["model", "app"]
|
|
306
306
|
- "model": Tool visible to and callable by the agent
|
|
307
307
|
- "app": Tool callable by the app from this server only`)});c.object({mimeTypes:c.array(c.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});c.object({method:c.literal("ui/message"),params:c.object({role:c.literal("user").describe('Message role, currently only "user" is supported.'),content:c.array(Ot).describe("Message content blocks (text, image, etc.).")})});c.object({method:c.literal("ui/notifications/sandbox-resource-ready"),params:c.object({html:c.string().describe("HTML content to load into the inner iframe."),sandbox:c.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:oo.optional().describe("CSP configuration from resource metadata."),permissions:so.optional().describe("Sandbox permissions from resource metadata.")})});var yI=c.object({method:c.literal("ui/notifications/tool-result"),params:ji.describe("Standard MCP tool execution result.")}),Of=c.object({toolInfo:c.object({id:wt.optional().describe("JSON-RPC id of the tools/call request."),tool:cr.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:oI.optional().describe("Current color theme preference."),styles:vI.optional().describe("Style configuration for theming the app."),displayMode:$t.optional().describe("How the UI is currently displayed."),availableDisplayModes:c.array($t).optional().describe("Display modes the host supports."),containerDimensions:c.union([c.object({height:c.number().describe("Fixed container height in pixels.")}),c.object({maxHeight:c.union([c.number(),c.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(c.union([c.object({width:c.number().describe("Fixed container width in pixels.")}),c.object({maxWidth:c.union([c.number(),c.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
|
|
308
|
-
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),kI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Of.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ot).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ti.describe("App identification (name and version)."),appCapabilities:_I.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var II=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ti.describe("Host application identification and version."),hostCapabilities:gI.describe("Features and capabilities provided by the host."),hostContext:Of.describe("Rich context about the host environment.")}).passthrough();class wI extends S${_appInfo;_capabilities;options;_hostCapabilities;_hostInfo;_hostContext;constructor(t,n={},r={autoResize:!0}){super(r),this._appInfo=t,this._capabilities=n,this.options=r,this.setRequestHandler(Ni,i=>(console.log("Received ping:",i.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(t){this.setNotificationHandler(dI,n=>t(n.params))}set ontoolinputpartial(t){this.setNotificationHandler(mI,n=>t(n.params))}set ontoolresult(t){this.setNotificationHandler(yI,n=>t(n.params))}set ontoolcancelled(t){this.setNotificationHandler(fI,n=>t(n.params))}set onhostcontextchanged(t){this.setNotificationHandler(kI,n=>{this._hostContext={...this._hostContext,...n.params},t(n.params)})}set onteardown(t){this.setRequestHandler(hI,(n,r)=>t(n.params,r))}set oncalltool(t){this.setRequestHandler(Ls,(n,r)=>t(n.params,r))}set onlisttools(t){this.setRequestHandler(Cs,(n,r)=>t(n.params,r))}assertCapabilityForMethod(t){}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${t} registered`)}}assertNotificationCapability(t){}assertTaskCapability(t){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(t){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(t,n){return await this.request({method:"tools/call",params:t},ji,n)}sendMessage(t,n){return this.request({method:"ui/message",params:t},cI,n)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,n){return this.request({method:"ui/update-model-context",params:t},Gn,n)}openLink(t,n){return this.request({method:"ui/open-link",params:t},lI,n)}sendOpenLink=this.openLink;requestDisplayMode(t,n){return this.request({method:"ui/request-display-mode",params:t},$I,n)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,n=0,r=0,i=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let o=document.documentElement,s=o.style.width,u=o.style.height;o.style.width="fit-content",o.style.height="fit-content";let l=o.getBoundingClientRect();o.style.width=s,o.style.height=u;let d=window.innerWidth-o.clientWidth,f=Math.ceil(l.width+d),p=Math.ceil(l.height);(f!==n||p!==r)&&(n=f,r=p,this.sendSizeChanged({width:f,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(t=new z$(window.parent,window.parent),n){await super.connect(t);try{let r=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Z$}},II,n);if(r===void 0)throw Error(`Server sent invalid initialize result: ${r}`);this._hostCapabilities=r.hostCapabilities,this._hostInfo=r.hostInfo,this._hostContext=r.hostContext,await this.notification({method:"ui/notifications/initialized"}),this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(r){throw this.close(),r}}}let Ze=null,rt=null,Ie=null,An=null;const Ko=new Map,mi=document.getElementById("loading"),SI=document.getElementById("app"),Tf=document.getElementById("title"),Nf=document.getElementById("subtitle"),xI=document.getElementById("metrics"),zI=document.getElementById("highlights"),bt=document.getElementById("figure-select"),ZI=document.getElementById("figure-title"),UI=document.getElementById("figure-file"),OI=document.getElementById("figure-caption"),$e=document.getElementById("main-image"),ci=document.getElementById("open-standalone"),Zn=document.getElementById("open-monitor-btn"),Ge=document.getElementById("copy-url"),Xo=document.getElementById("related-monitor"),TI=document.getElementById("download-current");function NI(e){const t=String(e.job_type??"train_som"),n=e.grid??[],r=e.quantization_error!=null?Number(e.quantization_error).toFixed(4):"N/A",i=e.topographic_error!=null?Number(e.topographic_error).toFixed(4):"N/A",a=String(e.n_samples??"N/A"),o=String(e.n_features??"N/A"),s=e.coverage??{},u=l=>l!=null?`${(Number(l)*100).toFixed(1)}%`:"N/A";return t==="train_floop_siom"?[{label:"Topology",value:String(e.topology??"free")},{label:"Nodes",value:String(e.occupied_nodes??e.final_length??"N/A")},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:t==="train_siom"?[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:"SIOM"},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:String(e.model??"SOM")},{label:"QE",value:r},{label:"TE",value:i},{label:"Samples",value:a},{label:"Features",value:o}]}function jI(e,t){const n=e.summary??{},r=(n.files??[]).filter(s=>/\.(png|pdf|svg)$/i.test(s)),i=String(n.output_format??"png"),o=(r.length>0?r:[`combined.${i}`]).map(s=>{const u=s.replace(/\.(png|pdf|svg)$/i,""),l=u.startsWith("component_")?"component":u==="combined"||u==="coverage_overview"?"summary":"diagnostic";return{key:u,label:u.replace(/^component_\d+_/,"Component: ").replace(/_/g," "),filename:s,kind:l,caption:/\.(png|jpe?g|gif|webp)$/i.test(s)?void 0:"This figure is available for download, but it is not inline-displayable in this first iteration.",url:/\.(png|jpe?g|gif|webp)$/i.test(s)?`/api/results/${t}/image/${s}`:void 0}});return{type:"results-explorer",jobId:t,title:"Results Explorer",subtitle:String(e.label??""),metrics:NI(n),highlights:[n.training_duration_seconds!=null?`Training duration: ${n.training_duration_seconds}s`:"",n.selected_columns?`Variables: ${n.selected_columns.join(", ")}`:""].filter(Boolean),availableFigures:o,defaultFigureKey:o.find(s=>s.key==="combined"&&s.url)?.key??o.find(s=>!!s.url)?.key??o.find(s=>s.key==="combined")?.key??o[0]?.key,trainingMonitorUrl:`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,standaloneUrl:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`,relatedUrls:{trainingMonitor:`${window.location.origin}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,resultsExplorer:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`}}}function PI(e){xI.innerHTML=e.map(t=>`<div class="metric"><span class="metric-label">${t.label}</span><span class="metric-value">${t.value}</span></div>`).join("")}function EI(e){zI.innerHTML=e.length>0?e.map(t=>`<div class="highlight">${t}</div>`).join(""):'<div class="highlight">No extra highlights for this result yet.</div>'}const DI={summary:"Summary",diagnostic:"Diagnostics",component:"Components",artifact:"Other"},RI=["summary","diagnostic","component","artifact"];function AI(e){const t=new Map;for(const n of e){const r=n.kind??"diagnostic",i=t.get(r)??[];i.push(n),t.set(r,i)}bt.innerHTML="";for(const n of RI){const r=t.get(n);if(!r||r.length===0)continue;const i=document.createElement("optgroup");i.label=DI[n]??n;for(const a of r){const o=document.createElement("option");o.value=a.key,o.textContent=`${a.label} (${a.filename})`,a.key===rt?.key&&(o.selected=!0),i.appendChild(o)}bt.appendChild(i)}}bt.addEventListener("change",()=>{const e=Ze?.availableFigures.find(t=>t.key===bt.value);e&&jf(e)});async function CI(e,t){const n=`${e}/${t}`,r=Ko.get(n);if(r)return r;if(!Ie)return null;try{const a=(await Ie.callServerTool({name:"_fetch_figure",arguments:{job_id:e,filename:t}})).content?.find(o=>o.type==="image");if(a){const o=`data:${a.mimeType};base64,${a.data}`;return Ko.set(n,o),o}}catch{}return null}function LI(e){!Ie||!Ze||Ie.updateModelContext({content:[{type:"text",text:`User is viewing the "${e.label}" figure (${e.filename}) of results job ${Ze.jobId} in the Results Explorer.`}]}).catch(()=>{})}async function jf(e){if(rt=e,ZI.textContent=e.label,UI.textContent=e.filename,OI.textContent=e.caption??"",bt.value=e.key,LI(e),e.url)$e.src=e.url;else if(Ie&&Ze){const t=Ze.defaultFigureKey;if(e.key===t&&An)$e.src=An;else{$e.removeAttribute("src");const n=await CI(Ze.jobId,e.filename);n&&rt?.key===e.key&&($e.src=n)}}else $e.removeAttribute("src")}function MI(e){return e.standaloneUrl??window.location.href}function FI(e){const t=MI(e);e.standaloneUrl?(ci.href=e.standaloneUrl,ci.style.display="inline-flex"):Ef&&(ci.href=window.location.href,ci.style.display="inline-flex"),Ge.style.display="inline-flex",Ge.dataset.url=t;const n=e.relatedUrls?.trainingMonitor;n&&(Xo.href=n,Xo.style.display="inline-flex")}function Yo(e){Ze=e,Tf.textContent=e.title,Nf.textContent=e.subtitle??`Job ${e.jobId}`,PI(e.metrics),EI(e.highlights),FI(e);const t=e.trainingMonitorUrl??e.relatedUrls?.trainingMonitor??`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(e.jobId)}`;Zn.href=t,Zn.style.display="inline-flex",Zn.title="View live or completed training curves for this job";const n=e.availableFigures.find(r=>r.key===e.defaultFigureKey)??e.availableFigures[0]??null;rt=n,AI(e.availableFigures),n&&jf(n),mi.style.display="none",SI.style.display="block"}TI.addEventListener("click",()=>{const e=$e.src;if(!e||!rt)return;const t=document.createElement("a");t.href=e,t.download=rt.filename,t.click()});Ge.addEventListener("click",async()=>{const e=Ge.dataset.url||Ze?.standaloneUrl||window.location.href;try{await navigator.clipboard.writeText(e),Ge.textContent="Copied!",setTimeout(()=>{Ge.textContent="Copy explorer URL"},1500)}catch{window.prompt("Copy explorer URL:",e)}});$e.style.cursor="zoom-in";$e.addEventListener("click",()=>{if(!Ie||!$e.src)return;const e=Ie.getHostContext?.(),t=e?.availableDisplayModes;if(!Array.isArray(t)||!t.includes("fullscreen"))return;const n=e?.displayMode==="fullscreen"?"inline":"fullscreen";Ie.requestDisplayMode({mode:n}).then(r=>{$e.style.cursor=r.mode==="fullscreen"?"zoom-out":"zoom-in"}).catch(()=>{})});const Pf=new URLSearchParams(window.location.search),Ef=Pf.get("mode")==="standalone",di=Pf.get("job_id");if(Ef&&di){const e="The local viz port is assigned per MCP session and changes when the proxy restarts. Re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.";(async()=>{for(let n=1;n<=3;n++)try{const r=await fetch(`/api/results/${di}`);if(r.status===404){mi.textContent=`Results for job ${di} were not found (HTTP 404). The job may still be finalizing or was deleted, or this link points at a stale session. ${e}`;return}if(!r.ok)throw new Error(`HTTP ${r.status}`);const i=await r.json();Yo(jI(i,di));return}catch(r){if(n<3){await new Promise(i=>setTimeout(i,1e3*n));continue}mi.textContent=`Could not load results from the local viz server (${r instanceof Error?r.message:"error"}). ${e}`}})()}else{const e=new wI({name:"Results Explorer",version:"1.0.0"},{},{autoResize:!0});Ie=e,e.ontoolinput=t=>{const n=t?.arguments??{},r=n.job_id!=null?String(n.job_id):"";r&&(Tf.textContent="Results Explorer",Nf.textContent=`Loading job ${r}...`)},e.ontoolresult=t=>{const n=t.content?.find(i=>i.type==="image");n&&(An=`data:${n.mimeType};base64,${n.data}`);const r=t.structuredContent;if(!r||typeof r!="object"){mi.textContent="The results explorer did not receive structured data.";return}Yo(r)},e.connect()}</script>
|
|
308
|
+
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),kI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Of.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ot).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ti.describe("App identification (name and version)."),appCapabilities:_I.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var II=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ti.describe("Host application identification and version."),hostCapabilities:gI.describe("Features and capabilities provided by the host."),hostContext:Of.describe("Rich context about the host environment.")}).passthrough();class wI extends S${_appInfo;_capabilities;options;_hostCapabilities;_hostInfo;_hostContext;constructor(t,n={},r={autoResize:!0}){super(r),this._appInfo=t,this._capabilities=n,this.options=r,this.setRequestHandler(Ni,i=>(console.log("Received ping:",i.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(t){this.setNotificationHandler(dI,n=>t(n.params))}set ontoolinputpartial(t){this.setNotificationHandler(mI,n=>t(n.params))}set ontoolresult(t){this.setNotificationHandler(yI,n=>t(n.params))}set ontoolcancelled(t){this.setNotificationHandler(fI,n=>t(n.params))}set onhostcontextchanged(t){this.setNotificationHandler(kI,n=>{this._hostContext={...this._hostContext,...n.params},t(n.params)})}set onteardown(t){this.setRequestHandler(hI,(n,r)=>t(n.params,r))}set oncalltool(t){this.setRequestHandler(Ls,(n,r)=>t(n.params,r))}set onlisttools(t){this.setRequestHandler(Cs,(n,r)=>t(n.params,r))}assertCapabilityForMethod(t){}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${t} registered`)}}assertNotificationCapability(t){}assertTaskCapability(t){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(t){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(t,n){return await this.request({method:"tools/call",params:t},ji,n)}sendMessage(t,n){return this.request({method:"ui/message",params:t},cI,n)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,n){return this.request({method:"ui/update-model-context",params:t},Gn,n)}openLink(t,n){return this.request({method:"ui/open-link",params:t},lI,n)}sendOpenLink=this.openLink;requestDisplayMode(t,n){return this.request({method:"ui/request-display-mode",params:t},$I,n)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,n=0,r=0,i=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let o=document.documentElement,s=o.style.width,u=o.style.height;o.style.width="fit-content",o.style.height="fit-content";let l=o.getBoundingClientRect();o.style.width=s,o.style.height=u;let d=window.innerWidth-o.clientWidth,f=Math.ceil(l.width+d),p=Math.ceil(l.height);(f!==n||p!==r)&&(n=f,r=p,this.sendSizeChanged({width:f,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(t=new z$(window.parent,window.parent),n){await super.connect(t);try{let r=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Z$}},II,n);if(r===void 0)throw Error(`Server sent invalid initialize result: ${r}`);this._hostCapabilities=r.hostCapabilities,this._hostInfo=r.hostInfo,this._hostContext=r.hostContext,await this.notification({method:"ui/notifications/initialized"}),this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(r){throw this.close(),r}}}let Ze=null,rt=null,Ie=null,An=null;const Ko=new Map,mi=document.getElementById("loading"),SI=document.getElementById("app"),Tf=document.getElementById("title"),Nf=document.getElementById("subtitle"),xI=document.getElementById("metrics"),zI=document.getElementById("highlights"),bt=document.getElementById("figure-select"),ZI=document.getElementById("figure-title"),UI=document.getElementById("figure-file"),OI=document.getElementById("figure-caption"),$e=document.getElementById("main-image"),ci=document.getElementById("open-standalone"),Zn=document.getElementById("open-monitor-btn"),Ge=document.getElementById("copy-url"),Xo=document.getElementById("related-monitor"),TI=document.getElementById("download-current");function NI(e){const t=String(e.job_type??"train_som"),n=e.grid??[],r=e.quantization_error!=null?Number(e.quantization_error).toFixed(4):"N/A",i=e.topographic_error!=null?Number(e.topographic_error).toFixed(4):"N/A",a=String(e.n_samples??"N/A"),o=String(e.n_features??"N/A"),s=e.coverage??{},u=l=>l!=null?`${(Number(l)*100).toFixed(1)}%`:"N/A";return t==="train_floop_siom"?[{label:"Topology",value:String(e.topology??"free")},{label:"Nodes",value:String(e.occupied_nodes??e.final_length??"N/A")},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:t==="train_siom"?[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:"SIOM"},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:String(e.model??"SOM")},{label:"QE",value:r},{label:"TE",value:i},{label:"Samples",value:a},{label:"Features",value:o}]}function jI(e,t){const n=e.summary??{},r=(n.files??[]).filter(s=>/\.(png|pdf|svg)$/i.test(s)),i=String(n.output_format??"png"),o=(r.length>0?r:[`combined.${i}`]).map(s=>{const u=s.replace(/\.(png|pdf|svg)$/i,""),l=u.startsWith("component_")?"component":u==="combined"||u==="coverage_overview"?"summary":"diagnostic";return{key:u,label:u.replace(/^component_\d+_/,"Component: ").replace(/_/g," "),filename:s,kind:l,caption:/\.(png|jpe?g|gif|webp)$/i.test(s)?void 0:"This figure is available for download, but it is not inline-displayable in this first iteration.",url:/\.(png|jpe?g|gif|webp)$/i.test(s)?`/api/results/${t}/image/${s}`:void 0}});return{type:"results-explorer",jobId:t,title:"Results Explorer",subtitle:String(e.label??""),metrics:NI(n),highlights:[n.training_duration_seconds!=null?`Training duration: ${n.training_duration_seconds}s`:"",n.selected_columns?`Variables: ${n.selected_columns.join(", ")}`:""].filter(Boolean),availableFigures:o,defaultFigureKey:o.find(s=>s.key==="combined"&&s.url)?.key??o.find(s=>!!s.url)?.key??o.find(s=>s.key==="combined")?.key??o[0]?.key,trainingMonitorUrl:`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,standaloneUrl:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`,relatedUrls:{trainingMonitor:`${window.location.origin}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,resultsExplorer:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`}}}function PI(e){xI.innerHTML=e.map(t=>`<div class="metric"><span class="metric-label">${t.label}</span><span class="metric-value">${t.value}</span></div>`).join("")}function EI(e){zI.innerHTML=e.length>0?e.map(t=>`<div class="highlight">${t}</div>`).join(""):'<div class="highlight">No extra highlights for this result yet.</div>'}const DI={summary:"Summary",diagnostic:"Diagnostics",component:"Components",artifact:"Other"},RI=["summary","diagnostic","component","artifact"];function AI(e){const t=new Map;for(const n of e){const r=n.kind??"diagnostic",i=t.get(r)??[];i.push(n),t.set(r,i)}bt.innerHTML="";for(const n of RI){const r=t.get(n);if(!r||r.length===0)continue;const i=document.createElement("optgroup");i.label=DI[n]??n;for(const a of r){const o=document.createElement("option");o.value=a.key,o.textContent=`${a.label} (${a.filename})`,a.key===rt?.key&&(o.selected=!0),i.appendChild(o)}bt.appendChild(i)}}bt.addEventListener("change",()=>{const e=Ze?.availableFigures.find(t=>t.key===bt.value);e&&jf(e)});async function CI(e,t){const n=`${e}/${t}`,r=Ko.get(n);if(r)return r;if(!Ie)return null;try{const a=(await Ie.callServerTool({name:"_fetch_figure",arguments:{job_id:e,filename:t}})).content?.find(o=>o.type==="image");if(a){const o=`data:${a.mimeType};base64,${a.data}`;return Ko.set(n,o),o}}catch{}return null}function LI(e){!Ie||!Ze||Ie.updateModelContext({content:[{type:"text",text:`User is viewing the "${e.label}" figure (${e.filename}) of results job ${Ze.jobId} in the Results Explorer.`}]}).catch(()=>{})}async function jf(e){if(rt=e,ZI.textContent=e.label,UI.textContent=e.filename,OI.textContent=e.caption??"",bt.value=e.key,LI(e),e.url)$e.src=e.url;else if(Ie&&Ze){const t=Ze.defaultFigureKey;if(e.key===t&&An)$e.src=An;else{$e.removeAttribute("src");const n=await CI(Ze.jobId,e.filename);n&&rt?.key===e.key&&($e.src=n)}}else $e.removeAttribute("src")}function MI(e){return e.standaloneUrl??window.location.href}function FI(e){const t=MI(e);e.standaloneUrl?(ci.href=e.standaloneUrl,ci.style.display="inline-flex"):Ef&&(ci.href=window.location.href,ci.style.display="inline-flex"),Ge.style.display="inline-flex",Ge.dataset.url=t;const n=e.relatedUrls?.trainingMonitor;n&&(Xo.href=n,Xo.style.display="inline-flex")}function Yo(e){Ze=e,Tf.textContent=e.title,Nf.textContent=e.subtitle??`Job ${e.jobId}`,PI(e.metrics),EI(e.highlights),FI(e);const t=e.trainingMonitorUrl??e.relatedUrls?.trainingMonitor??`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(e.jobId)}`;Zn.href=t,Zn.style.display="inline-flex",Zn.title="View live or completed training curves for this job";const n=e.availableFigures.find(r=>r.key===e.defaultFigureKey)??e.availableFigures[0]??null;rt=n,AI(e.availableFigures),n&&jf(n),mi.style.display="none",SI.style.display="block"}TI.addEventListener("click",()=>{const e=$e.src;if(!e||!rt)return;const t=document.createElement("a");t.href=e,t.download=rt.filename,t.click()});Ge.addEventListener("click",async()=>{const e=Ge.dataset.url||Ze?.standaloneUrl||window.location.href;try{await navigator.clipboard.writeText(e),Ge.textContent="Copied!",setTimeout(()=>{Ge.textContent="Copy explorer URL"},1500)}catch{window.prompt("Copy explorer URL:",e)}});$e.style.cursor="zoom-in";$e.addEventListener("click",()=>{if(!Ie||!$e.src)return;const e=Ie.getHostContext?.(),t=e?.availableDisplayModes;if(!Array.isArray(t)||!t.includes("fullscreen"))return;const n=e?.displayMode==="fullscreen"?"inline":"fullscreen";Ie.requestDisplayMode({mode:n}).then(r=>{$e.style.cursor=r.mode==="fullscreen"?"zoom-out":"zoom-in"}).catch(()=>{})});const Pf=new URLSearchParams(window.location.search),Ef=Pf.get("mode")==="standalone",di=Pf.get("job_id");if(Ef&&di){const e="The local viz port is assigned per MCP session and changes when the proxy restarts. Re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.";(async()=>{for(let n=1;n<=3;n++)try{const r=await fetch(`/api/results/${di}`);if(r.status===404){mi.innerHTML=`Results for job ${di} were not found (HTTP 404). The job may still be finalizing or was deleted, or this link points at a stale session. ${e}<br><button type="button" id="viz-retry-load" style="margin-top:0.75rem">Retry</button>`,document.getElementById("viz-retry-load")?.addEventListener("click",()=>location.reload());return}if(!r.ok)throw new Error(`HTTP ${r.status}`);const i=await r.json();Yo(jI(i,di));return}catch(r){if(n<3){await new Promise(i=>setTimeout(i,1e3*n));continue}mi.innerHTML=`Could not load results from the local viz server (${r instanceof Error?r.message:"error"}). ${e}<br><button type="button" id="viz-retry-load" style="margin-top:0.75rem">Retry</button>`,document.getElementById("viz-retry-load")?.addEventListener("click",()=>location.reload())}})()}else{const e=new wI({name:"Results Explorer",version:"1.0.0"},{},{autoResize:!0});Ie=e,e.ontoolinput=t=>{const n=t?.arguments??{},r=n.job_id!=null?String(n.job_id):"";r&&(Tf.textContent="Results Explorer",Nf.textContent=`Loading job ${r}...`)},e.ontoolresult=t=>{const n=t.content?.find(i=>i.type==="image");n&&(An=`data:${n.mimeType};base64,${n.data}`);const r=t.structuredContent;if(!r||typeof r!="object"){mi.textContent="The results explorer did not receive structured data.";return}Yo(r)},e.connect()}</script>
|
|
309
309
|
</head>
|
|
310
310
|
<body>
|
|
311
311
|
<div id="loading">Loading results explorer...</div>
|