@vcad/mcp 0.9.4-main.11 → 0.9.4-main.13

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.mjs +82 -504
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -6,4 +6,4 @@ vcad's MCP server as a self-contained bundle (server + BRep kernel WASM).
6
6
  { "mcpServers": { "vcad": { "command": "npx", "args": ["-y", "@vcad/mcp"] } } }
7
7
  ```
8
8
 
9
- Built from 681857ca2f62f173237de79ff2621cd1609480a7 at 2026-07-24T16:25:54.297Z. Source: https://github.com/ecto/vcad
9
+ Built from 70552964e3760fb5b103235917b00364f6d45763 at 2026-07-24T16:29:07.170Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -8022,14 +8022,22 @@ async function loadAndInit() {
8022
8022
  }
8023
8023
  await mod.default(hint ? { module_or_path: hint } : void 0);
8024
8024
  }
8025
+ assertKernelBaseline(mod);
8025
8026
  wasmModule2 = mod;
8026
8027
  generation++;
8027
8028
  return mod;
8028
8029
  }
8030
+ function assertKernelBaseline(mod) {
8031
+ const missing = REQUIRED_BINDINGS.filter((name) => typeof mod[name] !== "function");
8032
+ if (missing.length > 0) {
8033
+ const version2 = typeof mod.get_kernel_version === "function" ? mod.get_kernel_version() : "unknown";
8034
+ throw new Error(`stale @vcad/kernel-wasm bundle (kernel version ${version2}): missing required bindings ${missing.join(", ")} \u2014 rebuild the workspace (VCAD_WASM_SKIP unset) or update to a current bundle`);
8035
+ }
8036
+ }
8029
8037
  function getKernelWasmSync() {
8030
8038
  return wasmModule2;
8031
8039
  }
8032
- var wasmPromise, wasmModule2, wasmInputHint, bindgenStarted, lastModuleBuffer, lastTrap, generation;
8040
+ var wasmPromise, wasmModule2, wasmInputHint, bindgenStarted, lastModuleBuffer, lastTrap, generation, REQUIRED_BINDINGS;
8033
8041
  var init_wasm_singleton = __esm({
8034
8042
  "../engine/dist/wasm-singleton.js"() {
8035
8043
  "use strict";
@@ -8038,6 +8046,13 @@ var init_wasm_singleton = __esm({
8038
8046
  bindgenStarted = false;
8039
8047
  lastTrap = null;
8040
8048
  generation = 0;
8049
+ REQUIRED_BINDINGS = [
8050
+ "evaluateDocument",
8051
+ "documentDiff",
8052
+ "documentMerge",
8053
+ "documentToLoon",
8054
+ "documentToLoonChecked"
8055
+ ];
8041
8056
  }
8042
8057
  });
8043
8058
 
@@ -8509,7 +8524,10 @@ function evaluateDocument2(doc, kernel2, options = {}) {
8509
8524
  const resolved = resolveDocument(doc);
8510
8525
  doc = resolved.doc;
8511
8526
  doc = expandPartInstances(doc, kernel2);
8512
- if (kernel2.evaluateDocument) {
8527
+ if (!kernel2.evaluateDocument) {
8528
+ throw new Error("kernel WASM bundle is missing the evaluateDocument binding \u2014 rebuild @vcad/kernel-wasm (stale bundle); old bundles are unsupported");
8529
+ }
8530
+ {
8513
8531
  try {
8514
8532
  const docJson = JSON.stringify(doc);
8515
8533
  const result = kernel2.evaluateDocument(docJson, options.skipClashDetection ?? false);
@@ -9637,114 +9655,11 @@ var init_gpu = __esm({
9637
9655
  });
9638
9656
 
9639
9657
  // ../engine/dist/diff.js
9640
- function decompose(doc) {
9641
- const obj = doc;
9642
- const entities = /* @__PURE__ */ new Map();
9643
- for (const [field, kind] of MAP_TABLES) {
9644
- const table = obj[field];
9645
- if (table && typeof table === "object" && !Array.isArray(table)) {
9646
- for (const [id, v] of Object.entries(table)) {
9647
- entities.set(`${kind}\0${id}`, v);
9648
- }
9649
- }
9650
- }
9651
- for (const [field, kind, idField] of VEC_TABLES) {
9652
- const items = obj[field];
9653
- if (Array.isArray(items)) {
9654
- for (const item of items) {
9655
- const raw = item?.[idField];
9656
- const id = typeof raw === "string" ? raw : JSON.stringify(raw);
9657
- entities.set(`${kind}\0${id}`, item);
9658
- }
9659
- }
9660
- }
9661
- for (const field of DOC_FIELDS) {
9662
- const v = obj[field];
9663
- if (v !== void 0 && v !== null)
9664
- entities.set(`doc\0${field}`, v);
9665
- }
9666
- return entities;
9667
- }
9668
- function deepEqual(a, b) {
9669
- if (a === b)
9670
- return true;
9671
- if (typeof a !== typeof b || a === null || b === null)
9672
- return false;
9673
- if (typeof a !== "object")
9674
- return false;
9675
- if (Array.isArray(a) !== Array.isArray(b))
9676
- return false;
9677
- if (Array.isArray(a) && Array.isArray(b)) {
9678
- if (a.length !== b.length)
9679
- return false;
9680
- return a.every((v, i) => deepEqual(v, b[i]));
9681
- }
9682
- const ka = Object.keys(a).filter((k) => a[k] !== void 0);
9683
- const kb = Object.keys(b).filter((k) => b[k] !== void 0);
9684
- if (ka.length !== kb.length)
9685
- return false;
9686
- return ka.every((k) => deepEqual(a[k], b[k]));
9687
- }
9688
- function valueDiff(prefix, oldV, newV, out) {
9689
- if (deepEqual(oldV, newV))
9690
- return;
9691
- const join4 = (k) => prefix ? `${prefix}.${k}` : k;
9692
- const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
9693
- if (isObj(oldV) && isObj(newV)) {
9694
- const keys = /* @__PURE__ */ new Set([...Object.keys(oldV), ...Object.keys(newV)]);
9695
- for (const k of keys) {
9696
- valueDiff(join4(k), oldV[k] ?? null, newV[k] ?? null, out);
9697
- }
9698
- return;
9699
- }
9700
- if (Array.isArray(oldV) && Array.isArray(newV) && oldV.length === newV.length) {
9701
- oldV.forEach((v, i) => valueDiff(join4(String(i)), v, newV[i], out));
9702
- return;
9703
- }
9704
- out.push({ path: prefix, old: oldV, new: newV });
9705
- }
9706
- function entityName(v) {
9707
- const n = v?.name;
9708
- return typeof n === "string" ? n : void 0;
9709
- }
9710
- function semanticDiffFallback(oldDoc, newDoc) {
9711
- const a = decompose(oldDoc);
9712
- const b = decompose(newDoc);
9713
- const keys = [.../* @__PURE__ */ new Set([...a.keys(), ...b.keys()])].sort();
9714
- const changes = [];
9715
- for (const key of keys) {
9716
- const [kind, id] = key.split("\0");
9717
- const va = a.get(key);
9718
- const vb = b.get(key);
9719
- if (va === void 0 && vb !== void 0) {
9720
- changes.push({ kind, id, name: entityName(vb), type: "added", value: vb });
9721
- } else if (va !== void 0 && vb === void 0) {
9722
- changes.push({ kind, id, name: entityName(va), type: "removed", value: va });
9723
- } else if (!deepEqual(va, vb)) {
9724
- const fields = [];
9725
- valueDiff("", va, vb, fields);
9726
- if (fields.length > 0) {
9727
- changes.push({
9728
- kind,
9729
- id,
9730
- name: entityName(vb) ?? entityName(va),
9731
- type: "modified",
9732
- fields
9733
- });
9734
- }
9735
- }
9736
- }
9737
- return { changes };
9738
- }
9739
9658
  function semanticDiff(oldDoc, newDoc, kernel2) {
9740
- if (kernel2?.documentDiff) {
9741
- try {
9742
- return kernel2.documentDiff(JSON.stringify(oldDoc), JSON.stringify(newDoc));
9743
- } catch (e) {
9744
- console.warn("[diff] kernel documentDiff failed, using TS fallback:", e);
9745
- }
9659
+ if (!kernel2?.documentDiff) {
9660
+ throw new Error("semantic diff needs a kernel build with the documentDiff binding \u2014 ensure the kernel WASM is initialized (getKernelWasm) and current");
9746
9661
  }
9747
- return semanticDiffFallback(oldDoc, newDoc);
9662
+ return kernel2.documentDiff(JSON.stringify(oldDoc), JSON.stringify(newDoc));
9748
9663
  }
9749
9664
  function mergeAvailable(kernel2) {
9750
9665
  return typeof kernel2?.documentMerge === "function";
@@ -9755,33 +9670,9 @@ function threeWayMerge(base, ours, theirs, resolutions, kernel2) {
9755
9670
  }
9756
9671
  return kernel2.documentMerge(JSON.stringify(base), JSON.stringify(ours), JSON.stringify(theirs), resolutions.length > 0 ? JSON.stringify(resolutions) : null);
9757
9672
  }
9758
- var MAP_TABLES, VEC_TABLES, DOC_FIELDS;
9759
9673
  var init_diff = __esm({
9760
9674
  "../engine/dist/diff.js"() {
9761
9675
  "use strict";
9762
- MAP_TABLES = [
9763
- ["nodes", "node"],
9764
- ["materials", "material"],
9765
- ["part_materials", "part-material"],
9766
- ["partDefs", "part-def"],
9767
- ["parameters", "parameter"],
9768
- ["bindings", "binding"]
9769
- ];
9770
- VEC_TABLES = [
9771
- ["roots", "root", "root"],
9772
- ["instances", "instance", "id"],
9773
- ["joints", "joint", "id"],
9774
- ["clearance_specs", "clearance", "label"]
9775
- ];
9776
- DOC_FIELDS = [
9777
- "version",
9778
- "scene",
9779
- "groundInstanceId",
9780
- "schematic",
9781
- "pcb",
9782
- "molecule",
9783
- "timeline"
9784
- ];
9785
9676
  }
9786
9677
  });
9787
9678
 
@@ -11631,7 +11522,6 @@ __export(dist_exports, {
11631
11522
  searchEcadParts: () => searchEcadParts,
11632
11523
  searchParts: () => searchParts,
11633
11524
  semanticDiff: () => semanticDiff,
11634
- semanticDiffFallback: () => semanticDiffFallback,
11635
11525
  solveDesignConstraints: () => solveDesignConstraints2,
11636
11526
  solveForwardKinematics: () => solveForwardKinematics2,
11637
11527
  threeWayMerge: () => threeWayMerge,
@@ -11801,6 +11691,8 @@ var init_dist = __esm({
11801
11691
  sheetMetalSequence: wasmModule7.sheetMetalSequence,
11802
11692
  nestSheetMetalParts: wasmModule7.nestSheetMetalParts,
11803
11693
  getSheetMetalMaterials: wasmModule7.getSheetMetalMaterials,
11694
+ simulateStrikeKernel: wasmModule7.simulateStrikeKernel,
11695
+ noteToHz: wasmModule7.noteToHz,
11804
11696
  getSheetMetalBendTable: wasmModule7.getSheetMetalBendTable,
11805
11697
  getSheetMetalShopCatalog: wasmModule7.getSheetMetalShopCatalog,
11806
11698
  sheetMetalFoldedStep: wasmModule7.sheetMetalFoldedStep,
@@ -12342,6 +12234,27 @@ var init_dist = __esm({
12342
12234
  return null;
12343
12235
  return sheetMetalSequence2(chain, this.kernel);
12344
12236
  }
12237
+ /**
12238
+ * Run the kernel's mallet-strike pipeline (free-free bar modal analysis +
12239
+ * synthesis + FFT verdict). Input/output shapes are JSON mirrors of
12240
+ * `vcad_kernel_acoustics::strike::{StrikeInput, StrikeResult}` with the
12241
+ * WAV base64-encoded as `wav_base64`.
12242
+ */
12243
+ simulateStrike(input) {
12244
+ const kernel2 = this.kernel;
12245
+ if (!kernel2.simulateStrikeKernel) {
12246
+ throw new Error("kernel WASM build lacks simulateStrikeKernel \u2014 rebuild @vcad/kernel-wasm");
12247
+ }
12248
+ return JSON.parse(kernel2.simulateStrikeKernel(JSON.stringify(input)));
12249
+ }
12250
+ /** Parse a note name ("C6", "F#4") to Hz via the kernel. */
12251
+ noteToHz(note) {
12252
+ const kernel2 = this.kernel;
12253
+ if (!kernel2.noteToHz) {
12254
+ throw new Error("kernel WASM build lacks noteToHz \u2014 rebuild @vcad/kernel-wasm");
12255
+ }
12256
+ return kernel2.noteToHz(note);
12257
+ }
12345
12258
  /** Return the kernel's curated sheet-metal materials registry. */
12346
12259
  getSheetMetalMaterials() {
12347
12260
  return getSheetMetalMaterials2(this.kernel);
@@ -26615,33 +26528,33 @@ var init_server = __esm({
26615
26528
  });
26616
26529
 
26617
26530
  // node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
26618
- function assertToolsCallTaskCapability(requests, method, entityName2) {
26531
+ function assertToolsCallTaskCapability(requests, method, entityName) {
26619
26532
  if (!requests) {
26620
- throw new Error(`${entityName2} does not support task creation (required for ${method})`);
26533
+ throw new Error(`${entityName} does not support task creation (required for ${method})`);
26621
26534
  }
26622
26535
  switch (method) {
26623
26536
  case "tools/call":
26624
26537
  if (!requests.tools?.call) {
26625
- throw new Error(`${entityName2} does not support task creation for tools/call (required for ${method})`);
26538
+ throw new Error(`${entityName} does not support task creation for tools/call (required for ${method})`);
26626
26539
  }
26627
26540
  break;
26628
26541
  default:
26629
26542
  break;
26630
26543
  }
26631
26544
  }
26632
- function assertClientRequestTaskCapability(requests, method, entityName2) {
26545
+ function assertClientRequestTaskCapability(requests, method, entityName) {
26633
26546
  if (!requests) {
26634
- throw new Error(`${entityName2} does not support task creation (required for ${method})`);
26547
+ throw new Error(`${entityName} does not support task creation (required for ${method})`);
26635
26548
  }
26636
26549
  switch (method) {
26637
26550
  case "sampling/createMessage":
26638
26551
  if (!requests.sampling?.createMessage) {
26639
- throw new Error(`${entityName2} does not support task creation for sampling/createMessage (required for ${method})`);
26552
+ throw new Error(`${entityName} does not support task creation for sampling/createMessage (required for ${method})`);
26640
26553
  }
26641
26554
  break;
26642
26555
  case "elicitation/create":
26643
26556
  if (!requests.elicitation?.create) {
26644
- throw new Error(`${entityName2} does not support task creation for elicitation/create (required for ${method})`);
26557
+ throw new Error(`${entityName} does not support task creation for elicitation/create (required for ${method})`);
26645
26558
  }
26646
26559
  break;
26647
26560
  default:
@@ -40930,6 +40843,7 @@ var init_save_load = __esm({
40930
40843
  var init_document_to_loon = __esm({
40931
40844
  "../core/dist/utils/document-to-loon.js"() {
40932
40845
  "use strict";
40846
+ init_wasm_singleton2();
40933
40847
  }
40934
40848
  });
40935
40849
 
@@ -72670,335 +72584,6 @@ var init_sheet_metal2 = __esm({
72670
72584
 
72671
72585
  // src/tools/acoustics.ts
72672
72586
  import { writeFileSync as writeFileSync5 } from "node:fs";
72673
- function freeFreeBetaL(count) {
72674
- const roots = [];
72675
- for (let n = 1; n <= count; n++) {
72676
- let x = (2 * n + 1) * Math.PI / 2;
72677
- for (let i = 0; i < 50; i++) {
72678
- const f = Math.cosh(x) * Math.cos(x) - 1;
72679
- const df = Math.sinh(x) * Math.cos(x) - Math.cosh(x) * Math.sin(x);
72680
- const step = f / df;
72681
- x -= step;
72682
- if (Math.abs(step) < 1e-13) break;
72683
- }
72684
- roots.push(x);
72685
- }
72686
- return roots;
72687
- }
72688
- function modeShape(betaL, xi) {
72689
- const sigma = (Math.cosh(betaL) - Math.cos(betaL)) / (Math.sinh(betaL) - Math.sin(betaL));
72690
- const raw = (t2) => Math.cosh(betaL * t2) + Math.cos(betaL * t2) - sigma * (Math.sinh(betaL * t2) + Math.sin(betaL * t2));
72691
- return raw(xi) / Math.abs(raw(0));
72692
- }
72693
- function closedFormHz(bar, count) {
72694
- const L = bar.length_mm / 1e3;
72695
- const t2 = bar.thickness_mm / 1e3;
72696
- const E = bar.modulus_gpa * 1e9;
72697
- const c = t2 * Math.sqrt(E / (12 * bar.density_kg_m3));
72698
- return freeFreeBetaL(count).map((bl) => bl * bl * c / (2 * Math.PI * L * L));
72699
- }
72700
- function effectiveWidthMm(bar, xMm) {
72701
- let w = bar.width_mm;
72702
- const r = bar.hole_dia_mm / 2;
72703
- for (const h of bar.holes_mm) {
72704
- const d = Math.abs(xMm - h);
72705
- if (d < r) w -= 2 * Math.sqrt(r * r - d * d);
72706
- }
72707
- return Math.max(w, 1e-6);
72708
- }
72709
- function femHz(bar, count, nel = 96) {
72710
- const L = bar.length_mm / 1e3;
72711
- const t2 = bar.thickness_mm / 1e3;
72712
- const E = bar.modulus_gpa * 1e9;
72713
- const rho = bar.density_kg_m3;
72714
- const nDof = 2 * (nel + 1);
72715
- const K = zeros(nDof, nDof);
72716
- const M = zeros(nDof, nDof);
72717
- const le = L / nel;
72718
- const gp = [-0.8611363115940526, -0.3399810435848563, 0.3399810435848563, 0.8611363115940526];
72719
- const gw = [0.3478548451374538, 0.6521451548625461, 0.6521451548625461, 0.3478548451374538];
72720
- for (let e = 0; e < nel; e++) {
72721
- const x0 = e * le;
72722
- const ke = zeros(4, 4);
72723
- const me = zeros(4, 4);
72724
- for (let g = 0; g < 4; g++) {
72725
- const xi = gp[g];
72726
- const x = x0 + (xi + 1) / 2 * le;
72727
- const wEff = effectiveWidthMm(bar, x * 1e3) / 1e3;
72728
- const A = wEff * t2;
72729
- const I = wEff * t2 * t2 * t2 / 12;
72730
- const s = le / 2;
72731
- const N = [
72732
- 0.25 * (1 - xi) * (1 - xi) * (2 + xi),
72733
- s * 0.25 * (1 - xi) * (1 - xi) * (1 + xi),
72734
- 0.25 * (1 + xi) * (1 + xi) * (2 - xi),
72735
- s * 0.25 * (1 + xi) * (1 + xi) * (xi - 1)
72736
- ];
72737
- const c2 = 2 / le * (2 / le);
72738
- const B = [
72739
- 1.5 * xi * c2,
72740
- s * (1.5 * xi - 0.5) * c2,
72741
- -1.5 * xi * c2,
72742
- s * (1.5 * xi + 0.5) * c2
72743
- ];
72744
- const wJ = gw[g] * s;
72745
- for (let i = 0; i < 4; i++) {
72746
- for (let j = 0; j < 4; j++) {
72747
- ke[i][j] += E * I * B[i] * B[j] * wJ;
72748
- me[i][j] += rho * A * N[i] * N[j] * wJ;
72749
- }
72750
- }
72751
- }
72752
- const dof = [2 * e, 2 * e + 1, 2 * e + 2, 2 * e + 3];
72753
- for (let i = 0; i < 4; i++) {
72754
- for (let j = 0; j < 4; j++) {
72755
- K[dof[i]][dof[j]] += ke[i][j];
72756
- M[dof[i]][dof[j]] += me[i][j];
72757
- }
72758
- }
72759
- }
72760
- const Lc = cholesky(M);
72761
- const C = congruence(K, Lc);
72762
- const lambda = jacobiEigenvalues(C);
72763
- lambda.sort((a, b) => a - b);
72764
- const elastic = lambda.filter((l) => l > (2 * Math.PI) ** 2);
72765
- return elastic.slice(0, count).map((l) => Math.sqrt(l) / (2 * Math.PI));
72766
- }
72767
- function zeros(r, c) {
72768
- return Array.from({ length: r }, () => new Array(c).fill(0));
72769
- }
72770
- function cholesky(A) {
72771
- const n = A.length;
72772
- const L = zeros(n, n);
72773
- for (let i = 0; i < n; i++) {
72774
- for (let j = 0; j <= i; j++) {
72775
- let s = A[i][j];
72776
- for (let k = 0; k < j; k++) s -= L[i][k] * L[j][k];
72777
- if (i === j) {
72778
- if (s <= 0) throw new Error("mass matrix not positive definite");
72779
- L[i][i] = Math.sqrt(s);
72780
- } else {
72781
- L[i][j] = s / L[j][j];
72782
- }
72783
- }
72784
- }
72785
- return L;
72786
- }
72787
- function congruence(K, L) {
72788
- const n = K.length;
72789
- const Y = zeros(n, n);
72790
- for (let c = 0; c < n; c++) {
72791
- for (let i = 0; i < n; i++) {
72792
- let s = K[i][c];
72793
- for (let k = 0; k < i; k++) s -= L[i][k] * Y[k][c];
72794
- Y[i][c] = s / L[i][i];
72795
- }
72796
- }
72797
- const C = zeros(n, n);
72798
- for (let r = 0; r < n; r++) {
72799
- for (let i = 0; i < n; i++) {
72800
- let s = Y[r][i];
72801
- for (let k = 0; k < i; k++) s -= L[i][k] * C[r][k];
72802
- C[r][i] = s / L[i][i];
72803
- }
72804
- }
72805
- for (let i = 0; i < n; i++)
72806
- for (let j = 0; j < i; j++) {
72807
- const v = 0.5 * (C[i][j] + C[j][i]);
72808
- C[i][j] = v;
72809
- C[j][i] = v;
72810
- }
72811
- return C;
72812
- }
72813
- function jacobiEigenvalues(A) {
72814
- const n = A.length;
72815
- const a = A.map((row) => row.slice());
72816
- for (let sweep = 0; sweep < 30; sweep++) {
72817
- let off = 0;
72818
- for (let i = 0; i < n; i++)
72819
- for (let j = i + 1; j < n; j++) off += a[i][j] * a[i][j];
72820
- if (off < 1e-18 * n * n) break;
72821
- for (let p = 0; p < n - 1; p++) {
72822
- for (let q = p + 1; q < n; q++) {
72823
- if (Math.abs(a[p][q]) < 1e-30) continue;
72824
- const theta = (a[q][q] - a[p][p]) / (2 * a[p][q]);
72825
- const t2 = Math.sign(theta) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
72826
- const c = 1 / Math.sqrt(t2 * t2 + 1);
72827
- const s = t2 * c;
72828
- for (let k = 0; k < n; k++) {
72829
- const akp = a[k][p];
72830
- const akq = a[k][q];
72831
- a[k][p] = c * akp - s * akq;
72832
- a[k][q] = s * akp + c * akq;
72833
- }
72834
- for (let k = 0; k < n; k++) {
72835
- const apk = a[p][k];
72836
- const aqk = a[q][k];
72837
- a[p][k] = c * apk - s * aqk;
72838
- a[q][k] = s * apk + c * aqk;
72839
- }
72840
- }
72841
- }
72842
- }
72843
- return a.map((row, i) => row[i]);
72844
- }
72845
- function malletSpectrum(hz, contactMs) {
72846
- const tc = contactMs / 1e3;
72847
- const u = 2 * hz * tc;
72848
- const denom = Math.abs(1 - u * u);
72849
- if (denom < 1e-9) return Math.PI / 4;
72850
- return Math.abs(Math.cos(Math.PI * hz * tc)) / denom;
72851
- }
72852
- function strikeModes(bar, femFreqs, strikeFrac, contactMs, materialQ = 2500, suspensionQ0 = 150) {
72853
- const betas = freeFreeBetaL(femFreqs.length);
72854
- const modes = femFreqs.map((hz, i) => {
72855
- const phiStrike = modeShape(betas[i], strikeFrac);
72856
- const gain = Math.abs(phiStrike) * malletSpectrum(hz, contactMs);
72857
- let phiSq = 0;
72858
- for (const h of bar.holes_mm) {
72859
- const phi = modeShape(betas[i], h / bar.length_mm);
72860
- phiSq += phi * phi;
72861
- }
72862
- const qSusp = suspensionQ0 / Math.max(phiSq, 1e-9);
72863
- const q = 1 / (1 / materialQ + 1 / qSusp);
72864
- return {
72865
- n: i + 1,
72866
- hz,
72867
- gain,
72868
- q,
72869
- t60_s: Math.log(1e3) * q / (Math.PI * hz)
72870
- };
72871
- });
72872
- const peak = Math.max(...modes.map((m) => m.gain), 1e-12);
72873
- for (const m of modes) m.gain /= peak;
72874
- return modes;
72875
- }
72876
- function synthesize(modes, durationS, sampleRate) {
72877
- const n = Math.floor(durationS * sampleRate);
72878
- const out = new Float64Array(n);
72879
- for (const m of modes) {
72880
- if (m.hz >= 0.45 * sampleRate) continue;
72881
- const w = 2 * Math.PI * m.hz / sampleRate;
72882
- const tau = m.q / (Math.PI * m.hz);
72883
- const decay = Math.exp(-1 / (tau * sampleRate));
72884
- let amp = m.gain;
72885
- for (let k = 0; k < n; k++) {
72886
- out[k] += amp * Math.sin(w * k);
72887
- amp *= decay;
72888
- }
72889
- }
72890
- let peak = 1e-12;
72891
- for (let k = 0; k < n; k++) peak = Math.max(peak, Math.abs(out[k]));
72892
- const norm = 0.891 / peak;
72893
- for (let k = 0; k < n; k++) out[k] *= norm;
72894
- return out;
72895
- }
72896
- function encodeWav(samples, sampleRate) {
72897
- const n = samples.length;
72898
- const buf = new ArrayBuffer(44 + n * 2);
72899
- const v = new DataView(buf);
72900
- const str2 = (off, s) => {
72901
- for (let i = 0; i < s.length; i++) v.setUint8(off + i, s.charCodeAt(i));
72902
- };
72903
- str2(0, "RIFF");
72904
- v.setUint32(4, 36 + n * 2, true);
72905
- str2(8, "WAVE");
72906
- str2(12, "fmt ");
72907
- v.setUint32(16, 16, true);
72908
- v.setUint16(20, 1, true);
72909
- v.setUint16(22, 1, true);
72910
- v.setUint32(24, sampleRate, true);
72911
- v.setUint32(28, sampleRate * 2, true);
72912
- v.setUint16(32, 2, true);
72913
- v.setUint16(34, 16, true);
72914
- str2(36, "data");
72915
- v.setUint32(40, n * 2, true);
72916
- for (let i = 0; i < n; i++) {
72917
- const s = Math.max(-1, Math.min(1, samples[i]));
72918
- v.setInt16(44 + i * 2, Math.round(s * 32767), true);
72919
- }
72920
- return new Uint8Array(buf);
72921
- }
72922
- function fft(re, im) {
72923
- const n = re.length;
72924
- for (let i = 1, j = 0; i < n; i++) {
72925
- let bit = n >> 1;
72926
- for (; j & bit; bit >>= 1) j ^= bit;
72927
- j ^= bit;
72928
- if (i < j) {
72929
- [re[i], re[j]] = [re[j], re[i]];
72930
- [im[i], im[j]] = [im[j], im[i]];
72931
- }
72932
- }
72933
- for (let len = 2; len <= n; len <<= 1) {
72934
- const ang = -2 * Math.PI / len;
72935
- const wr = Math.cos(ang);
72936
- const wi = Math.sin(ang);
72937
- for (let i = 0; i < n; i += len) {
72938
- let cr = 1;
72939
- let ci = 0;
72940
- for (let k = 0; k < len / 2; k++) {
72941
- const ur = re[i + k];
72942
- const ui = im[i + k];
72943
- const vr = re[i + k + len / 2] * cr - im[i + k + len / 2] * ci;
72944
- const vi = re[i + k + len / 2] * ci + im[i + k + len / 2] * cr;
72945
- re[i + k] = ur + vr;
72946
- im[i + k] = ui + vi;
72947
- re[i + k + len / 2] = ur - vr;
72948
- im[i + k + len / 2] = ui - vi;
72949
- const ncr = cr * wr - ci * wi;
72950
- ci = cr * wi + ci * wr;
72951
- cr = ncr;
72952
- }
72953
- }
72954
- }
72955
- }
72956
- function spectrumPeaks(samples, sampleRate, maxPeaks = 8) {
72957
- const n = Math.min(65536, 1 << Math.floor(Math.log2(samples.length)));
72958
- const re = new Float64Array(n);
72959
- const im = new Float64Array(n);
72960
- for (let i = 0; i < n; i++) {
72961
- const w = 0.5 * (1 - Math.cos(2 * Math.PI * i / (n - 1)));
72962
- re[i] = samples[i] * w;
72963
- }
72964
- fft(re, im);
72965
- const half = n / 2;
72966
- const mag = new Float64Array(half);
72967
- let maxMag = 1e-30;
72968
- for (let i = 0; i < half; i++) {
72969
- mag[i] = Math.hypot(re[i], im[i]);
72970
- maxMag = Math.max(maxMag, mag[i]);
72971
- }
72972
- const peaks = [];
72973
- const floorDb = -60;
72974
- for (let i = 2; i < half - 2; i++) {
72975
- if (mag[i] <= mag[i - 1] || mag[i] < mag[i + 1]) continue;
72976
- const db = 20 * Math.log10(mag[i] / maxMag);
72977
- if (db < floorDb) continue;
72978
- const a = Math.log(mag[i - 1] + 1e-30);
72979
- const b = Math.log(mag[i] + 1e-30);
72980
- const c = Math.log(mag[i + 1] + 1e-30);
72981
- const delta = 0.5 * (a - c) / (a - 2 * b + c || 1e-30);
72982
- peaks.push({ hz: (i + delta) * sampleRate / n, db });
72983
- }
72984
- peaks.sort((a, b) => b.db - a.db);
72985
- const kept = [];
72986
- for (const p of peaks) {
72987
- if (kept.every((k) => Math.abs(p.hz - k.hz) / k.hz > 0.03)) kept.push(p);
72988
- if (kept.length >= maxPeaks) break;
72989
- }
72990
- return kept.sort((a, b) => a.hz - b.hz);
72991
- }
72992
- function noteToHz(note) {
72993
- const m = /^([A-Ga-g])([#b]?)(-?\d)$/.exec(note.trim());
72994
- if (!m) throw new Error(`unparseable note ${JSON.stringify(note)} \u2014 use e.g. "C6", "F#4"`);
72995
- const base = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
72996
- let semis = base[m[1].toUpperCase()];
72997
- if (m[2] === "#") semis += 1;
72998
- if (m[2] === "b") semis -= 1;
72999
- const midi = (Number(m[3]) + 1) * 12 + semis;
73000
- return 440 * 2 ** ((midi - 69) / 12);
73001
- }
73002
72587
  function lookupMaterial(engine, name) {
73003
72588
  const key = name.trim().toLowerCase().replace(/[_ ]/g, "-");
73004
72589
  const registry4 = engine.getSheetMetalMaterials();
@@ -73045,10 +72630,8 @@ function barFromDocument(engine, documentId) {
73045
72630
  let holeDia = 0;
73046
72631
  for (const loop of holeLoops) {
73047
72632
  const cs = loop.map((p) => axis === "x" ? p.x : p.y);
73048
- const os = loop.map((p) => axis === "x" ? p.y : p.x);
73049
72633
  holesMm.push((Math.min(...cs) + Math.max(...cs)) / 2);
73050
72634
  holeDia = Math.max(holeDia, Math.max(...cs) - Math.min(...cs));
73051
- void os;
73052
72635
  }
73053
72636
  return {
73054
72637
  bar: {
@@ -73095,28 +72678,26 @@ function simulateStrike(input, engine) {
73095
72678
  const durationS = Math.min(Math.max(Number(a.duration_s ?? 2.5), 0.5), 10);
73096
72679
  const strikeFrac = Math.min(Math.max(Number(a.strike_position ?? 0.5), 0.02), 0.98);
73097
72680
  const contactMs = Math.min(Math.max(Number(a.mallet_contact_ms ?? 0.5), 0.05), 10);
73098
- const nModes = 6;
73099
- const closed = closedFormHz(bar, nModes);
73100
- const fem = femHz(bar, nModes);
73101
- const modes = strikeModes(bar, fem, strikeFrac, contactMs);
73102
- const audible = modes.filter((m) => m.hz < 0.45 * sampleRate && m.gain > 1e-4);
73103
- const samples = synthesize(audible, durationS, sampleRate);
73104
- const peaks = spectrumPeaks(samples, sampleRate);
73105
72681
  let expected;
73106
72682
  if (typeof a.expect_hz === "number") expected = a.expect_hz;
73107
- else if (typeof a.note === "string") expected = noteToHz(a.note);
73108
- const toleranceCents = Number(a.tolerance_cents ?? 10);
73109
- const dominant = peaks.reduce(
73110
- (best, p) => p.db > best.db ? p : best,
73111
- { hz: 0, db: -Infinity }
73112
- );
73113
- const verdict = expected !== void 0 ? {
73114
- expected_hz: expected,
73115
- measured_hz: dominant.hz,
73116
- cents_error: cents(dominant.hz, expected),
73117
- tolerance_cents: toleranceCents,
73118
- pass: Math.abs(cents(dominant.hz, expected)) <= toleranceCents
73119
- } : void 0;
72683
+ else if (typeof a.note === "string") expected = engine.noteToHz(a.note);
72684
+ const wantWav = typeof a.wav_filename === "string" && a.wav_filename.length > 0;
72685
+ if (wantWav && !/\.wav$/i.test(a.wav_filename)) {
72686
+ throw new Error("wav_filename must end in .wav");
72687
+ }
72688
+ const result = engine.simulateStrike({
72689
+ bar,
72690
+ strike_position: strikeFrac,
72691
+ mallet_contact_ms: contactMs,
72692
+ duration_s: durationS,
72693
+ sample_rate: sampleRate,
72694
+ n_modes: 6,
72695
+ expected_hz: expected ?? null,
72696
+ tolerance_cents: Number(a.tolerance_cents ?? 10),
72697
+ include_wav: wantWav
72698
+ });
72699
+ const closed = result.closed_form_hz;
72700
+ const fem = result.fem_hz;
73120
72701
  const payload = {
73121
72702
  bar: { ...bar, material: mat.name, material_display: mat.display_name },
73122
72703
  physics: {
@@ -73133,22 +72714,19 @@ function simulateStrike(input, engine) {
73133
72714
  duration_s: durationS,
73134
72715
  sample_rate: sampleRate
73135
72716
  },
73136
- modes: audible.map((m) => ({
72717
+ modes: result.modes.map((m) => ({
73137
72718
  n: m.n,
73138
72719
  hz: round24(m.hz),
73139
72720
  gain: round4(m.gain),
73140
72721
  q: Math.round(m.q),
73141
72722
  t60_s: round24(m.t60_s)
73142
72723
  })),
73143
- spectrum_peaks: peaks.map((p) => ({ hz: round24(p.hz), db: round24(p.db) })),
73144
- ...verdict ? { verdict } : {},
72724
+ spectrum_peaks: result.spectrum_peaks.map((p) => ({ hz: round24(p.hz), db: round24(p.db) })),
72725
+ ...result.verdict ? { verdict: result.verdict } : {},
73145
72726
  limits: "1-D transverse bending only (no torsional/lateral modes); decay Q is a heuristic (material + cord-at-holes), frequencies are not"
73146
72727
  };
73147
- if (typeof a.wav_filename === "string" && a.wav_filename.length > 0) {
73148
- if (!/\.wav$/i.test(a.wav_filename)) {
73149
- throw new Error("wav_filename must end in .wav");
73150
- }
73151
- const bytes = encodeWav(samples, sampleRate);
72728
+ if (wantWav && result.wav_base64) {
72729
+ const bytes = Buffer.from(result.wav_base64, "base64");
73152
72730
  if (isRemoteDeployment()) {
73153
72731
  const cap = maxInlineExportBytes();
73154
72732
  if (bytes.length > cap) {
@@ -73159,7 +72737,7 @@ function simulateStrike(input, engine) {
73159
72737
  payload.wav = {
73160
72738
  filename: a.wav_filename,
73161
72739
  bytes: bytes.length,
73162
- data_base64: Buffer.from(bytes).toString("base64")
72740
+ data_base64: result.wav_base64
73163
72741
  };
73164
72742
  } else {
73165
72743
  const path = resolveWithinRoot(
@@ -78325,8 +77903,8 @@ var init_server3 = __esm({
78325
77903
  init_order_feed();
78326
77904
  init_animate();
78327
77905
  PKG_VERSION = (() => {
78328
- if ("0.9.4-main.11") {
78329
- return "0.9.4-main.11";
77906
+ if ("0.9.4-main.13") {
77907
+ return "0.9.4-main.13";
78330
77908
  }
78331
77909
  try {
78332
77910
  const req = createRequire2(import.meta.url);
@@ -78335,8 +77913,8 @@ var init_server3 = __esm({
78335
77913
  return "0.0.0";
78336
77914
  }
78337
77915
  })();
78338
- BUILD_SHA = "681857ca2f62f173237de79ff2621cd1609480a7";
78339
- BUILD_TIME = "2026-07-24T16:25:54.297Z";
77916
+ BUILD_SHA = "70552964e3760fb5b103235917b00364f6d45763";
77917
+ BUILD_TIME = "2026-07-24T16:29:07.170Z";
78340
77918
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
78341
77919
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
78342
77920
  INSTANCE_ID = randomUUID6().slice(0, 8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.11",
3
+ "version": "0.9.4-main.13",
4
4
  "description": "vcad MCP server — parametric CAD + PCB design tools for AI agents (self-contained: bundled server + kernel WASM)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "homepage": "https://vcad.io",
21
21
  "vcadBuild": {
22
- "sha": "681857ca2f62f173237de79ff2621cd1609480a7",
23
- "builtAt": "2026-07-24T16:25:54.297Z"
22
+ "sha": "70552964e3760fb5b103235917b00364f6d45763",
23
+ "builtAt": "2026-07-24T16:29:07.170Z"
24
24
  }
25
25
  }