@barivia/barsom-mcp 0.22.1 → 0.22.4

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.
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { registerAuditedTool } from "../audit.js";
3
- import { apiCall, textResult } from "../shared.js";
3
+ import { apiCall } from "../shared.js";
4
4
  import { buildTrainSubmitExtras } from "../train_submit_message.js";
5
+ import { TRAINING_MONITOR_URI, buildTrainingMonitorResult, } from "./training_monitor.js";
5
6
  import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, assertValidNormalizationArgs, } from "./training_core.js";
6
7
  export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Returns a job_id; poll with jobs(action=status) and then results(action=get). All actions are async.
7
8
 
@@ -89,7 +90,12 @@ async function submitTrainJob(opts) {
89
90
  }
90
91
  data.message = msg;
91
92
  data.suggested_next_step = extras.suggested_next_step;
92
- return textResult(data);
93
+ const monitor = await buildTrainingMonitorResult(newJobId, { preamble: msg });
94
+ return {
95
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }, ...monitor.content],
96
+ structuredContent: monitor.structuredContent,
97
+ _meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
98
+ };
93
99
  }
94
100
  /**
95
101
  * Core training dispatcher. Shared by the `train` tool and the deprecated
@@ -98,7 +104,7 @@ async function submitTrainJob(opts) {
98
104
  export async function runTrain(action, args) {
99
105
  const dataset_id = args.dataset_id;
100
106
  if (action === "map" || action === "siom_map" || action === "impute") {
101
- const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, emit_cell_uncertainty, } = args;
107
+ const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, viz_upload_cyclic_components, emit_cell_uncertainty, } = args;
102
108
  let PRESETS = {};
103
109
  try {
104
110
  PRESETS = await fetchTrainingPresets();
@@ -123,7 +129,8 @@ export async function runTrain(action, args) {
123
129
  temporal_features, feature_weights, transforms, auto_log_transforms,
124
130
  time_delay_embeddings, categorical_features,
125
131
  normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend,
126
- output_format, output_dpi, colormap, row_range,
132
+ output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size,
133
+ viz_upload_cyclic_components,
127
134
  }, PRESETS);
128
135
  if (action === "impute") {
129
136
  params._job_type = "train_impute";
@@ -113,6 +113,8 @@ export const TRAINING_INPUT_SCHEMA = {
113
113
  .describe("Visualization density; auto-capped when feature count > 40"),
114
114
  viz_top_components: z.number().int().min(0).max(64).optional()
115
115
  .describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
116
+ viz_upload_cyclic_components: z.boolean().optional()
117
+ .describe("When true with individual component PNGs, also publish cos/sin cyclic expansion planes (default false; recovered hour/month panels stay in combined.png)"),
116
118
  emit_cell_uncertainty: z.boolean().optional()
117
119
  .describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
118
120
  gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
@@ -245,7 +247,7 @@ export function buildFloopParams(args) {
245
247
  return { params, paramSummary };
246
248
  }
247
249
  export function buildTrainMapParams(args, presets) {
248
- const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
250
+ const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, viz_upload_cyclic_components, } = args;
249
251
  const p = preset ? presets[preset] : undefined;
250
252
  const params = {
251
253
  model: model ?? "SOM",
@@ -308,6 +310,9 @@ export function buildTrainMapParams(args, presets) {
308
310
  params.siom_qe_backend = siom_qe_backend;
309
311
  if (siom_qe_batch_size !== undefined)
310
312
  params.siom_qe_batch_size = siom_qe_batch_size;
313
+ if (viz_upload_cyclic_components !== undefined) {
314
+ params.viz_upload_cyclic_components = viz_upload_cyclic_components;
315
+ }
311
316
  const effectiveGrid = params.grid;
312
317
  const effectiveEpochs = params.epochs;
313
318
  const effectiveBatch = params.batch_size;
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { runMcpToolAudit } from "../audit.js";
4
- import { apiCall, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
4
+ import { apiCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
5
+ import { appendUiDeliveryContent, beginInlineFallback, resolveUiDelivery, tryAttachLearningCurve, } from "../ui-delivery.js";
5
6
  import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
7
+ import { buildVizStandaloneUrl, resultsExplorerStandaloneLinkText, } from "../viz_links.js";
6
8
  export const TRAINING_MONITOR_URI = "ui://barsom/training-monitor";
7
9
  export const TRAINING_MONITOR_REFRESH_MS = 5000;
8
10
  function hasCurveArrays(data) {
@@ -14,8 +16,12 @@ function hasCurveArrays(data) {
14
16
  ];
15
17
  return keys.some((k) => Array.isArray(data[k]) && data[k].length > 0);
16
18
  }
17
- function buildStructuredContent(job_id, data) {
19
+ function buildStructuredContent(job_id, data, port = getVizPort()) {
18
20
  const id = String(data.id ?? job_id);
21
+ const standaloneUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.trainingMonitor, id) ??
22
+ buildVizStandaloneUrl("training-monitor", id);
23
+ const resultsExplorerUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.resultsExplorer, id) ??
24
+ buildVizStandaloneUrl("results-explorer", id);
19
25
  return {
20
26
  ...data,
21
27
  type: "training-monitor",
@@ -23,6 +29,9 @@ function buildStructuredContent(job_id, data) {
23
29
  id,
24
30
  job_id: id,
25
31
  refresh_interval_ms: TRAINING_MONITOR_REFRESH_MS,
32
+ standaloneUrl,
33
+ resultsExplorerUrl,
34
+ relatedUrls: buildVizRelatedUrls(port, id, BARSOM_VIZ_VIEWS),
26
35
  };
27
36
  }
28
37
  function hasTeCurveArrays(data) {
@@ -81,6 +90,71 @@ async function enrichWithTrainingLog(job_id, data) {
81
90
  return data;
82
91
  }
83
92
  }
93
+ export async function buildTrainingMonitorResult(job_id, opts) {
94
+ const { fetch_training_log, preamble } = opts ?? {};
95
+ let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
96
+ const jobStatus = String(data.status ?? "");
97
+ if (fetch_training_log || needsTrainingLogEnrichment(data)) {
98
+ data = await enrichWithTrainingLog(job_id, data);
99
+ }
100
+ const port = getVizPort();
101
+ const structuredContent = buildStructuredContent(job_id, data, port);
102
+ const status = jobStatus;
103
+ const progress = (data.progress ?? 0) * 100;
104
+ const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
105
+ const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
106
+ const epoch = data.epoch != null ? Number(data.epoch) : null;
107
+ const totalEpochs = data.total_epochs != null ? Number(data.total_epochs) : null;
108
+ const timingParts = [];
109
+ if (elapsedSec != null && elapsedSec >= 0)
110
+ timingParts.push(`elapsed ${Math.round(elapsedSec)}s`);
111
+ if (etaSec != null && etaSec > 0)
112
+ timingParts.push(`ETA ~${Math.round(etaSec)}s`);
113
+ if (epoch != null && totalEpochs != null)
114
+ timingParts.push(`epoch ${epoch}/${totalEpochs}`);
115
+ const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
116
+ const text = (preamble ? `${preamble}\n\n` : "") +
117
+ `Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
118
+ `Optional UI; jobs(action=status) is enough to finish the workflow headless.`;
119
+ const content = [{ type: "text", text }];
120
+ const terminal = status === "completed" || status === "failed" || status === "cancelled";
121
+ let inlineImageAttached = false;
122
+ if (terminal) {
123
+ inlineImageAttached = await tryAttachLearningCurve(content, job_id);
124
+ }
125
+ const uiDelivery = resolveUiDelivery({
126
+ tool: "training_monitor",
127
+ jobId: job_id,
128
+ jobStatus: status,
129
+ inlineImageAttached,
130
+ });
131
+ appendUiDeliveryContent(content, uiDelivery, {
132
+ linkLabel: "Open training monitor",
133
+ primaryUrl: structuredContent.standaloneUrl,
134
+ });
135
+ if (status === "completed" && uiDelivery.urls?.length) {
136
+ const resultsUrl = uiDelivery.urls.find((u) => u.includes("results-explorer"));
137
+ if (resultsUrl) {
138
+ content.push({
139
+ type: "text",
140
+ text: `Job completed — browse map figures: [Open results explorer](${resultsUrl})`,
141
+ });
142
+ }
143
+ }
144
+ structuredContent.ui_delivery = uiDelivery;
145
+ return { structuredContent, content, text };
146
+ }
147
+ /** After terminal + finalize, append results explorer open hint (monitor completion path). */
148
+ export function appendResultsExplorerCompletionHint(content, job_id) {
149
+ const link = resultsExplorerStandaloneLinkText(job_id);
150
+ if (link) {
151
+ content.push({
152
+ type: "text",
153
+ text: `${link}\n` +
154
+ `AGENT: when the job is fully complete (finalize done if any), call results_explorer(job_id="${job_id}") in the same turn and surface this link — do not wait for the user to ask.`,
155
+ });
156
+ }
157
+ }
84
158
  export function registerTrainingMonitorTool(server) {
85
159
  registerAppTool(server, "training_monitor", {
86
160
  title: "Training Monitor",
@@ -94,42 +168,11 @@ export function registerTrainingMonitorTool(server) {
94
168
  },
95
169
  _meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
96
170
  }, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
171
+ beginInlineFallback();
97
172
  const { job_id, fetch_training_log } = args;
98
- let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
99
- const jobStatus = String(data.status ?? "");
100
- if (fetch_training_log || needsTrainingLogEnrichment(data)) {
101
- data = await enrichWithTrainingLog(job_id, data);
102
- }
103
- const structuredContent = buildStructuredContent(job_id, data);
104
- const status = jobStatus;
105
- const progress = (data.progress ?? 0) * 100;
106
- const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
107
- const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
108
- const epoch = data.epoch != null ? Number(data.epoch) : null;
109
- const totalEpochs = data.total_epochs != null ? Number(data.total_epochs) : null;
110
- const timingParts = [];
111
- if (elapsedSec != null && elapsedSec >= 0)
112
- timingParts.push(`elapsed ${Math.round(elapsedSec)}s`);
113
- if (etaSec != null && etaSec > 0)
114
- timingParts.push(`ETA ~${Math.round(etaSec)}s`);
115
- if (epoch != null && totalEpochs != null)
116
- timingParts.push(`epoch ${epoch}/${totalEpochs}`);
117
- const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
118
- const text = `Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
119
- `Optional UI; jobs(action=status) is enough to finish the workflow.`;
120
- const content = [{ type: "text", text }];
121
- const port = getVizPort();
122
- const standaloneUrl = port
123
- ? `http://localhost:${port}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(job_id)}`
124
- : undefined;
125
- if (!getClientSupportsMcpApps() && standaloneUrl) {
126
- content.push({
127
- type: "text",
128
- text: `Live training monitor: [Open training monitor](${standaloneUrl})\n` +
129
- `AGENT: surface this to the user as a clickable markdown link (e.g. "[Open training monitor](${standaloneUrl})") in your reply — do not leave it buried in tool output. Re-post the link after the job completes so the user can return to the curves.\n` +
130
- `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)}`,
131
- });
132
- }
173
+ const { structuredContent, content, text } = await buildTrainingMonitorResult(job_id, {
174
+ fetch_training_log,
175
+ });
133
176
  return {
134
177
  ...structuredTextResult(structuredContent, text, content),
135
178
  _meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
@@ -1,17 +1,15 @@
1
- import { getVizPort } from "./shared.js";
1
+ import { buildVizStandaloneUrl } from "./viz_links.js";
2
2
  export function buildTrainSubmitExtras(opts) {
3
3
  const { newJobId, totalRows, hasTransforms, prepareJobId, variantPrefix, paramSummary, impute, resultsHint } = opts;
4
- const monitorUrl = getVizPort() > 0
5
- ? `http://localhost:${getVizPort()}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(newJobId)}`
6
- : null;
4
+ const monitorUrl = buildVizStandaloneUrl("training-monitor", newJobId);
7
5
  let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
8
6
  if (prepareJobId) {
9
7
  msg += `Preprocessing job prepare_training_matrix (${prepareJobId}) runs on worker-io first; train job ${newJobId} starts after it completes. Poll jobs(action=status, job_id="${prepareJobId}") until completed, then poll the train job. `;
10
8
  }
11
- msg += `Recommended: training_monitor(job_id="${newJobId}")`;
9
+ msg += `Training monitor opened for job ${newJobId}`;
12
10
  if (monitorUrl)
13
- msg += ` or open ${monitorUrl}`;
14
- msg += ` while polling jobs(action=status, job_id="${newJobId}") every 60–120s for large jobs. `;
11
+ msg += ` standalone: ${monitorUrl}`;
12
+ msg += `. Poll jobs(action=status, job_id="${newJobId}") every 60–120s for large jobs if needed. `;
15
13
  msg += `When status is completed, check jobs(action=status) for finalize_job_id — if present, poll that finalize_training job before results(action=get). `;
16
14
  msg += `Then use results(action=get, job_id="${newJobId}")`;
17
15
  if (resultsHint) {
@@ -83,3 +83,35 @@ export function lastEpochTeFromCurves(data) {
83
83
  }
84
84
  return null;
85
85
  }
86
+ export function isTerminalJobStatus(status) {
87
+ return status === "completed" || status === "failed" || status === "cancelled";
88
+ }
89
+ /** True when compute + finalize (if any) are done — safe to open results_explorer. */
90
+ export function isFullyComplete(data) {
91
+ const status = String(data.status ?? "");
92
+ if (status !== "completed")
93
+ return isTerminalJobStatus(status);
94
+ const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
95
+ ? String(data.finalize_job_id)
96
+ : null;
97
+ if (!finalizeId)
98
+ return true;
99
+ return String(data.finalize_status ?? "") === "completed";
100
+ }
101
+ /** Keep polling while finalize is still running after parent compute completed. */
102
+ export function shouldKeepPollingJob(data) {
103
+ const status = String(data.status ?? "");
104
+ if (!isTerminalJobStatus(status))
105
+ return true;
106
+ if (status !== "completed")
107
+ return false;
108
+ if (data.finalize_failed === true)
109
+ return false;
110
+ const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
111
+ ? String(data.finalize_job_id)
112
+ : null;
113
+ const finalizeStatus = data.finalize_status != null && String(data.finalize_status) !== ""
114
+ ? String(data.finalize_status)
115
+ : null;
116
+ return finalizeId != null && finalizeStatus !== "completed";
117
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Client-capability adaptation for MCP App tools (training_monitor, results_explorer).
3
+ * Respects BARIVIA_UI_DELIVERY override and emits a structured block agents can rely on.
4
+ */
5
+ import { apiRawCall, getClientSupportsMcpApps, getVizPort, mimeForFilename, resetInlineAttachBudget, } from "./shared.js";
6
+ import { buildVizStandaloneUrl } from "./viz_links.js";
7
+ export function parseUiDeliveryOverride() {
8
+ const raw = (process.env.BARIVIA_UI_DELIVERY ?? "auto").trim().toLowerCase();
9
+ if (raw === "apps" || raw === "localhost" || raw === "inline" || raw === "auto") {
10
+ return raw;
11
+ }
12
+ return "auto";
13
+ }
14
+ function vizPortPinned() {
15
+ return Boolean(process.env.BARIVIA_VIZ_PORT?.trim());
16
+ }
17
+ function viewForTool(tool) {
18
+ return tool === "training_monitor" ? "training-monitor" : "results-explorer";
19
+ }
20
+ /** Prefer viz_links (single URL builder) so train/monitor/explorer stay consistent. */
21
+ export function buildStandaloneVizUrl(view, jobId) {
22
+ return buildVizStandaloneUrl(viewForTool(view), jobId);
23
+ }
24
+ export function buildHealthUrl(jobId) {
25
+ const port = getVizPort();
26
+ if (!port)
27
+ return undefined;
28
+ const base = `http://localhost:${port}/api/health`;
29
+ return jobId ? `${base}?job_id=${encodeURIComponent(jobId)}` : base;
30
+ }
31
+ function hintForTier(tier, tool, opts) {
32
+ const label = tool === "training_monitor" ? "training monitor" : "results explorer";
33
+ switch (tier) {
34
+ case "embedded":
35
+ return `The ${label} MCP App panel is embedded in this client. Also mention the standalone URL if the user may open a browser tab.`;
36
+ case "localhost":
37
+ return opts.standaloneUrl
38
+ ? `AGENT: Post a clickable markdown link to the user (e.g. "[Open ${label}](${opts.standaloneUrl})") — do not bury it in tool output or use broken workspace file paths. Re-post after the job completes if status was running.`
39
+ : `AGENT: Viz server unavailable — use jobs(status) and results(get) for progress and figures.`;
40
+ case "inline_image":
41
+ return `AGENT: Inline raster figure(s) are attached below. Summarize metrics in your reply; offer results_explorer or results(get) for additional figures.`;
42
+ case "text_only":
43
+ return `AGENT: No embedded panel or localhost viz — use jobs(status) for training progress and results(get) or results_explorer for figures.`;
44
+ }
45
+ }
46
+ export function resolveUiDelivery(options) {
47
+ const override = parseUiDeliveryOverride();
48
+ const mcpApps = getClientSupportsMcpApps();
49
+ const port = getVizPort() || null;
50
+ const standaloneUrl = buildStandaloneVizUrl(options.tool, options.jobId);
51
+ let delivery;
52
+ if (override === "apps") {
53
+ delivery = mcpApps ? "embedded" : port ? "localhost" : "text_only";
54
+ }
55
+ else if (override === "localhost") {
56
+ delivery = port ? "localhost" : options.inlineImageAttached ? "inline_image" : "text_only";
57
+ }
58
+ else if (override === "inline") {
59
+ delivery = options.inlineImageAttached ? "inline_image" : "text_only";
60
+ }
61
+ else {
62
+ // auto
63
+ if (mcpApps) {
64
+ delivery = "embedded";
65
+ }
66
+ else if (port) {
67
+ delivery = "localhost";
68
+ }
69
+ else if (options.inlineImageAttached) {
70
+ delivery = "inline_image";
71
+ }
72
+ else {
73
+ delivery = "text_only";
74
+ }
75
+ }
76
+ const urls = [];
77
+ if (standaloneUrl && (delivery === "embedded" || delivery === "localhost")) {
78
+ urls.push(standaloneUrl);
79
+ }
80
+ const healthUrl = buildHealthUrl(options.jobId);
81
+ if (healthUrl && delivery === "localhost") {
82
+ urls.push(healthUrl);
83
+ }
84
+ const crossLink = options.tool === "training_monitor" && options.jobStatus === "completed"
85
+ ? buildStandaloneVizUrl("results_explorer", options.jobId)
86
+ : options.tool === "results_explorer"
87
+ ? buildStandaloneVizUrl("training_monitor", options.jobId)
88
+ : undefined;
89
+ if (crossLink && !urls.includes(crossLink)) {
90
+ urls.push(crossLink);
91
+ }
92
+ return {
93
+ delivery,
94
+ urls: urls.length > 0 ? urls : undefined,
95
+ hint_for_agent: hintForTier(delivery, options.tool, { standaloneUrl, jobStatus: options.jobStatus }),
96
+ mcp_apps_supported: mcpApps,
97
+ viz_port: port,
98
+ port_pinned: vizPortPinned(),
99
+ override,
100
+ };
101
+ }
102
+ /**
103
+ * Prepend prominent standalone URL (hosts may advertise Apps but not mount them),
104
+ * then structured ui_delivery JSON + agent hint.
105
+ */
106
+ export function appendUiDeliveryContent(content, block, options) {
107
+ const primary = options?.primaryUrl ?? block.urls?.[0];
108
+ const label = options?.linkLabel ?? "Open interactive view";
109
+ const prefix = [];
110
+ if (primary) {
111
+ const embedNote = block.delivery === "embedded"
112
+ ? "Embedded panel if your host supports MCP Apps — otherwise open the standalone URL below (expected on some Cursor builds)."
113
+ : "Open the standalone URL below when no inline panel appears.";
114
+ prefix.push({
115
+ type: "text",
116
+ text: `${embedNote}\n` +
117
+ `Standalone URL (copy if Open is blocked):\n${primary}\n` +
118
+ `[${label}](${primary})\n` +
119
+ `${block.hint_for_agent}`,
120
+ });
121
+ }
122
+ prefix.push({
123
+ type: "text",
124
+ text: "```json\n" + JSON.stringify({ ui_delivery: block }, null, 2) + "\n```",
125
+ });
126
+ if (primary && !block.port_pinned && block.delivery !== "text_only") {
127
+ prefix.push({
128
+ type: "text",
129
+ text: "Tip: set BARIVIA_VIZ_PORT in MCP env for a session-stable localhost port; re-run this tool if the page stops updating.",
130
+ });
131
+ }
132
+ content.unshift(...prefix);
133
+ }
134
+ export async function tryAttachLearningCurve(content, jobId) {
135
+ for (const ext of ["png", "svg"]) {
136
+ try {
137
+ const filename = `learning_curve.${ext}`;
138
+ const { data: buf } = await apiRawCall(`/v1/results/${jobId}/image/${filename}`);
139
+ content.push({
140
+ type: "image",
141
+ data: buf.toString("base64"),
142
+ mimeType: mimeForFilename(filename),
143
+ annotations: { audience: ["user"], priority: 0.75 },
144
+ });
145
+ return true;
146
+ }
147
+ catch {
148
+ continue;
149
+ }
150
+ }
151
+ return false;
152
+ }
153
+ export function getMcpClientDiagnostics() {
154
+ const override = parseUiDeliveryOverride();
155
+ const mcpApps = getClientSupportsMcpApps();
156
+ const port = getVizPort() || null;
157
+ let activeTier = "text_only";
158
+ if (override === "apps") {
159
+ activeTier = mcpApps ? "embedded" : port ? "localhost" : "text_only";
160
+ }
161
+ else if (override === "localhost") {
162
+ activeTier = port ? "localhost" : "text_only";
163
+ }
164
+ else if (override === "inline") {
165
+ activeTier = "inline_image";
166
+ }
167
+ else if (mcpApps) {
168
+ activeTier = "embedded";
169
+ }
170
+ else if (port) {
171
+ activeTier = "localhost";
172
+ }
173
+ return {
174
+ mcp_apps_supported: mcpApps,
175
+ viz_port: port,
176
+ viz_port_pinned: vizPortPinned(),
177
+ ui_delivery_override: override,
178
+ active_delivery_tier: activeTier,
179
+ health_url: buildHealthUrl(),
180
+ };
181
+ }
182
+ /** Reset inline budget at tool entry when attaching fallback images. */
183
+ export function beginInlineFallback() {
184
+ resetInlineAttachBudget();
185
+ }