@barivia/barsom-mcp 0.10.2 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,8 @@ MCP clients typically run it with **`npx`** (downloads on first use):
27
27
 
28
28
  **Verify:** `npm view @barivia/barsom-mcp version`
29
29
 
30
+ **Audit logging (0.10.3+):** Each tool invocation emits a structured **`mcp_tool_call`** line on stderr for support correlation — not sent to the Barivia API. Fields: `tool`, `action`, `duration_ms`, `outcome`, **`rid`** (matches API `X-Request-ID`), **`job_id`** / **`dataset_id`** when the action returns or accepts them, scale hints **`grid_x`** / **`grid_y`** on training submits, and **`timeout_hit`** / **`output_truncated`** when proxy fetch/output limits apply. No API keys or file paths. Grep client stderr alongside API/worker logs by `rid`; see `docs/OPERATIONS_MANUAL.md` §5b.
31
+
30
32
  **Also available:** hosted HTTP MCP at **`https://mcp.barivia.se/mcp`** (same API key; no npm) for clients that support remote MCP.
31
33
 
32
34
  **Cursor / multi-account note:** Cursor's tool dispatcher resolves tools under a `serverIdentifier` (e.g. `user-barivia-bbb-maps`), which can differ from the key you wrote in `mcp.json` (e.g. `barivia-bbb-maps`). If a tool call fails with **"server does not exist"** or similar, list the resolved identifier with your client's tool inspector (`mcp.list-tools` or equivalent) and use that identifier — not the `mcp.json` key.
@@ -98,7 +100,7 @@ Call at the **start of mapping work** (or when the user asks what the MCP can do
98
100
  | Action | Use when |
99
101
  |--------|----------|
100
102
  | `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)`. |
103
+ | `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`. `quality.csv` reports two labeled numbers per column: `kfold_holdout_pool` (honest held-out) and `resubstitution_optimistic` (in-sample, optimistic — not validation). Plain numeric columns only. Valid parent for `inference(impute_column)`. |
102
104
  | `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. |
103
105
  | `train_floop_siom` | Submitting a FLooP-SIOM job — growing node-budget manifold; default `topology=free` (CHL); optional `topology=chain` for strict 1D linked list. |
104
106
  | `train_floop_chain` | Deprecated alias for `train_floop_siom` — same behavior. |
@@ -200,7 +202,7 @@ MCP Client (Cursor/Claude) ←stdio→ @barivia/barsom-mcp ←HTTPS→ api.bariv
200
202
 
201
203
  | Symptom | What to check |
202
204
  |--------|----------------|
203
- | `401` / invalid key | `BARIVIA_API_KEY` in MCP config; regenerate or verify at [barivia.se](https://barivia.se). Error text includes a **request id** for support. |
205
+ | `401` / invalid key | `BARIVIA_API_KEY` in MCP config; check your email or manage your key in the [account dashboard](https://barivia.se/dashboard). Error text includes a **request id** for support. |
204
206
  | Request timed out | Raise `BARIVIA_FETCH_TIMEOUT_MS` (e.g. `120000`). Large uploads already use an extended timeout. |
205
207
  | `Path must be within the workspace` / upload can’t find file | Set `BARIVIA_WORKSPACE_ROOT` to your project directory, or use an absolute path / `file:///...` URI. |
206
208
  | Job stuck “running” | Poll `jobs(action=status)` every 10–15s; large grids or FLooP-SIOM can take several minutes—not an MCP error. |
package/dist/audit.js ADDED
@@ -0,0 +1,169 @@
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
+ "grid_x",
20
+ "grid_y",
21
+ ]);
22
+ function scrubParams(args) {
23
+ const out = {};
24
+ for (const [k, v] of Object.entries(args)) {
25
+ if (!AUDIT_PARAM_KEYS.has(k))
26
+ continue;
27
+ if (v === undefined || v === null)
28
+ continue;
29
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
30
+ out[k] = v;
31
+ }
32
+ }
33
+ const grid = args.grid;
34
+ if (Array.isArray(grid) && grid.length >= 2) {
35
+ if (out.grid_x === undefined && typeof grid[0] === "number")
36
+ out.grid_x = grid[0];
37
+ if (out.grid_y === undefined && typeof grid[1] === "number")
38
+ out.grid_y = grid[1];
39
+ }
40
+ return out;
41
+ }
42
+ function extractIds(result) {
43
+ const ids = {};
44
+ if (result === null || typeof result !== "object")
45
+ return ids;
46
+ const r = result;
47
+ if (typeof r.job_id === "string")
48
+ ids.job_id = r.job_id;
49
+ if (typeof r.id === "string" && !ids.job_id)
50
+ ids.job_id = r.id;
51
+ if (typeof r.dataset_id === "string")
52
+ ids.dataset_id = r.dataset_id;
53
+ const sc = r.structuredContent;
54
+ if (sc && typeof sc === "object") {
55
+ const s = sc;
56
+ if (typeof s.jobId === "string")
57
+ ids.job_id = s.jobId;
58
+ if (typeof s.datasetId === "string")
59
+ ids.dataset_id = s.datasetId;
60
+ }
61
+ try {
62
+ const text = JSON.stringify(result);
63
+ const jobMatch = text.match(/"job_id"\s*:\s*"([a-f0-9-]{36})"/i);
64
+ if (jobMatch && !ids.job_id)
65
+ ids.job_id = jobMatch[1];
66
+ const dsMatch = text.match(/"dataset_id"\s*:\s*"([a-f0-9-]{36})"/i);
67
+ if (dsMatch && !ids.dataset_id)
68
+ ids.dataset_id = dsMatch[1];
69
+ }
70
+ catch {
71
+ /* ignore */
72
+ }
73
+ return ids;
74
+ }
75
+ function extractAuditExtras(result) {
76
+ const extras = {};
77
+ if (result === null || typeof result !== "object")
78
+ return extras;
79
+ const r = result;
80
+ const audit = r._audit;
81
+ if (audit && typeof audit === "object") {
82
+ const a = audit;
83
+ if (a.timeout_hit === true)
84
+ extras.timeout_hit = true;
85
+ if (a.output_truncated === true)
86
+ extras.output_truncated = true;
87
+ }
88
+ try {
89
+ const text = JSON.stringify(result);
90
+ if (/Poll with jobs\(action=status/i.test(text))
91
+ extras.timeout_hit = true;
92
+ if (/_truncated\s*:\s*true/i.test(text))
93
+ extras.output_truncated = true;
94
+ }
95
+ catch {
96
+ /* ignore */
97
+ }
98
+ return extras;
99
+ }
100
+ function stripAuditMeta(result) {
101
+ if (result === null || typeof result !== "object" || Array.isArray(result))
102
+ return result;
103
+ if (!("_audit" in result))
104
+ return result;
105
+ const copy = { ...result };
106
+ delete copy._audit;
107
+ return copy;
108
+ }
109
+ function errorCodeFrom(err) {
110
+ if (err && typeof err === "object") {
111
+ const e = err;
112
+ if (e.httpStatus !== undefined)
113
+ return `http_${e.httpStatus}`;
114
+ const m = e.message ?? "";
115
+ const codeMatch = m.match(/error_code:\s*(\w+)/);
116
+ if (codeMatch)
117
+ return codeMatch[1];
118
+ }
119
+ return undefined;
120
+ }
121
+ /**
122
+ * Run a tool handler with structured audit logging.
123
+ */
124
+ export async function runMcpToolAudit(tool, action, args, handler) {
125
+ const t0 = Date.now();
126
+ const params = scrubParams(args);
127
+ try {
128
+ const raw = await handler();
129
+ const result = stripAuditMeta(raw);
130
+ const ids = extractIds(result);
131
+ const extras = extractAuditExtras(raw);
132
+ logAudit({
133
+ tool,
134
+ action,
135
+ duration_ms: Date.now() - t0,
136
+ outcome: "ok",
137
+ ...ids,
138
+ ...(extras.timeout_hit ? { timeout_hit: true } : {}),
139
+ ...(extras.output_truncated ? { output_truncated: true } : {}),
140
+ ...(Object.keys(params).length > 0 ? { params } : {}),
141
+ });
142
+ return result;
143
+ }
144
+ catch (err) {
145
+ logAudit({
146
+ tool,
147
+ action,
148
+ duration_ms: Date.now() - t0,
149
+ outcome: "error",
150
+ error_code: errorCodeFrom(err),
151
+ ...(Object.keys(params).length > 0 ? { params } : {}),
152
+ });
153
+ throw err;
154
+ }
155
+ }
156
+ /**
157
+ * Register an MCP tool with structured audit logging on each invocation.
158
+ */
159
+ export function registerAuditedTool(server, name, description, schema, handler) {
160
+ server.tool(name, description, schema, (async (args) => {
161
+ const rec = args;
162
+ const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
163
+ return runMcpToolAudit(name, action, rec, () => handler(args));
164
+ }));
165
+ }
166
+ /** Attach audit flags to a tool result (stripped before returning to MCP client). */
167
+ export function withAuditFlags(result, flags) {
168
+ return { ...result, _audit: flags };
169
+ }
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 n,RESOURCE_MIME_TYPE as s}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as m,setClientSupportsMcpApps as d,CLIENT_VERSION as u}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as h,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as w}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as v}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as P}from"./tools/training_guidance.js";import{registerFeedbackTool as x}from"./tools/feedback.js";import{registerTrainingPrepTools as I,TRAINING_PREP_URI as k}from"./tools/training_prep.js";import{registerTrainingMonitorTool as M,TRAINING_MONITOR_URI as O}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText 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 A=new e({name:"analytics-engine",version:u,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `jobs(train_map)`, `jobs(train_siom_map)`, or `jobs(train_floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool). Then `jobs(compare)`, `results(download/recolor/transition_flow)`, or `inference` as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool map (compact)\n\n| Area | Tool | Notes |\n|------|------|--------|\n| Data | `datasets` | upload, preview, analyze, list, subset, add_expression, reduce_spectral (pca/log_sample/uniform_sample/stats for long ordered numeric blocks), delete |\n| Jobs | `jobs` | train_map, train_siom_map, train_floop_siom (entitled), status, list, compare, cancel, delete, batch_predict, run_baseline_study; `train_floop_chain` = deprecated alias for train_floop_siom |\n| Results | `results` | get (figures="none" for metrics-only), export, download, recolor (async), transition_flow (async; time-ordered rows only) |\n| Inference | `inference` | predict (regime-aware; output="compact"|"annotated"), impute_column (neighbor-pool fill for a non-training column), compare, project_columns, report |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow` | orientation + SOP |\n| Parameters | `training_guidance` | presets and field hints (API-scoped) |\n| Prep | `prepare_training` prompt, `training_prep` + `submit_prepared_training` | checklist / interactive UI |\n| Explore | `results_explorer`, `training_monitor` | optional MCP Apps; `explore_map` = deprecated alias of `results_explorer`; `jobs(status)` and `results(get)` suffice without them |\n| Other | `send_feedback` | only after user agrees |\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions, `results(recolor)`, `results(transition_flow)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Slow networks: users can raise `BARIVIA_FETCH_TIMEOUT_MS`.\n\n## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints; `training_prep` = UI + guarded submit. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `recolor`, `transition_flow`, or `project_columns`, use the **new** `job_id` returned for follow-up `results` if applicable.'});n(A,y,y,{mimeType:s},async()=>{const e=await c("results-explorer");return{contents:[{uri:y,mimeType:s,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),n(A,k,k,{mimeType:s},async()=>{const e=await c("training-prep");return{contents:[{uri:k,mimeType:s,text:e??"<html><body>Training Preparation view not built yet.</body></html>"}]}}),n(A,O,O,{mimeType:s},async()=>{const e=await c("training-monitor");return{contents:[{uri:O,mimeType:s,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),v(A),h(A),I(A),M(A),f(A),g(A,_),b(A),w(A),j(A),P(A),x(A),A.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).","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `jobs` (train/poll/compare/…; train_map accepts an optional `label` for readable compare rows), `results` (get/download/export/recolor/transition_flow; figures="none" for metrics-only), `inference` (predict; impute_column for topology-neighbor pool fill; compare; project_columns; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints) · `training_prep` + `submit_prepared_training` (interactive UI).","","**Optional UI:** `results_explorer`, `training_monitor` — nice for browsing; not required if you use `results` + `jobs(status)`.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `transition_flow` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. 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")}}]})),A.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; training_prep tool = interactive UI + submit_prepared_training.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await S(e)}}]}));const T=new t;(async function(){try{const e=await a(l,p,c);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=A.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(s))},await A.connect(T)})().catch(console.error);
2
+ import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as n,RESOURCE_MIME_TYPE as s}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as m,setClientSupportsMcpApps as d,CLIENT_VERSION as u}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as h,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as w}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as v}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as P}from"./tools/training_guidance.js";import{registerFeedbackTool as x}from"./tools/feedback.js";import{registerTrainingPrepTools as I,TRAINING_PREP_URI as k}from"./tools/training_prep.js";import{registerTrainingMonitorTool as M,TRAINING_MONITOR_URI as O}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText 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 A=new e({name:"analytics-engine",version:u,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `jobs(train_map)`, `jobs(train_siom_map)`, or `jobs(train_floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool). Then `jobs(compare)`, `results(download/recolor/transition_flow)`, or `inference` as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool map (compact)\n\n| Area | Tool | Notes |\n|------|------|--------|\n| Data | `datasets` | upload, preview, analyze, list, subset, add_expression, reduce_spectral (pca/log_sample/uniform_sample/stats for long ordered numeric blocks), delete |\n| Jobs | `jobs` | train_map, train_impute (sparse data: map + dense imputed.csv), train_siom_map, train_floop_siom (entitled), status, list, compare, cancel, delete, batch_predict, run_baseline_study; `train_floop_chain` = deprecated alias for train_floop_siom |\n| Results | `results` | get (figures="none" for metrics-only), export, download, recolor (async), transition_flow (async; time-ordered rows only) |\n| Inference | `inference` | predict (regime-aware; output="compact"|"annotated"), impute_column (neighbor-pool fill for a non-training column), compare, project_columns, report |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow` | orientation + SOP |\n| Parameters | `training_guidance` | presets and field hints (API-scoped) |\n| Prep | `prepare_training` prompt, `training_prep` + `submit_prepared_training` | checklist / interactive UI |\n| Explore | `results_explorer`, `training_monitor` | optional MCP Apps; `explore_map` = deprecated alias of `results_explorer`; `jobs(status)` and `results(get)` suffice without them |\n| Other | `send_feedback` | only after user agrees |\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions, `results(recolor)`, `results(transition_flow)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Slow networks: users can raise `BARIVIA_FETCH_TIMEOUT_MS`.\n\n## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints; `training_prep` = UI + guarded submit. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `recolor`, `transition_flow`, or `project_columns`, use the **new** `job_id` returned for follow-up `results` if applicable.'});n(A,y,y,{mimeType:s},async()=>{const e=await c("results-explorer");return{contents:[{uri:y,mimeType:s,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),n(A,k,k,{mimeType:s},async()=>{const e=await c("training-prep");return{contents:[{uri:k,mimeType:s,text:e??"<html><body>Training Preparation view not built yet.</body></html>"}]}}),n(A,O,O,{mimeType:s},async()=>{const e=await c("training-monitor");return{contents:[{uri:O,mimeType:s,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),v(A),h(A),I(A),M(A),f(A),g(A,_),b(A),w(A),j(A),P(A),x(A),A.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).","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `jobs` (train/poll/compare/…; train_map accepts an optional `label` for readable compare rows), `results` (get/download/export/recolor/transition_flow; figures="none" for metrics-only), `inference` (predict; impute_column for topology-neighbor pool fill; compare; project_columns; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints) · `training_prep` + `submit_prepared_training` (interactive UI).","","**Optional UI:** `results_explorer`, `training_monitor` — nice for browsing; not required if you use `results` + `jobs(status)`.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `transition_flow` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. 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")}}]})),A.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; training_prep tool = interactive UI + submit_prepared_training.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await S(e)}}]}));const T=new t;(async function(){try{const e=await a(l,p,c);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=A.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(s))},await A.connect(T)})().catch(console.error);
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
+ }
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,9 +20,11 @@ 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.2";
23
+ export const CLIENT_VERSION = "0.10.5";
23
24
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
24
25
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
26
+ /** Self-serve account dashboard (manage plan, billing, and API keys). */
27
+ export const PUBLIC_DASHBOARD_URL = `${PUBLIC_SITE_ORIGIN}/dashboard`;
25
28
  /** Poll window for datasets(add_expression) / derive jobs (server-side work can exceed 30s). */
26
29
  export const POLL_DERIVE_MAX_MS = 120_000;
27
30
  /** Large CSV uploads may exceed default FETCH_TIMEOUT_MS. */
@@ -219,7 +222,7 @@ export function formatApiErrorMessage(status, bodyText, requestId) {
219
222
  const detail = (parsed?.error != null && String(parsed.error)) ||
220
223
  (bodyText.trim() ? bodyText.trim() : `HTTP ${status}`);
221
224
  const code = parsed?.error_code != null ? ` (error_code: ${parsed.error_code})` : "";
222
- const accountHint = ` Regenerate or verify your key via ${PUBLIC_SITE_ORIGIN} if needed.`;
225
+ const accountHint = ` Check your email or manage your key at ${PUBLIC_DASHBOARD_URL} if needed.`;
223
226
  const hint = status === 400
224
227
  ? " Check parameter types and required fields."
225
228
  : status === 401
@@ -227,7 +230,7 @@ export function formatApiErrorMessage(status, bodyText, requestId) {
227
230
  : status === 403
228
231
  ? ` Access denied for this operation (plan or permissions).${accountHint}`
229
232
  : status === 402
230
- ? ` Credits or subscription may be insufficient — see ${PUBLIC_SITE_ORIGIN}.`
233
+ ? ` Credits or subscription may be insufficient — upgrade at ${PUBLIC_DASHBOARD_URL}.`
231
234
  : status === 404
232
235
  ? " The resource may not exist or may have been deleted."
233
236
  : status === 409
@@ -277,7 +280,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
277
280
  }
278
281
  const effectiveTimeout = requestTimeoutMs ?? FETCH_TIMEOUT_MS;
279
282
  const t0 = Date.now();
280
- console.error(`${new Date().toISOString()} API -> ${method} ${path} rid=${requestId}`);
283
+ logInfo("API request", { rid: requestId, method, path });
281
284
  let lastError;
282
285
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
283
286
  try {
@@ -292,10 +295,19 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
292
295
  await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
293
296
  continue;
294
297
  }
295
- console.error(`${new Date().toISOString()} API <- ${resp.status} ${Date.now() - t0}ms rid=${requestId}`);
298
+ logInfo("API response", {
299
+ rid: requestId,
300
+ outcome: "error",
301
+ duration_ms: Date.now() - t0,
302
+ error_code: `http_${resp.status}`,
303
+ });
296
304
  throwApiError(resp.status, text, requestId);
297
305
  }
298
- console.error(`${new Date().toISOString()} API <- ${resp.status} ${Date.now() - t0}ms rid=${requestId}`);
306
+ logInfo("API response", {
307
+ rid: requestId,
308
+ outcome: "ok",
309
+ duration_ms: Date.now() - t0,
310
+ });
299
311
  return JSON.parse(text);
300
312
  }
301
313
  catch (err) {
@@ -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.tool("account", `Manage your Barivia account — check plan/license info, request cloud burst compute, view billing history.
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
  |--------|----------|
@@ -37,8 +38,11 @@ NOT FOR: Training itself — use jobs(action=train_map). This tool only manages
37
38
  const leaseData = await apiCall("GET", "/v1/compute/lease").catch(() => null);
38
39
  const algoNames = {
39
40
  train_som: "SOM",
41
+ train_impute: "Imputation",
40
42
  train_siom: "SIOM",
41
43
  train_floop_siom: "FLooP-SIOM",
44
+ cfd_mesh_convergence: "CFD mesh-convergence",
45
+ cfd_richardson_gci: "CFD Richardson/GCI",
42
46
  };
43
47
  const algos = Array.isArray(plan.allowed_algorithms) && plan.allowed_algorithms.length > 0
44
48
  ? plan.allowed_algorithms.map((a) => algoNames[a] ?? a).join(", ")
@@ -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.tool("datasets", `Manage datasets: upload, preview, list, subset, add_expression, or delete.
8
+ registerAuditedTool(server, "datasets", `Manage datasets: upload, preview, list, subset, add_expression, or delete.
8
9
 
9
10
  | Action | Use when |
10
11
  |--------|----------|
@@ -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 ({ job_id }) => handleResultsExplorer(job_id));
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 ({ job_id }) => handleResultsExplorer(job_id));
173
- server.tool("_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.", {
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 }) => {
@@ -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.tool("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
+ 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.tool("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
+ 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;
@@ -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.tool("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
+ 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
  |--------|----------|--------|
@@ -60,7 +61,7 @@ action=predict: Score rows against the trained map.
60
61
  Routing: prefer dataset_id for many rows or whenever the map uses irregular SIOM / GeneralTopology layouts — the async worker path is the supported batch scorer. Single-row rows take a fast stateless path that may return invalid_inference_input on some topologies; if so, retry with dataset_id (a one-row dataset is fine). FLooP-SIOM: use dataset_id predict first.
61
62
  When the scored set has at most ${PREDICT_PREVIEW_ROW_CAP} rows, completed responses include a short per-line preview in the tool text for chat agents.
62
63
 
63
- action=impute_column: Map-local imputation as read-only post-processing (the trained map is frozen; not a held-out validity claim). Requires dataset_id + target_column. The dataset must contain all training features (same names and cyclic expansion as predict) plus the target column. target_column must NOT have been in jobs(train_map) columns — train without it, then impute. Pools finite target values from rows whose BMUs lie on this row's BMU and its topology neighbors (BMU + neighbors, often 7 nodes on hex interior; fewer on borders if the parent map is non-periodic; periodic hex wraps), aggregated neighbourhood-distance-weighted by default (weighting="distance" — closer nodes count more; weighting="uniform" for a flat pool). Excludes the current row from its own pool. only_missing (default true): keep observed values. impute_aggregation: mean or median of the pool. Optional cv_folds (2-20) writes quality.csv (held-out MAE/RMSE/R2); target_column_kind handles categorical (mode) / cumulative (warns). Output imputed.csv: row_id, target_original, target_imputed, impute_source (observed | imputed | insufficient_data), bmu_node_index, n_patch_nodes, n_pool_rows, pool_std, pool_p5, pool_p95.
64
+ action=impute_column: Map-local imputation on the frozen trained map (read-only; not a held-out validity claim). Requires dataset_id + target_column; the dataset must contain all training features (same names + cyclic expansion as predict) plus the target column. target_column must NOT have been a training feature. Pools finite target values from rows on this row's BMU and its topology neighbors (BMU + neighbors; ~7 on hex interior, fewer on non-periodic borders), neighbourhood-distance-weighted by default (weighting="uniform" for a flat pool); excludes the current row. only_missing (default true) keeps observed values; impute_aggregation mean|median. Optional cv_folds (2-20) quality.csv (held-out MAE/RMSE/R2). target_column_kind: categorical (mode) / cumulative (warns). Output imputed.csv columns: row_id, target_original, target_imputed, impute_source (observed|imputed|insufficient_data), bmu_node_index, n_patch_nodes, n_pool_rows, pool_std, pool_p5, pool_p95.
64
65
 
65
66
  action=compare: dataset_id must refer to a dataset with the same feature set as training (same column names and preprocessing, including cyclic expansion). A = training dataset; B = cohort to compare. Density-diff: positive = B gained vs A; negative = A had more. Returns density-diff heatmap (e.g. density_diff.png).
66
67
  action=project_columns: Project one or more columns from a dataset onto the trained map. Pass dataset_id (the dataset containing the columns) and columns (array of column names). Uses cached BMUs when dataset is the training set; supports partial-feature mapping when dataset has only a subset of training features. Returns one component plane image per column. Get files via results(action=download, job_id=<returned_job_id>).
@@ -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
 
@@ -32,7 +33,7 @@ ESCALATION (action=status):
32
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 (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
+ 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. quality.csv reports two clearly-labeled numbers per column: kfold_holdout_pool (honest held-out) and resubstitution_optimistic (in-sample, optimistic). status returns progress_phase (e.g. ordering/convergence/cv/upload). High-dimensional datasets automatically adjust visualization density, metric set, and backend (call training_guidance for specifics). 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.tool("jobs", description, {
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"),
@@ -309,7 +310,7 @@ 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, default 5) → quality.csv"),
313
+ .describe("train_impute only: per-column imputation quality → quality.csv with two labeled methods (kfold_holdout_pool = held-out; resubstitution_optimistic = in-sample). 0=off, default 5."),
313
314
  viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
314
315
  .describe("Visualization density; auto-capped when feature count > 40"),
315
316
  viz_top_components: z.number().int().min(0).max(64).optional()
@@ -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.tool("results", `Retrieve, recolor, download, export, or run temporal flow on a completed map job.
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
  |--------|----------|------------|
@@ -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.tool("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
+ 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 ({ job_id }) => {
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.tool("submit_prepared_training", "Submit a previously reviewed training-prep configuration. Requires an unexpired review token and explicit confirmation.", {
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 }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barivia/barsom-mcp",
3
- "version": "0.10.2",
3
+ "version": "0.10.5",
4
4
  "description": "barSOM MCP proxy — connect any MCP client to the barSOM cloud API for Self-Organizing Map analytics",
5
5
  "keywords": [
6
6
  "mcp",