@barivia/barsom-mcp 0.10.0 → 0.10.3
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 +118 -0
- package/dist/logger.js +40 -0
- package/dist/prepare_training_prompt.js +1 -1
- package/dist/shared.js +14 -4
- package/dist/tools/account.js +2 -1
- package/dist/tools/datasets.js +7 -4
- package/dist/tools/explore_map.js +4 -3
- package/dist/tools/feedback.js +2 -1
- package/dist/tools/guide_barsom.js +2 -1
- package/dist/tools/inference.js +2 -1
- package/dist/tools/jobs.js +25 -8
- package/dist/tools/results.js +43 -3
- package/dist/tools/training_guidance.js +2 -1
- package/dist/tools/training_monitor.js +4 -2
- package/dist/tools/training_prep.js +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,11 +97,12 @@ Call at the **start of mapping work** (or when the user asks what the MCP can do
|
|
|
97
97
|
|
|
98
98
|
| Action | Use when |
|
|
99
99
|
|--------|----------|
|
|
100
|
-
| `train_map` | Submitting a new map training job — full control: model type, grid, epochs, cyclic/temporal features, transforms. Returns `job_id`; poll with `jobs(action=status, job_id=...)`. |
|
|
100
|
+
| `train_map` | Submitting a new map training job — full control: model type, grid, epochs, cyclic/temporal features, transforms. Returns `job_id`; poll with `jobs(action=status, job_id=...)`. Complete-case data only (no NaNs in training columns). |
|
|
101
|
+
| `train_impute` | Sparse training data: accelerated missSOM / SIOM missSOM — trains a map and imputes missing cells in one job. Defaults: `model=auto`, `cv_folds=5`. Returns map artifacts + `imputed.csv`, `imputation_mask.csv`, optional `quality.csv` / `imputation_uncertainty.csv`. Plain numeric columns only. Valid parent for `inference(impute_column)`. |
|
|
101
102
|
| `train_siom_map` | Submitting a self-interacting map training job — same grid-map workflow plus SIOM controls such as `gamma`, `siom_decay`, and penalty selection. |
|
|
102
103
|
| `train_floop_siom` | Submitting a FLooP-SIOM job — growing node-budget manifold; default `topology=free` (CHL); optional `topology=chain` for strict 1D linked list. |
|
|
103
104
|
| `train_floop_chain` | Deprecated alias for `train_floop_siom` — same behavior. |
|
|
104
|
-
| `status` | Polling after any async job — every 10–15s |
|
|
105
|
+
| `status` | Polling after any async job — every 10–15s; map jobs expose `progress_phase` (ordering, convergence, cv, artifacts) |
|
|
105
106
|
| `list` | Finding job IDs, checking pipeline state |
|
|
106
107
|
| `compare` | Picking the best run from a set (QE, TE, silhouette table) |
|
|
107
108
|
| `cancel` | Stopping a running job |
|
|
@@ -127,7 +128,7 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
|
|
|
127
128
|
| Action | Output | Timing |
|
|
128
129
|
|--------|--------|--------|
|
|
129
130
|
| `predict` | Score rows against the trained map. **Inputs:** `dataset_id` (defaults to the parent training dataset) **or** inline `rows` (≤500). **Output style** (`output` param): `"compact"` → `predictions.csv` (row_id, bmu_x/y, bmu_node_index, cluster_id [+ QE / qe_p95 / potential_anomaly when scoring **new** data]); `"annotated"` → `annotated.csv` (original CSV + BMU columns appended). **Regime auto-detected:** when the resolved dataset matches the training dataset, QE columns are intentionally omitted in compact output (training-set fit ≠ generalisation; the p95 anomaly flag would be circular). Prefer `dataset_id` for batches and SIOM/irregular maps. | 5–120s |
|
|
130
|
-
| `impute_column` | Fill a numeric **target_column** not used in training: **requires** `dataset_id` + `target_column`. Dataset must contain all training features plus the target. Pools observed target values from rows mapped to this row's BMU and topology neighbors (BMU + neighbors, often 7 nodes on hex interior; fewer on borders unless the map is periodic). `only_missing` (default true); `impute_aggregation`: mean or median. **Not** held-out validated — map-local estimate. Output **`imputed.csv`**. | 5–120s |
|
|
131
|
+
| `impute_column` | Fill a numeric **target_column** not used in training: **requires** `dataset_id` + `target_column`. Parent job: completed **train_map** or **train_impute**. Dataset must contain all training features plus the target. Pools observed target values from rows mapped to this row's BMU and topology neighbors (BMU + neighbors, often 7 nodes on hex interior; fewer on borders unless the map is periodic). `only_missing` (default true); `impute_aggregation`: mean or median. **Not** held-out validated — map-local estimate. Output **`imputed.csv`**. For holes across many training columns, prefer **jobs(train_impute)** first. | 5–120s |
|
|
131
132
|
| `compare` | density-diff heatmap + top gained/lost nodes — drift, A/B, cohort | 30–120s |
|
|
132
133
|
| `project_columns` | Project one or more dataset columns onto the trained map (component planes) | async |
|
|
133
134
|
| `report` | Report **manifest** (figure names, download URLs, metrics, cluster summary) — sync; use with `results(download)` on the training `job_id` for `report.pdf` when present; build custom PDFs in Quarto/Jupyter | immediate |
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool audit wrapper — tool name, action, latency, outcome; no secrets.
|
|
3
|
+
*/
|
|
4
|
+
import { logAudit } from "./logger.js";
|
|
5
|
+
/** Keys safe to include in audit param summaries (no paths, keys, or bodies). */
|
|
6
|
+
const AUDIT_PARAM_KEYS = new Set([
|
|
7
|
+
"action",
|
|
8
|
+
"job_type",
|
|
9
|
+
"model",
|
|
10
|
+
"preset",
|
|
11
|
+
"backend",
|
|
12
|
+
"output_format",
|
|
13
|
+
"figures",
|
|
14
|
+
"job_id",
|
|
15
|
+
"dataset_id",
|
|
16
|
+
"compare",
|
|
17
|
+
"topology",
|
|
18
|
+
"viz_mode",
|
|
19
|
+
]);
|
|
20
|
+
function scrubParams(args) {
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [k, v] of Object.entries(args)) {
|
|
23
|
+
if (!AUDIT_PARAM_KEYS.has(k))
|
|
24
|
+
continue;
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
continue;
|
|
27
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
28
|
+
out[k] = v;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function extractIds(result) {
|
|
34
|
+
const ids = {};
|
|
35
|
+
if (result === null || typeof result !== "object")
|
|
36
|
+
return ids;
|
|
37
|
+
const r = result;
|
|
38
|
+
if (typeof r.job_id === "string")
|
|
39
|
+
ids.job_id = r.job_id;
|
|
40
|
+
if (typeof r.id === "string" && !ids.job_id)
|
|
41
|
+
ids.job_id = r.id;
|
|
42
|
+
if (typeof r.dataset_id === "string")
|
|
43
|
+
ids.dataset_id = r.dataset_id;
|
|
44
|
+
const sc = r.structuredContent;
|
|
45
|
+
if (sc && typeof sc === "object") {
|
|
46
|
+
const s = sc;
|
|
47
|
+
if (typeof s.jobId === "string")
|
|
48
|
+
ids.job_id = s.jobId;
|
|
49
|
+
if (typeof s.datasetId === "string")
|
|
50
|
+
ids.dataset_id = s.datasetId;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const text = JSON.stringify(result);
|
|
54
|
+
const jobMatch = text.match(/"job_id"\s*:\s*"([a-f0-9-]{36})"/i);
|
|
55
|
+
if (jobMatch && !ids.job_id)
|
|
56
|
+
ids.job_id = jobMatch[1];
|
|
57
|
+
const dsMatch = text.match(/"dataset_id"\s*:\s*"([a-f0-9-]{36})"/i);
|
|
58
|
+
if (dsMatch && !ids.dataset_id)
|
|
59
|
+
ids.dataset_id = dsMatch[1];
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* ignore */
|
|
63
|
+
}
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
66
|
+
function errorCodeFrom(err) {
|
|
67
|
+
if (err && typeof err === "object") {
|
|
68
|
+
const e = err;
|
|
69
|
+
if (e.httpStatus !== undefined)
|
|
70
|
+
return `http_${e.httpStatus}`;
|
|
71
|
+
const m = e.message ?? "";
|
|
72
|
+
const codeMatch = m.match(/error_code:\s*(\w+)/);
|
|
73
|
+
if (codeMatch)
|
|
74
|
+
return codeMatch[1];
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Run a tool handler with structured audit logging.
|
|
80
|
+
*/
|
|
81
|
+
export async function runMcpToolAudit(tool, action, args, handler) {
|
|
82
|
+
const t0 = Date.now();
|
|
83
|
+
const params = scrubParams(args);
|
|
84
|
+
try {
|
|
85
|
+
const result = await handler();
|
|
86
|
+
const ids = extractIds(result);
|
|
87
|
+
logAudit({
|
|
88
|
+
tool,
|
|
89
|
+
action,
|
|
90
|
+
duration_ms: Date.now() - t0,
|
|
91
|
+
outcome: "ok",
|
|
92
|
+
...ids,
|
|
93
|
+
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
94
|
+
});
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
logAudit({
|
|
99
|
+
tool,
|
|
100
|
+
action,
|
|
101
|
+
duration_ms: Date.now() - t0,
|
|
102
|
+
outcome: "error",
|
|
103
|
+
error_code: errorCodeFrom(err),
|
|
104
|
+
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
105
|
+
});
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Register an MCP tool with structured audit logging on each invocation.
|
|
111
|
+
*/
|
|
112
|
+
export function registerAuditedTool(server, name, description, schema, handler) {
|
|
113
|
+
server.tool(name, description, schema, (async (args) => {
|
|
114
|
+
const rec = args;
|
|
115
|
+
const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
|
|
116
|
+
return runMcpToolAudit(name, action, rec, () => handler(args));
|
|
117
|
+
}));
|
|
118
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured stdout/stderr logging for the MCP proxy (Docker json-file friendly).
|
|
3
|
+
*/
|
|
4
|
+
const SERVICE_NAME = process.env.BARIVIA_SERVICE_NAME ?? process.env.BARSOM_SERVICE_NAME ?? "mcp-proxy";
|
|
5
|
+
const LOG_FORMAT = (process.env.BARIVIA_LOG_FORMAT ?? process.env.BARSOM_LOG_FORMAT ?? "text").toLowerCase();
|
|
6
|
+
function emit(fields) {
|
|
7
|
+
const level = fields.level ?? "info";
|
|
8
|
+
const payload = {
|
|
9
|
+
ts: new Date().toISOString(),
|
|
10
|
+
level,
|
|
11
|
+
service: SERVICE_NAME,
|
|
12
|
+
...fields,
|
|
13
|
+
};
|
|
14
|
+
delete payload.level;
|
|
15
|
+
if (LOG_FORMAT === "json") {
|
|
16
|
+
console.error(JSON.stringify(payload));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const parts = [`${payload.ts} ${level.toUpperCase().padEnd(5)} ${fields.msg}`];
|
|
20
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
21
|
+
if (k === "ts" || k === "msg" || k === "service")
|
|
22
|
+
continue;
|
|
23
|
+
if (v !== undefined && v !== null && v !== "") {
|
|
24
|
+
parts.push(`${k}=${String(v)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
console.error(parts.join(" | "));
|
|
28
|
+
}
|
|
29
|
+
export function logInfo(msg, fields = {}) {
|
|
30
|
+
emit({ ...fields, level: "info", msg });
|
|
31
|
+
}
|
|
32
|
+
export function logWarn(msg, fields = {}) {
|
|
33
|
+
emit({ ...fields, level: "warn", msg });
|
|
34
|
+
}
|
|
35
|
+
export function logError(msg, fields = {}) {
|
|
36
|
+
emit({ ...fields, level: "error", msg });
|
|
37
|
+
}
|
|
38
|
+
export function logAudit(fields) {
|
|
39
|
+
emit({ ...fields, event: "mcp_tool_call", level: "info", msg: "mcp_tool_call" });
|
|
40
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiCall } from "./shared.js";
|
|
2
|
-
const FALLBACK_PREFIX = "Please run datasets(action=preview, dataset_id=\"{id}\") to inspect columns, then datasets(action=analyze, dataset_id=\"{id}\") to see which columns and temporal periods are most informative. Then choose the training path: jobs(action=train_map, dataset_id=\"{id}\", ...) for
|
|
2
|
+
const FALLBACK_PREFIX = "Before upload: for jobs(action=train_map) ensure training columns have no NaNs; for jobs(action=train_impute) missing cells in training columns are OK (plain numeric only). Please run datasets(action=preview, dataset_id=\"{id}\") to inspect columns, then datasets(action=analyze, dataset_id=\"{id}\") to see which columns and temporal periods are most informative. Then choose the training path: jobs(action=train_map, dataset_id=\"{id}\", ...) for complete-case data, jobs(action=train_impute, dataset_id=\"{id}\", ...) for sparse matrices, jobs(action=train_siom_map, dataset_id=\"{id}\", ...) for a fixed-grid SIOM, or jobs(action=train_floop_siom, dataset_id=\"{id}\", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain).";
|
|
3
3
|
/** Used by the `prepare_training` MCP prompt; prefers tier-scoped text from the API when online. */
|
|
4
4
|
export async function resolvePrepareTrainingPromptText(datasetId) {
|
|
5
5
|
let promptText = FALLBACK_PREFIX.replaceAll("{id}", datasetId);
|
package/dist/shared.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { logInfo } from "./logger.js";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Config
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -19,7 +20,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
|
19
20
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
20
21
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
21
22
|
*/
|
|
22
|
-
export const CLIENT_VERSION = "0.10.
|
|
23
|
+
export const CLIENT_VERSION = "0.10.3";
|
|
23
24
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
24
25
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
25
26
|
/** Poll window for datasets(add_expression) / derive jobs (server-side work can exceed 30s). */
|
|
@@ -277,7 +278,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
277
278
|
}
|
|
278
279
|
const effectiveTimeout = requestTimeoutMs ?? FETCH_TIMEOUT_MS;
|
|
279
280
|
const t0 = Date.now();
|
|
280
|
-
|
|
281
|
+
logInfo("API request", { rid: requestId, method, path });
|
|
281
282
|
let lastError;
|
|
282
283
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
283
284
|
try {
|
|
@@ -292,10 +293,19 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
292
293
|
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
|
|
293
294
|
continue;
|
|
294
295
|
}
|
|
295
|
-
|
|
296
|
+
logInfo("API response", {
|
|
297
|
+
rid: requestId,
|
|
298
|
+
outcome: "error",
|
|
299
|
+
duration_ms: Date.now() - t0,
|
|
300
|
+
error_code: `http_${resp.status}`,
|
|
301
|
+
});
|
|
296
302
|
throwApiError(resp.status, text, requestId);
|
|
297
303
|
}
|
|
298
|
-
|
|
304
|
+
logInfo("API response", {
|
|
305
|
+
rid: requestId,
|
|
306
|
+
outcome: "ok",
|
|
307
|
+
duration_ms: Date.now() - t0,
|
|
308
|
+
});
|
|
299
309
|
return JSON.parse(text);
|
|
300
310
|
}
|
|
301
311
|
catch (err) {
|
package/dist/tools/account.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { registerAuditedTool } from "../audit.js";
|
|
2
3
|
import { apiCall } from "../shared.js";
|
|
3
4
|
export function registerAccountTool(server) {
|
|
4
|
-
server
|
|
5
|
+
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info, request cloud burst compute, view billing history.
|
|
5
6
|
|
|
6
7
|
| Action | Use when |
|
|
7
8
|
|--------|----------|
|
package/dist/tools/datasets.js
CHANGED
|
@@ -2,9 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { registerAuditedTool } from "../audit.js";
|
|
5
6
|
import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, POLL_DERIVE_MAX_MS, UPLOAD_DATASET_TIMEOUT_MS, } from "../shared.js";
|
|
6
7
|
export function registerDatasetsTool(server) {
|
|
7
|
-
server
|
|
8
|
+
registerAuditedTool(server, "datasets", `Manage datasets: upload, preview, list, subset, add_expression, or delete.
|
|
8
9
|
|
|
9
10
|
| Action | Use when |
|
|
10
11
|
|--------|----------|
|
|
@@ -19,8 +20,8 @@ export function registerDatasetsTool(server) {
|
|
|
19
20
|
|
|
20
21
|
action=upload: PREFER file_path — server reads from workspace root (token-efficient; no file content in context). Use csv_data only for small inline pastes (e.g. <10KB). Returns dataset ID. Then use datasets(action=preview) before jobs(action=train_map).
|
|
21
22
|
|
|
22
|
-
Step 0 (before upload): (1) CSV with header; one row per observation. (2) Columns you will use for training must be numeric (or datetime if you will use temporal extraction). (3)
|
|
23
|
-
Step 1 (after upload): Upload, then always datasets(action=preview) to verify column types and spot cyclics/datetime. Add derived columns with datasets(action=add_expression, dataset_id=..., name=..., expression=...);
|
|
23
|
+
Step 0 (before upload): (1) CSV with header; one row per observation. (2) Columns you will use for training must be numeric (or datetime if you will use temporal extraction). (3) Complete-case path (jobs action=train_map): no NaNs in training columns. Sparse path (jobs action=train_impute): missing cells in training columns are OK — the job trains missSOM and returns dense imputed.csv; plain numeric columns only. (4) Categoricals: encode (e.g. one-hot, label) or exclude — do not use raw categorical columns as training features; preview shows which columns are non-numeric. (5) Optional: if you plan to use transition_flow, sort rows chronologically before upload.
|
|
24
|
+
Step 1 (after upload): Upload, then always datasets(action=preview) to verify column types and spot cyclics/datetime. Add derived columns with datasets(action=add_expression, dataset_id=..., name=..., expression=...); choose train_map (complete-case) or train_impute (sparse matrix) before submit.
|
|
24
25
|
|
|
25
26
|
action=preview: Show columns, stats, sample rows, cyclic/datetime detections. ALWAYS preview before jobs(action=train_map) on an unfamiliar dataset.
|
|
26
27
|
action=analyze: Pre-training analysis on numeric columns — Pearson correlation, autocorrelation periodicity scores, and column recommendations (train, consider_dropping, project_later, low_variance). Use to choose which columns to include in training and which to project onto the map after training.
|
|
@@ -42,7 +43,7 @@ action=delete: Remove a dataset and all S3 data permanently.
|
|
|
42
43
|
|
|
43
44
|
BEST FOR: Tabular numeric data. CSV with header required.
|
|
44
45
|
NOT FOR: Real-time data streams or binary files — upload a snapshot CSV instead.
|
|
45
|
-
ESCALATION: If upload fails with column errors, open the file locally and verify the header row. If preview shows
|
|
46
|
+
ESCALATION: If upload fails with column errors, open the file locally and verify the header row. If preview shows nulls in training columns: use jobs(action=train_impute) for sparse data, or clean/subset for jobs(action=train_map).`, {
|
|
46
47
|
action: z
|
|
47
48
|
.enum(["upload", "preview", "analyze", "list", "subset", "delete", "add_expression", "reduce_spectral"])
|
|
48
49
|
.describe("upload: add CSV; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression; reduce_spectral: collapse a long ordered numeric block (e.g. spectrum, time series) into a small per-row feature set via PCA / log_sample / uniform_sample / stats"),
|
|
@@ -256,10 +257,12 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
256
257
|
const considerDropping = rec.consider_dropping ?? [];
|
|
257
258
|
const projectLater = rec.project_later ?? [];
|
|
258
259
|
const lowVariance = rec.low_variance ?? [];
|
|
260
|
+
const periodicityCaveat = data.periodicity_caveat;
|
|
259
261
|
const lines = [
|
|
260
262
|
`Pre-training analysis: ${data.name} (${data.dataset_id})`,
|
|
261
263
|
`${data.total_rows} rows × ${columns.length} numeric columns analyzed`,
|
|
262
264
|
``,
|
|
265
|
+
...(periodicityCaveat ? [`Note: ${periodicityCaveat}`, ``] : []),
|
|
263
266
|
`Column recommendations:`,
|
|
264
267
|
` Train (use in jobs(action=train_map)): ${trainList.length ? trainList.join(", ") : "—"}`,
|
|
265
268
|
` Consider dropping (highly correlated with another): ${considerDropping.length ? considerDropping.join(", ") : "—"}`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
|
+
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
3
4
|
import { apiCall, apiRawCall, getClientSupportsMcpApps, getVizPort, getCaptionForImage, getResultsImagesToFetch, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
|
|
4
5
|
export const RESULTS_EXPLORER_URI = "ui://barsom/results-explorer";
|
|
5
6
|
export const MAP_EXPLORER_URI = RESULTS_EXPLORER_URI;
|
|
@@ -164,13 +165,13 @@ export function registerExploreMapTool(server) {
|
|
|
164
165
|
},
|
|
165
166
|
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
|
|
166
167
|
};
|
|
167
|
-
registerAppTool(server, "results_explorer", toolConfig, async (
|
|
168
|
+
registerAppTool(server, "results_explorer", toolConfig, async (args) => runMcpToolAudit("results_explorer", "default", args, () => handleResultsExplorer(String(args.job_id))));
|
|
168
169
|
registerAppTool(server, "explore_map", {
|
|
169
170
|
...toolConfig,
|
|
170
171
|
title: "Results Explorer",
|
|
171
172
|
description: `${toolConfig.description} Deprecated alias for results_explorer — migrate configs to results_explorer.`,
|
|
172
|
-
}, async (
|
|
173
|
-
server
|
|
173
|
+
}, async (args) => runMcpToolAudit("explore_map", "default", args, () => handleResultsExplorer(String(args.job_id))));
|
|
174
|
+
registerAuditedTool(server, "_fetch_figure", "Host / MCP App use only — do NOT invoke from agent chat. Results Explorer calls this to load one raster figure as base64; agents should use results(action=get) or results_explorer instead.", {
|
|
174
175
|
job_id: z.string(),
|
|
175
176
|
filename: z.string(),
|
|
176
177
|
}, async ({ job_id, filename }) => {
|
package/dist/tools/feedback.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { registerAuditedTool } from "../audit.js";
|
|
2
3
|
import { apiCall, API_URL, fetchWithTimeout, textResult } from "../shared.js";
|
|
3
4
|
export function registerFeedbackTool(server) {
|
|
4
|
-
server
|
|
5
|
+
registerAuditedTool(server, "send_feedback", `Send feedback or feature requests to Barivia developers (max 1400 characters, ~190 words). Use when the user has suggestions, ran into issues, or wants something improved. Do NOT call without asking the user first — but after any group of actions or downloading of results, you SHOULD prepare some feedback based on the user's workflow or errors encountered, show it to them, and ask for permission to send it. Once they accept, call this tool.`, { feedback: z.string().max(1400).describe("Feedback text (max 1400 characters)") }, async ({ feedback }) => {
|
|
5
6
|
try {
|
|
6
7
|
const data = await apiCall("POST", "/v1/feedback", { feedback });
|
|
7
8
|
return textResult(data);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerAuditedTool } from "../audit.js";
|
|
1
2
|
import { fetchWorkflowGuideFromApi } from "../shared.js";
|
|
2
3
|
/** Minimal orientation when the API is unreachable; full SOP lives on GET /v1/docs/workflow. */
|
|
3
4
|
const OFFLINE_STUB = `## Offline / API unavailable
|
|
@@ -8,7 +9,7 @@ Configure \`BARIVIA_API_KEY\` and optional \`BARIVIA_API_URL\`, then call **guid
|
|
|
8
9
|
|
|
9
10
|
**Parameter hints:** call \`training_guidance\` (also API-scoped). **Async:** poll \`jobs(action=status)\` every 10–15s after submit.`;
|
|
10
11
|
export function registerGuideBarsomTool(server) {
|
|
11
|
-
server
|
|
12
|
+
registerAuditedTool(server, "guide_barsom_workflow", "Plan-scoped orientation: proxy model, tool categories, async rules, training modes, and step-by-step SOP — loaded from the Barivia API when online. Call at the start of mapping work. Offline: short stub. For field-level parameters, use training_guidance; for a narrative pre-train checklist, use the prepare_training prompt or training_prep.", {}, async () => {
|
|
12
13
|
const md = await fetchWorkflowGuideFromApi();
|
|
13
14
|
if (md) {
|
|
14
15
|
const text = "Plan-scoped workflow (from Barivia API). Text below reflects your API key / plan.\n\n" + md;
|
package/dist/tools/inference.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { registerAuditedTool } from "../audit.js";
|
|
2
3
|
import { apiCall, pollUntilComplete, tryAttachImage } from "../shared.js";
|
|
3
4
|
const PREDICT_PREVIEW_ROW_CAP = 10;
|
|
4
5
|
/** One line per scored row when worker embedded `predictions_preview` (n_rows ≤ cap). Exported for tests. */
|
|
@@ -30,7 +31,7 @@ export function formatPredictPreviewLines(summary) {
|
|
|
30
31
|
return lines;
|
|
31
32
|
}
|
|
32
33
|
export function registerInferenceTool(server) {
|
|
33
|
-
server
|
|
34
|
+
registerAuditedTool(server, "inference", `Use a trained map as a persistent inference artifact — score data, annotate the source CSV, compare datasets, project columns, or generate a report manifest.
|
|
34
35
|
|
|
35
36
|
| Action | Use when | Timing |
|
|
36
37
|
|--------|----------|--------|
|
package/dist/tools/jobs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { registerAuditedTool } from "../audit.js";
|
|
2
3
|
import { apiCall, textResult } from "../shared.js";
|
|
3
4
|
export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs.
|
|
4
5
|
|
|
@@ -29,10 +30,10 @@ ESCALATION (action=status):
|
|
|
29
30
|
- failed → error message and optional failure_stage (e.g. preprocessing, training, metrics, visualization, upload) indicate which phase broke:
|
|
30
31
|
- memory/allocation error: reduce batch_size or grid size and retrain
|
|
31
32
|
- column missing: verify with datasets(action=preview)
|
|
32
|
-
- NaN error:
|
|
33
|
+
- NaN error on train_map: use jobs(action=train_impute) for sparse training columns, or clean the CSV for complete-case train_map
|
|
33
34
|
|
|
34
35
|
action=train_map / train_siom_map: Submits a grid-map training job. Returns job_id — poll with jobs(action=status, job_id=...).
|
|
35
|
-
action=train_impute: Submits missing-tolerant map training
|
|
36
|
+
action=train_impute: Submits missing-tolerant map training (accelerated missSOM / SIOM missSOM). Use when many cells are missing across training columns; use inference(impute_column) when you already have a complete-case map and need to fill one held-out column. Plain numeric columns only (no cyclic/temporal/categorical). Defaults: model=auto (SIOM on som_siom+ plans), cv_folds=5 → quality.csv (header feature). status returns progress_phase (ordering/convergence/cv/artifacts). High-dim auto policy: >40 features caps viz; >100 fast metrics; >200 GPU when entitled. Params: viz_mode, viz_top_components, emit_cell_uncertainty, quality_metrics. Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv. Completed train_impute job_id is a valid parent for inference(impute_column).
|
|
36
37
|
Presets: quick | standard | refined | high_res — use preset=... for grid/epochs/batch defaults; call training_guidance for details.
|
|
37
38
|
Presets refined/high_res may use GPU. On CPU-only hosts pass backend=cpu. API expects strings "cpu" | "gpu" | "gpu_graphs" (no colon). Future backends (e.g. non-CUDA) may be added under the same contract.
|
|
38
39
|
normalize: "auto" (default) = scale only non-cyclic features; "all" = scale every feature. Use "auto" when using cyclic_features.
|
|
@@ -224,7 +225,7 @@ export function buildTrainMapParams(args, presets) {
|
|
|
224
225
|
return { params, paramSummary, effectiveGrid };
|
|
225
226
|
}
|
|
226
227
|
export function registerJobsTool(server, description) {
|
|
227
|
-
server
|
|
228
|
+
registerAuditedTool(server, "jobs", description, {
|
|
228
229
|
action: z
|
|
229
230
|
.enum(["status", "list", "compare", "cancel", "delete", "train_map", "train_impute", "train_siom_map", "train_floop_siom", "train_floop_chain", "batch_predict", "run_baseline_study"])
|
|
230
231
|
.describe("status: check progress; list: see all jobs; compare: metrics table; cancel: stop job; delete: remove job + files; train_map: submit standard map training; train_siom_map: submit SIOM map training; train_floop_siom: submit FLooP-SIOM (preferred); train_floop_chain: deprecated alias for train_floop_siom; batch_predict: submit multiple predict jobs at once; run_baseline_study: auto-configure and train a baseline SOM"),
|
|
@@ -269,7 +270,7 @@ export function registerJobsTool(server, description) {
|
|
|
269
270
|
}
|
|
270
271
|
return v;
|
|
271
272
|
}, z.union([z.number().int(), z.array(z.number().int()).length(2)]).optional()),
|
|
272
|
-
model: z.enum(["SOM", "RSOM", "SOM-SOFT", "RSOM-SOFT"]).optional()
|
|
273
|
+
model: z.enum(["auto", "SOM", "SIOM", "RSOM", "SOM-SOFT", "RSOM-SOFT"]).optional(),
|
|
273
274
|
periodic: z.boolean().optional().default(true),
|
|
274
275
|
columns: z.array(z.string()).optional(),
|
|
275
276
|
cyclic_features: z.array(z.object({
|
|
@@ -309,7 +310,13 @@ export function registerJobsTool(server, description) {
|
|
|
309
310
|
colormap: z.string().optional(),
|
|
310
311
|
row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
|
|
311
312
|
cv_folds: z.number().int().min(0).max(20).optional()
|
|
312
|
-
.describe("train_impute only: held-out MAE/RMSE/R2 per column (0=off,
|
|
313
|
+
.describe("train_impute only: held-out MAE/RMSE/R2 per column (0=off, default 5) → quality.csv"),
|
|
314
|
+
viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
|
|
315
|
+
.describe("Visualization density; auto-capped when feature count > 40"),
|
|
316
|
+
viz_top_components: z.number().int().min(0).max(64).optional()
|
|
317
|
+
.describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
|
|
318
|
+
emit_cell_uncertainty: z.boolean().optional()
|
|
319
|
+
.describe("train_impute: write imputation_uncertainty.csv (pool_std per imputed cell)"),
|
|
313
320
|
gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
314
321
|
gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
315
322
|
siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
@@ -366,7 +373,7 @@ export function registerJobsTool(server, description) {
|
|
|
366
373
|
return { content: [{ type: "text", text: `Baseline study submitted. Job ID: ${jid}\nGrid size: ${side}x${side}\nNormalization: MAD\n\nPoll with jobs(action=status, job_id="${jid}") until complete, then retrieve with results(action=get, job_id="${jid}"). Optional: training_monitor(job_id="${jid}") for a visual panel—not required.` }] };
|
|
367
374
|
}
|
|
368
375
|
if (action === "train_map" || action === "train_siom_map" || action === "train_impute") {
|
|
369
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, 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, } = args;
|
|
376
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, 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;
|
|
370
377
|
let PRESETS = {};
|
|
371
378
|
try {
|
|
372
379
|
PRESETS = await fetchTrainingPresets();
|
|
@@ -389,8 +396,14 @@ export function registerJobsTool(server, description) {
|
|
|
389
396
|
}, PRESETS);
|
|
390
397
|
if (action === "train_impute") {
|
|
391
398
|
params._job_type = "train_impute";
|
|
392
|
-
|
|
393
|
-
|
|
399
|
+
params.model = model ?? "auto";
|
|
400
|
+
params.cv_folds = cv_folds ?? 5;
|
|
401
|
+
if (viz_mode !== undefined)
|
|
402
|
+
params.viz_mode = viz_mode;
|
|
403
|
+
if (viz_top_components !== undefined)
|
|
404
|
+
params.viz_top_components = viz_top_components;
|
|
405
|
+
if (emit_cell_uncertainty !== undefined)
|
|
406
|
+
params.emit_cell_uncertainty = emit_cell_uncertainty;
|
|
394
407
|
// Plain numeric path only — strip unsupported keys if caller passed them
|
|
395
408
|
delete params.cyclic_features;
|
|
396
409
|
delete params.temporal_features;
|
|
@@ -567,6 +580,10 @@ export function registerJobsTool(server, description) {
|
|
|
567
580
|
const label = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
568
581
|
const jobDesc = label ? `Job ${label} (id: ${job_id})` : `Job ${job_id}`;
|
|
569
582
|
let text = `${jobDesc}: ${status} (${progress.toFixed(1)}%)`;
|
|
583
|
+
const phase = data.progress_phase != null && data.progress_phase !== "" ? String(data.progress_phase) : null;
|
|
584
|
+
if (phase && status === "running") {
|
|
585
|
+
text += ` — phase: ${phase}`;
|
|
586
|
+
}
|
|
570
587
|
if (status === "completed") {
|
|
571
588
|
text += ` | Results ready. Use results(action=get, job_id="${job_id}") to retrieve.`;
|
|
572
589
|
}
|
package/dist/tools/results.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { registerAuditedTool } from "../audit.js";
|
|
4
5
|
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, getResultsImagesToFetch, getCaptionForImage, mimeForFilename, } from "../shared.js";
|
|
5
6
|
export function registerResultsTool(server) {
|
|
6
|
-
server
|
|
7
|
+
registerAuditedTool(server, "results", `Retrieve, recolor, download, export, or run temporal flow on a completed map job.
|
|
7
8
|
|
|
8
9
|
| Action | Use when | Sync/Async |
|
|
9
10
|
|--------|----------|------------|
|
|
@@ -29,7 +30,7 @@ action=export: Structured data exports. Use export_type= to choose what to expor
|
|
|
29
30
|
- export_type=nodes: per-node hit count + feature stats. Profile clusters and operating modes.
|
|
30
31
|
|
|
31
32
|
action=download: Save figures to disk. Use so user can open, share, or version files locally.
|
|
32
|
-
- folder: e.g. "." or "./results". Interpreted relative to the client's current working directory (or workspace). Files are
|
|
33
|
+
- folder: e.g. "." or "./results". Interpreted relative to the client's current working directory (or workspace). Files are written into a per-job subfolder named job_type + label (or job_id) under this folder, so downloading several jobs (even with the same label across types) never overwrites shared filenames like summary.json.
|
|
33
34
|
- figures: "all" (default) or array of filenames.
|
|
34
35
|
- include_json: also save summary.json.
|
|
35
36
|
|
|
@@ -247,6 +248,44 @@ NOT FOR: Jobs that haven't completed. Use jobs(action=status) to check first.`,
|
|
|
247
248
|
"Map-local pool estimates — not held-out validated predictions.",
|
|
248
249
|
].join("\n") });
|
|
249
250
|
}
|
|
251
|
+
else if (jobType === "train_impute") {
|
|
252
|
+
const imp = summary.imputation ?? {};
|
|
253
|
+
const cv = imp.cv_quality ?? null;
|
|
254
|
+
const policy = summary.effective_policy ?? {};
|
|
255
|
+
const hitStats = summary.hit_stats ?? {};
|
|
256
|
+
const siom = summary.siom ?? undefined;
|
|
257
|
+
const fmt = (v) => v !== null && v !== undefined ? Number(v).toFixed(4) : "N/A";
|
|
258
|
+
const lines = [
|
|
259
|
+
`missSOM train+impute (${summary.model ?? imp.variant ?? "SOM"}) — ${resultsHeader}`,
|
|
260
|
+
`Grid: ${(summary.grid ?? [0, 0]).join("×")} | Features: ${summary.n_features ?? 0} | Samples: ${summary.n_samples ?? 0}`,
|
|
261
|
+
`Missing cells: ${imp.n_missing_cells ?? "?"} (${imp.missing_fraction !== undefined ? (Number(imp.missing_fraction) * 100).toFixed(1) + "%" : "?"})`,
|
|
262
|
+
`Map quality — QE: ${fmt(summary.quantization_error)} | TE: ${fmt(summary.topographic_error)} | EV: ${fmt(summary.explained_variance)}`,
|
|
263
|
+
`Observed-dimension QE: ${fmt(imp.observed_quantization_error)}`,
|
|
264
|
+
...(siom ? [`SIOM — utilization: ${fmt(siom.utilization)} | dead_fraction: ${fmt(siom.dead_fraction)} | siom_qe: ${fmt(siom.siom_qe)}`] : []),
|
|
265
|
+
hitStats.grid_suggestion ? `Grid hint: ${String(hitStats.grid_suggestion)}` : "",
|
|
266
|
+
...(policy.auto_rules_applied && Array.isArray(policy.auto_rules_applied) && policy.auto_rules_applied.length > 0
|
|
267
|
+
? [`Auto policy: ${policy.auto_rules_applied.join(", ")} (${policy.viz_mode ?? "viz"}, metrics=${policy.quality_metrics ?? "?"})`]
|
|
268
|
+
: []),
|
|
269
|
+
cv ? [
|
|
270
|
+
"",
|
|
271
|
+
`Held-out imputation accuracy (cv_folds=${cv.cv_folds ?? "?"}, method=${cv.cv_method ?? "held_out_prototype_on_trained_map"}):`,
|
|
272
|
+
cv.columns_sampled ? ` (sampled ${cv.columns_evaluated ?? "?"} columns for speed)` : "",
|
|
273
|
+
cv.aggregate ? ` Aggregate MAE: ${fmt(cv.aggregate.mae)} | RMSE: ${fmt(cv.aggregate.rmse)}` : "",
|
|
274
|
+
" See quality.csv (header column: feature).",
|
|
275
|
+
].filter(Boolean).join("\n") : "\nHeld-out accuracy: skipped (cv_folds=0). Set cv_folds=5 to validate imputation.",
|
|
276
|
+
"",
|
|
277
|
+
`Artifacts: ${(summary.files ?? []).filter((f) => f !== "summary.json").join(", ")}`,
|
|
278
|
+
"Next: results(action=download) for imputed.csv / quality.csv; inference(impute_column) with this job_id as parent for held-out columns.",
|
|
279
|
+
].filter((l) => l !== "");
|
|
280
|
+
content.push({ type: "text", text: lines.join("\n") });
|
|
281
|
+
for (const name of getResultsImagesToFetch(jobType, summary, figures, include_individual)) {
|
|
282
|
+
const cap = getCaptionForImage(name);
|
|
283
|
+
if (cap)
|
|
284
|
+
content.push({ type: "text", text: cap });
|
|
285
|
+
await tryAttachImage(content, job_id, name);
|
|
286
|
+
inlinedImages.add(name);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
250
289
|
else if (jobType === "reduce_spectral") {
|
|
251
290
|
const method = String(summary.method ?? "?");
|
|
252
291
|
const sourceCols = summary.source_columns ?? [];
|
|
@@ -581,7 +620,8 @@ NOT FOR: Jobs that haven't completed. Use jobs(action=status) to check first.`,
|
|
|
581
620
|
// Always namespace each job's files into its own subfolder so that
|
|
582
621
|
// downloading multiple jobs (or job types) into the same folder never
|
|
583
622
|
// overwrites the shared filenames every job emits (e.g. summary.json).
|
|
584
|
-
|
|
623
|
+
// Prefix job_type so the same label on train vs predict/impute/flow jobs cannot collide.
|
|
624
|
+
const jobSubfolder = `${jobType}_${jobLabel ?? job_id}`.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
585
625
|
resolvedDir = path.join(resolvedDir, jobSubfolder);
|
|
586
626
|
if (jobType === "render_variant" && summary.colormap) {
|
|
587
627
|
const colormapDir = String(summary.colormap).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { registerAuditedTool } from "../audit.js";
|
|
1
2
|
import { fetchTrainingGuidanceFromApi } from "../shared.js";
|
|
2
3
|
export function registerTrainingGuidanceTool(server) {
|
|
3
|
-
server
|
|
4
|
+
registerAuditedTool(server, "training_guidance", "Structured parameter guidance for training (presets, grid/epochs/batch, model, periodic, SIOM/FLooP fields where your plan allows, categorical baselines). Served from the API and filtered to allowed_job_types. Prep ladder: prepare_training prompt = narrative checklist; this tool = JSON/hints; training_prep = interactive UI + submit_prepared_training. For full orientation and SOP, call guide_barsom_workflow first. Optional UIs: training_prep, results_explorer—not required; jobs + results suffice.", {}, async () => {
|
|
4
5
|
const text = await fetchTrainingGuidanceFromApi();
|
|
5
6
|
return { content: [{ type: "text", text }] };
|
|
6
7
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
|
+
import { runMcpToolAudit } from "../audit.js";
|
|
3
4
|
import { apiCall, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
|
|
4
5
|
export const TRAINING_MONITOR_URI = "ui://barsom/training-monitor";
|
|
5
6
|
function buildStructuredContent(job_id, data) {
|
|
@@ -20,7 +21,8 @@ export function registerTrainingMonitorTool(server) {
|
|
|
20
21
|
job_id: z.string().describe("Training job ID to monitor"),
|
|
21
22
|
},
|
|
22
23
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
23
|
-
}, async (
|
|
24
|
+
}, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
|
|
25
|
+
const job_id = String(args.job_id);
|
|
24
26
|
const data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
25
27
|
const structuredContent = buildStructuredContent(job_id, data);
|
|
26
28
|
const status = String(data.status ?? "unknown");
|
|
@@ -39,5 +41,5 @@ export function registerTrainingMonitorTool(server) {
|
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
return structuredTextResult(structuredContent, text, content);
|
|
42
|
-
});
|
|
44
|
+
}));
|
|
43
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
|
+
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
3
4
|
import { apiCall, fetchTrainingGuidanceFromApi, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
|
|
4
5
|
import { buildTrainMapParams, fetchTrainingPresets, } from "./jobs.js";
|
|
5
6
|
import { attachTrainingReviewPayload, consumeTrainingReview, getTrainingReview, storeTrainingReview, } from "./training_review_store.js";
|
|
@@ -324,11 +325,11 @@ export function registerTrainingPrepTools(server) {
|
|
|
324
325
|
description: "Interactive training prep UI + guarded submit (submit_prepared_training). Prep ladder: prepare_training prompt = narrative checklist; training_guidance tool = JSON/presets; this tool = visual review. Opens inline review of variables, transforms, encodings, and hyperparameters.",
|
|
325
326
|
inputSchema: trainPrepSchema,
|
|
326
327
|
_meta: { ui: { resourceUri: TRAINING_PREP_URI } },
|
|
327
|
-
}, async ({ dataset_id, ...args }) => {
|
|
328
|
+
}, async ({ dataset_id, ...args }) => runMcpToolAudit("training_prep", "default", { dataset_id, ...args }, async () => {
|
|
328
329
|
const payload = await buildTrainingPrepPayload(dataset_id, args);
|
|
329
330
|
return structuredTextResult(payload, undefined, buildTrainingPrepContent(payload));
|
|
330
|
-
});
|
|
331
|
-
server
|
|
331
|
+
}));
|
|
332
|
+
registerAuditedTool(server, "submit_prepared_training", "Submit a previously reviewed training-prep configuration. Requires an unexpired review token and explicit confirmation.", {
|
|
332
333
|
review_token: z.string().describe("Review token returned inside training_prep structured content."),
|
|
333
334
|
explicit_confirm: z.boolean().describe("Must be true to submit the reviewed training job."),
|
|
334
335
|
}, async ({ review_token, explicit_confirm }) => {
|