@barivia/barmesh-mcp 0.2.0 → 0.2.1

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/dist/shared.js CHANGED
@@ -4,6 +4,10 @@
4
4
  * remains a thin HTTPS client to the same Barivia API (no domain logic here).
5
5
  */
6
6
  import fs from "node:fs/promises";
7
+ import { createReadStream } from "node:fs";
8
+ import { createGzip } from "node:zlib";
9
+ import { createHash } from "node:crypto";
10
+ import { Readable } from "node:stream";
7
11
  import path from "node:path";
8
12
  import { fileURLToPath } from "node:url";
9
13
  import { logInfo } from "./logger.js";
@@ -16,10 +20,49 @@ export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ??
16
20
  export const MAX_RETRIES = 2;
17
21
  export const RETRYABLE_STATUS = new Set([502, 503, 504]);
18
22
  /** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
19
- export const CLIENT_VERSION = "0.2.0";
23
+ export const CLIENT_VERSION = "0.2.1";
20
24
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
21
25
  /** Large per-cell CSV uploads may exceed the default fetch timeout. */
22
26
  export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
27
+ /** Files at/above this size use the presigned direct-to-R2 streaming upload path. */
28
+ export const LARGE_UPLOAD_BYTES = 64 * 1024 * 1024; // 64 MB
29
+ /** Timeout for a direct presigned PUT of a large (gzipped) file to R2. */
30
+ export const PRESIGNED_PUT_TIMEOUT_MS = 30 * 60_000; // 30 min
31
+ /** Poll window for the async stage_dataset job. */
32
+ export const POLL_STAGE_MAX_MS = 30 * 60_000; // 30 min
33
+ /** Streaming SHA-256 of a file (idempotency key) without reading it into memory. */
34
+ export async function streamFileSha256(srcPath) {
35
+ return new Promise((resolve, reject) => {
36
+ const h = createHash("sha256");
37
+ const s = createReadStream(srcPath);
38
+ s.on("data", (chunk) => h.update(chunk));
39
+ s.on("end", () => resolve(h.digest("hex")));
40
+ s.on("error", reject);
41
+ });
42
+ }
43
+ /** Stream a local file through gzip directly to a presigned PUT URL (e.g. R2). */
44
+ export async function putPresignedStream(url, srcPath, contentType, timeoutMs = PRESIGNED_PUT_TIMEOUT_MS) {
45
+ const gz = createReadStream(srcPath).pipe(createGzip());
46
+ const webStream = Readable.toWeb(gz);
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
49
+ try {
50
+ const resp = await fetch(url, {
51
+ method: "PUT",
52
+ body: webStream,
53
+ headers: { "Content-Type": contentType },
54
+ duplex: "half",
55
+ signal: controller.signal,
56
+ });
57
+ if (!resp.ok) {
58
+ const t = await resp.text().catch(() => "");
59
+ throw new Error(`Presigned upload failed: HTTP ${resp.status} ${t.slice(0, 200)}`);
60
+ }
61
+ }
62
+ finally {
63
+ clearTimeout(timer);
64
+ }
65
+ }
23
66
  // ---------------------------------------------------------------------------
24
67
  // Fetch helpers
25
68
  // ---------------------------------------------------------------------------
@@ -1,8 +1,9 @@
1
1
  import { gzipSync } from "node:zlib";
2
2
  import { z } from "zod";
3
3
  import fs from "node:fs/promises";
4
+ import path from "node:path";
4
5
  import { registerAuditedTool } from "../audit.js";
5
- import { apiCall, resolveFilePathForUpload, textResult, UPLOAD_DATASET_TIMEOUT_MS, } from "../shared.js";
6
+ import { apiCall, resolveFilePathForUpload, textResult, pollUntilComplete, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, } from "../shared.js";
6
7
  export function registerDatasetsTool(server) {
7
8
  registerAuditedTool(server, "barmesh_datasets", `Upload, preview, or list the combined per-cell mesh CSV used for convergence analysis.
8
9
 
@@ -30,6 +31,36 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
30
31
  let body;
31
32
  if (file_path && file_path.length > 0) {
32
33
  const resolved = await resolveFilePathForUpload(file_path, server);
34
+ const ext = path.extname(resolved).toLowerCase();
35
+ if (ext !== ".csv" && ext !== ".tsv") {
36
+ throw new Error("Only .csv and .tsv files can be uploaded as datasets.");
37
+ }
38
+ const HARD_MAX_BYTES = 5 * 1024 * 1024 * 1024; // 5 GB
39
+ const stat = await fs.stat(resolved);
40
+ if (stat.size > HARD_MAX_BYTES) {
41
+ throw new Error(`File too large (${(stat.size / 1024 / 1024 / 1024).toFixed(2)} GB). Maximum upload size is 5 GB.`);
42
+ }
43
+ if (stat.size >= LARGE_UPLOAD_BYTES) {
44
+ const idem = await streamFileSha256(resolved);
45
+ const init = (await apiCall("POST", "/v1/datasets/upload-url", { name, size_bytes: stat.size }, { "Idempotency-Key": idem }));
46
+ const datasetId = (init.dataset_id ?? init.id);
47
+ if (init.idempotent_replay) {
48
+ return textResult({ id: datasetId, status: init.status, idempotent_replay: true,
49
+ suggested_next_step: `barmesh_datasets(action=preview, dataset_id=${datasetId})` });
50
+ }
51
+ await putPresignedStream(init.upload_url, resolved, init.content_type ?? "application/octet-stream", PRESIGNED_PUT_TIMEOUT_MS);
52
+ const fin = (await apiCall("POST", `/v1/datasets/${datasetId}/finalize`, {}));
53
+ const jobId = (fin.id ?? fin.job_id);
54
+ const poll = await pollUntilComplete(jobId, POLL_STAGE_MAX_MS);
55
+ if (poll.status === "failed") {
56
+ return textResult({ id: datasetId, status: "failed", error: poll.error ?? "staging failed" });
57
+ }
58
+ const ready = poll.status === "completed";
59
+ return textResult({ id: datasetId, status: ready ? "ready" : "staging", job_id: jobId,
60
+ suggested_next_step: ready
61
+ ? `barmesh_datasets(action=preview, dataset_id=${datasetId})`
62
+ : `Still staging; poll jobs(action=status, job_id="${jobId}").` });
63
+ }
33
64
  body = await fs.readFile(resolved, "utf-8");
34
65
  }
35
66
  else if (csv_data && csv_data.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barivia/barmesh-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "barmesh MCP proxy — SOM-based CFD mesh-convergence and Richardson/GCI analysis on the Barivia cloud API",
5
5
  "keywords": [
6
6
  "mcp",