@vcad/mcp 0.9.4-main.18 → 0.9.4-main.20

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
@@ -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 48276771946e67b08cb31a66d123bdc2d7af926d at 2026-07-24T22:30:13.865Z. Source: https://github.com/ecto/vcad
9
+ Built from c5938bcd3843b4e600b28e18b5472d41337bd478 at 2026-07-25T14:49:15.914Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -125,6 +125,7 @@ __export(vcad_kernel_wasm_exports, {
125
125
  ecadDfmDefaultPack: () => ecadDfmDefaultPack,
126
126
  ecadEvaluateMotor: () => ecadEvaluateMotor,
127
127
  ecadExportFab: () => ecadExportFab,
128
+ ecadFabPrep: () => ecadFabPrep,
128
129
  ecadFillZones: () => ecadFillZones,
129
130
  ecadFindAlternatives: () => ecadFindAlternatives,
130
131
  ecadFootprintForName: () => ecadFootprintForName,
@@ -245,6 +246,7 @@ __export(vcad_kernel_wasm_exports, {
245
246
  sectionMesh: () => sectionMesh,
246
247
  sheetMetalFoldedStep: () => sheetMetalFoldedStep,
247
248
  sheetMetalSequence: () => sheetMetalSequence,
249
+ simulateFlow: () => simulateFlow,
248
250
  simulateStrikeKernel: () => simulateStrikeKernel,
249
251
  sketchCircleSegments: () => sketchCircleSegments,
250
252
  sketchHitTest: () => sketchHitTest,
@@ -1266,6 +1268,17 @@ function ecadExportFab(pcb_json) {
1266
1268
  }
1267
1269
  return takeFromExternrefTable0(ret[0]);
1268
1270
  }
1271
+ function ecadFabPrep(pcb_json, options_json) {
1272
+ const ptr0 = passStringToWasm0(pcb_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1273
+ const len0 = WASM_VECTOR_LEN;
1274
+ var ptr1 = isLikeNone(options_json) ? 0 : passStringToWasm0(options_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1275
+ var len1 = WASM_VECTOR_LEN;
1276
+ const ret = wasm.ecadFabPrep(ptr0, len0, ptr1, len1);
1277
+ if (ret[2]) {
1278
+ throw takeFromExternrefTable0(ret[1]);
1279
+ }
1280
+ return takeFromExternrefTable0(ret[0]);
1281
+ }
1269
1282
  function ecadFillZones(pcb_json) {
1270
1283
  const ptr0 = passStringToWasm0(pcb_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1271
1284
  const len0 = WASM_VECTOR_LEN;
@@ -2797,6 +2810,17 @@ function sheetMetalSequence(chain_json) {
2797
2810
  wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
2798
2811
  }
2799
2812
  }
2813
+ function simulateFlow(spec_json, options_json, include_fields) {
2814
+ const ptr0 = passStringToWasm0(spec_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
2815
+ const len0 = WASM_VECTOR_LEN;
2816
+ const ptr1 = passStringToWasm0(options_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
2817
+ const len1 = WASM_VECTOR_LEN;
2818
+ const ret = wasm.simulateFlow(ptr0, len0, ptr1, len1, include_fields);
2819
+ if (ret[2]) {
2820
+ throw takeFromExternrefTable0(ret[1]);
2821
+ }
2822
+ return takeFromExternrefTable0(ret[0]);
2823
+ }
2800
2824
  function simulateStrikeKernel(input_json) {
2801
2825
  let deferred3_0;
2802
2826
  let deferred3_1;
@@ -10249,6 +10273,25 @@ async function tryRunDrc(pcb) {
10249
10273
  return { ok: false, reason: "error", message: e instanceof Error ? e.message : String(e) };
10250
10274
  }
10251
10275
  }
10276
+ async function runFabPrep(pcb, options) {
10277
+ const wasm3 = await loadEcadWasm();
10278
+ if (!wasm3) {
10279
+ return { ok: false, reason: "unavailable", message: "ECAD kernel WASM not loaded" };
10280
+ }
10281
+ if (typeof wasm3.ecadFabPrep !== "function") {
10282
+ return {
10283
+ ok: false,
10284
+ reason: "unavailable",
10285
+ message: "kernel WASM predates ecadFabPrep"
10286
+ };
10287
+ }
10288
+ try {
10289
+ const out = wasm3.ecadFabPrep(JSON.stringify(pcb), options ? JSON.stringify(options) : void 0);
10290
+ return { ok: true, value: out };
10291
+ } catch (e) {
10292
+ return { ok: false, reason: "error", message: e instanceof Error ? e.message : String(e) };
10293
+ }
10294
+ }
10252
10295
  async function runPcbDfm(pcb, profile, rulePackToml) {
10253
10296
  const wasm3 = await loadEcadWasm();
10254
10297
  if (!wasm3 || typeof wasm3.ecadDfmCheck !== "function")
@@ -10375,6 +10418,7 @@ async function routeAll(pcb, width, netsFilter = [], effort = 1) {
10375
10418
  const empty = {
10376
10419
  traces: [],
10377
10420
  vias: [],
10421
+ zones: [],
10378
10422
  routed_nets: [],
10379
10423
  unrouted_nets: [],
10380
10424
  diagnostics: [],
@@ -11275,6 +11319,7 @@ __export(dist_exports, {
11275
11319
  runDrc: () => runDrc,
11276
11320
  runDrcInRegion: () => runDrcInRegion,
11277
11321
  runErc: () => runErc,
11322
+ runFabPrep: () => runFabPrep,
11278
11323
  runPcbDfm: () => runPcbDfm,
11279
11324
  sampleSequence: () => sampleSequence,
11280
11325
  sampleTrackValue: () => sampleTrackValue,
@@ -40816,6 +40861,109 @@ var init_CHANGELOG = __esm({
40816
40861
  CHANGELOG_default = {
40817
40862
  $schema: "./changelog.schema.json",
40818
40863
  entries: [
40864
+ {
40865
+ id: "2026-07-25-router-window-repair-commit-parity",
40866
+ version: "0.9.4",
40867
+ date: "2026-07-25",
40868
+ category: "fix",
40869
+ title: "Autoroute keeps window-repaired connections it used to drop",
40870
+ summary: "The last-resort window repair now searches at the widest net-class width, merges stacked via barrels, and probes real drills, so routings it finds survive the commit.",
40871
+ features: [
40872
+ "pcb",
40873
+ "routing",
40874
+ "drc"
40875
+ ],
40876
+ mcpTools: [
40877
+ "route_nets",
40878
+ "run_drc"
40879
+ ]
40880
+ },
40881
+ {
40882
+ id: "2026-07-25-router-diff-pair-si",
40883
+ version: "0.9.4",
40884
+ date: "2026-07-25",
40885
+ category: "fix",
40886
+ title: "Differential pairs route coupled and length-matched",
40887
+ summary: "Coupled pairs can now escape a BGA on an inner layer instead of failing outright, USB DP/DM pairs are recognized, and a finishing pass re-couples, descends and phase-compensates each pair so intra-pair skew lands inside the signal-integrity receipt's bounds.",
40888
+ features: [
40889
+ "pcb",
40890
+ "autorouter",
40891
+ "diff-pairs",
40892
+ "signal-integrity",
40893
+ "drc"
40894
+ ],
40895
+ mcpTools: [
40896
+ "route_nets",
40897
+ "route_diff_pair",
40898
+ "run_drc",
40899
+ "build_receipt"
40900
+ ]
40901
+ },
40902
+ {
40903
+ id: "2026-07-24-visionos-app",
40904
+ version: "0.9.4",
40905
+ date: "2026-07-24",
40906
+ category: "feat",
40907
+ title: "vcad for Apple Vision Pro",
40908
+ summary: "The full native editor ported to visionOS: documents float in a volume with the feature tree, inspector, tool palette, and AI composer around them.",
40909
+ features: [
40910
+ "native",
40911
+ "visionos",
40912
+ "assembly",
40913
+ "patterns"
40914
+ ]
40915
+ },
40916
+ {
40917
+ id: "2026-07-24-router-via-hole-to-hole",
40918
+ version: "0.9.4",
40919
+ date: "2026-07-24",
40920
+ category: "fix",
40921
+ title: "Autorouter enforces via hole-to-hole spacing",
40922
+ summary: "route_nets' legality oracle now indexes drilled holes independently of copper, so two vias whose layer spans don't overlap can no longer be placed with colliding drills \u2014 the hole-to-hole rule is enforced at probe time, not found by a later DRC.",
40923
+ features: [
40924
+ "pcb",
40925
+ "autorouter",
40926
+ "drc"
40927
+ ],
40928
+ mcpTools: [
40929
+ "route_nets",
40930
+ "run_drc",
40931
+ "validate_for_fab"
40932
+ ]
40933
+ },
40934
+ {
40935
+ id: "2026-07-24-router-prune-dangling-copper",
40936
+ version: "0.9.4",
40937
+ date: "2026-07-24",
40938
+ category: "fix",
40939
+ title: "Autorouter no longer leaves dead copper behind",
40940
+ summary: "route_nets now prunes its own dangling copper before returning \u2014 traces and vias whose island reaches no pad and no pour, left over from rip-up \u2014 cutting the CM5 40-net route from 1292 to 789 segments with routability unchanged.",
40941
+ features: [
40942
+ "pcb",
40943
+ "autorouter",
40944
+ "drc"
40945
+ ],
40946
+ mcpTools: [
40947
+ "route_nets",
40948
+ "run_drc"
40949
+ ]
40950
+ },
40951
+ {
40952
+ id: "2026-07-24-router-negotiation-first-ordering",
40953
+ version: "0.9.4",
40954
+ date: "2026-07-24",
40955
+ category: "perf",
40956
+ title: "Autorouter routes large boards in minutes, not hours",
40957
+ summary: "On layer-rich boards the GPU negotiator now takes the unrouted tail before the sequential CPU rescue arsenal, which fires once over its leftovers; the rip-up rounds it replaced are skipped and speculative rounds are budgeted against the baseline.",
40958
+ features: [
40959
+ "pcb",
40960
+ "autorouter",
40961
+ "gpu"
40962
+ ],
40963
+ mcpTools: [
40964
+ "route_nets"
40965
+ ]
40966
+ },
40819
40967
  {
40820
40968
  id: "2026-07-24-router-keepout-edge-clearance",
40821
40969
  version: "0.9.4",
@@ -40834,6 +40982,44 @@ var init_CHANGELOG = __esm({
40834
40982
  "validate_for_fab"
40835
40983
  ]
40836
40984
  },
40985
+ {
40986
+ id: "2026-07-24-router-drill-aware-vias-pad-layer-stubs",
40987
+ version: "0.9.4",
40988
+ date: "2026-07-24",
40989
+ category: "fix",
40990
+ title: "Router vias clear drills; stubs leave pads on the pad's layer",
40991
+ summary: "The routing oracle now checks hole-to-hole spacing across layer spans (two vias on disjoint spans no longer collide in the drill file), probes a via barrel on every layer it spans, and can pin a connection's terminals so stubs leave a pad on a layer the pad is actually on instead of floating on an inner layer.",
40992
+ features: [
40993
+ "pcb",
40994
+ "autorouter",
40995
+ "drc"
40996
+ ],
40997
+ mcpTools: [
40998
+ "route_nets",
40999
+ "run_drc",
41000
+ "validate_for_fab"
41001
+ ]
41002
+ },
41003
+ {
41004
+ id: "2026-07-24-router-copper-pour-synthesis",
41005
+ version: "0.9.4",
41006
+ date: "2026-07-24",
41007
+ category: "feat",
41008
+ title: "Autorouter pours planes for high-current nets",
41009
+ summary: "route_nets now synthesizes copper pours for power nets instead of routing them as thin traces: it sizes current with the same IPC-2221 model as size_trace_for_current, floods a layer, and stitches every pad to the plane.",
41010
+ features: [
41011
+ "pcb",
41012
+ "autorouter",
41013
+ "copper-pour",
41014
+ "power-integrity"
41015
+ ],
41016
+ mcpTools: [
41017
+ "route_nets",
41018
+ "size_trace_for_current",
41019
+ "run_drc",
41020
+ "validate_for_fab"
41021
+ ]
41022
+ },
40837
41023
  {
40838
41024
  id: "2026-07-24-lattice-gauge-mcp",
40839
41025
  version: "0.9.4",
@@ -40865,6 +41051,26 @@ var init_CHANGELOG = __esm({
40865
41051
  "simulate_flow"
40866
41052
  ]
40867
41053
  },
41054
+ {
41055
+ id: "2026-07-24-fab-prep-drc-delta-receipt",
41056
+ version: "0.9.4",
41057
+ date: "2026-07-24",
41058
+ category: "feat",
41059
+ title: "One-command fab prep with a DRC-delta receipt",
41060
+ summary: "vcad fab-prep (and the fab_prep MCP tool) takes a routed board to a complete Gerber package in one call: opt-in logged rule calibration, verdict ladder, strip-and-re-route fix loop, dangling-copper prune, and a receipt that separates route-attributable violations from the board's own baseline.",
41061
+ features: [
41062
+ "pcb",
41063
+ "drc",
41064
+ "autorouter",
41065
+ "fabrication",
41066
+ "cli"
41067
+ ],
41068
+ mcpTools: [
41069
+ "fab_prep",
41070
+ "validate_for_fab",
41071
+ "export_gerber"
41072
+ ]
41073
+ },
40868
41074
  {
40869
41075
  id: "2026-07-23-router-post-route-legalization",
40870
41076
  version: "0.9.4",
@@ -51968,6 +52174,7 @@ var init_tool_metadata = __esm({
51968
52174
  annotations: RW,
51969
52175
  outputSchema: objectOut({ export_kicad: { type: "object" } })
51970
52176
  },
52177
+ fab_prep: { title: "Fab Prep", annotations: RW },
51971
52178
  validate_for_fab: { title: "Validate for Fab", annotations: RO },
51972
52179
  calc_impedance: { title: "Calc Impedance", annotations: RO },
51973
52180
  size_impedance: { title: "Size Impedance", annotations: RO },
@@ -58128,6 +58335,7 @@ async function routeNets(args) {
58128
58335
  }
58129
58336
  let tracesRemoved = 0;
58130
58337
  let viasRemoved = 0;
58338
+ let zonesAdded = 0;
58131
58339
  if (targetNets.size > 0) {
58132
58340
  const beforeT = pcb.traces.length;
58133
58341
  const beforeV = pcb.vias.length;
@@ -58171,6 +58379,10 @@ async function routeNets(args) {
58171
58379
  return 0;
58172
58380
  };
58173
58381
  const applyRoute = (result) => {
58382
+ for (const z of result.zones ?? []) {
58383
+ pcb.zones.push(structuredClone(z));
58384
+ zonesAdded++;
58385
+ }
58174
58386
  for (const t2 of result.traces) {
58175
58387
  pcb.traces.push({
58176
58388
  start: { x: t2.start.x, y: t2.start.y },
@@ -58379,6 +58591,9 @@ async function routeNets(args) {
58379
58591
  nets_routed: routedNets.size,
58380
58592
  routability,
58381
58593
  traces_added: tracesAdded,
58594
+ // Copper pours the router synthesized for high-current nets: those
58595
+ // nets are now carried by a plane and stitched to it, not traced.
58596
+ ...zonesAdded > 0 ? { zones_added: zonesAdded } : {},
58382
58597
  // Copper hygiene: re-routing rips the prior route up first, so a
58383
58598
  // re-route reports both what it removed and what it laid — `added`
58384
58599
  // alone reads like monotonic growth even when copper is being
@@ -59534,6 +59749,85 @@ function detectUnsupportedFeatures(pcb) {
59534
59749
  }
59535
59750
  return out;
59536
59751
  }
59752
+ async function fabPrep(args) {
59753
+ const ctx = resolveDocInput(args);
59754
+ const pcb = getDocPcb2(ctx.doc);
59755
+ if (!pcb) {
59756
+ return ecadError(
59757
+ "Document has no PCB \u2014 run place_components first (or open a document that has a board)"
59758
+ );
59759
+ }
59760
+ if (!await isEcadAvailable2()) {
59761
+ return ecadError(
59762
+ "fab_prep requires the kernel DRC/routing engine (ECAD WASM unavailable) \u2014 refusing to report an unverifiable fab-readiness verdict"
59763
+ );
59764
+ }
59765
+ const dryRun = Boolean(args.dry_run);
59766
+ const options = {
59767
+ calibrate_rules: Boolean(args.calibrate_rules),
59768
+ route_remaining: args.route_remaining === void 0 ? true : Boolean(args.route_remaining),
59769
+ prune_dangling: args.prune_dangling === void 0 ? true : Boolean(args.prune_dangling),
59770
+ max_rounds: Math.max(0, Math.round(Number(args.max_rounds ?? 8))),
59771
+ accept_rules: Array.isArray(args.accept_rules) ? args.accept_rules : [],
59772
+ verdict: {
59773
+ budget: Math.max(1, Math.round(Number(args.budget ?? 5e6))),
59774
+ max_cluster: Math.max(1, Math.round(Number(args.max_cluster ?? 6)))
59775
+ }
59776
+ };
59777
+ const out = await runFabPrep(pcb, options);
59778
+ if (!out.ok) {
59779
+ return ecadError(`fab_prep could not run: ${out.message}`);
59780
+ }
59781
+ const { report, pcb: fixed } = out.value;
59782
+ if (!dryRun) {
59783
+ pcb.traces = fixed.traces;
59784
+ pcb.traceArcs = fixed.traceArcs;
59785
+ pcb.vias = fixed.vias;
59786
+ pcb.rules = fixed.rules;
59787
+ }
59788
+ const payload = {
59789
+ success: true,
59790
+ ...dryRun ? { dry_run: true } : {},
59791
+ converged: report.converged,
59792
+ ...report.blocker ? { blocker: report.blocker } : {},
59793
+ headline: fabPrepHeadline(report),
59794
+ // Both numbers, always. A caller that sees only one of them is being told
59795
+ // something other than what this run established.
59796
+ drc_delta: {
59797
+ baseline_total: report.delta.baseline_total,
59798
+ final_total: report.delta.final_total,
59799
+ route_attributable_total: report.delta.route_attributable_total,
59800
+ route_attributable_blocking: report.delta.route_attributable_fixable,
59801
+ route_attributable_accepted: report.delta.route_attributable_accepted,
59802
+ by_rule: report.delta.rules,
59803
+ baseline_note: "baseline = this same board with every trace and via stripped, checked under the same rules. It is not zero on an imported fixture and is not supposed to be; the router is answerable for the difference, not the total."
59804
+ },
59805
+ connectivity: report.connectivity,
59806
+ ...report.accepted_rules.length > 0 ? { accepted_rules: report.accepted_rules } : {},
59807
+ calibration: {
59808
+ requested: report.calibration_requested,
59809
+ applied: report.calibration.applied,
59810
+ refused: report.calibration.refused
59811
+ },
59812
+ initial_verdict: report.initial_verdict,
59813
+ rounds: report.rounds,
59814
+ pruned: { traces: report.pruned_traces, vias: report.pruned_vias },
59815
+ // Cap the offender list: a non-converging run on a dense board can name
59816
+ // thousands, and the full list lives in the board's own DRC.
59817
+ offenders: report.delta.offenders.slice(0, 25),
59818
+ offender_count: report.delta.offenders.length,
59819
+ next_action: report.converged ? "export_gerber (the clean-DRC gate will now pass)" : "resolve the offenders above (fix_drc / route_nets / set_placement), then re-run fab_prep",
59820
+ ...docResultPayload(ctx)
59821
+ };
59822
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
59823
+ }
59824
+ function fabPrepHeadline(report) {
59825
+ if (!report.converged) {
59826
+ return `NOT FAB-READY \u2014 ${report.delta.route_attributable_fixable} route-attributable violation(s) remain (${report.blocker ?? "loop did not converge"})`;
59827
+ }
59828
+ const waived = report.delta.route_attributable_accepted > 0 ? `, ${report.delta.route_attributable_accepted} waived under ${report.accepted_rules.join("+")}` : "";
59829
+ return `zero route-attributable violations${waived} \u2014 ${report.delta.final_total} on the finished board, ${report.delta.baseline_total} on the same board stripped of all routing`;
59830
+ }
59537
59831
  async function validateForFab(args) {
59538
59832
  const ctx = resolveDocInput(args);
59539
59833
  const pcb = getDocPcb2(ctx.doc);
@@ -64784,7 +65078,7 @@ async function fixDrc(args) {
64784
65078
  };
64785
65079
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
64786
65080
  }
64787
- var PCB_LAYER_SET, COPPER_LAYERS, COPPER_LAYER_SET, POSITION_TOLERANCE, PIN_SEP, pinKey, createSchematicSchema, docInputProperties, placeComponentsSchema, routeNetsSchema, runDrcSchema, runErcSchema, critiqueRouteSchema, routeDiffPairSchema, lengthMatchTracesSchema, exportGerberSchema, validateForFabSchema, exportKicadSchema, addCoilSchema, boardFromSolidSchema, addCoilArraySchema, windingLayoutSchema, calcImpedanceSchema, sizeImpedanceSchema, sizePdnSchema, calcCoilSchema, sizeCoilSchema, calcRfSchema, COIL_GEOMETRY, MU0, RAIL_PIN_NAMES, PIN_TYPES, CHIP_SIZE_CODES, CONNECTIVITY_GUARD_MAX_NETS, POWER_NET_RE, DRC_CATEGORY, DRC_DELTA_SAMPLE_CAP, DRC_DELTA_FULL_BOARD_MAX, LINT_CRYSTAL_MAX_MM, LINT_DECAP_MAX_MM, LINT_CONNECTOR_EDGE_MAX_MM, LINT_HIGH_CURRENT_SEP_MIN_MM, LINT_GROUND_NET_RE, LINT_SUPPLY_NET_RE, SENSITIVE_NET_RE, HIGH_CURRENT_CLASS_RE, refPrefix, dist2d, round2, sumSq, round33, vec2Schema, setBoardOutlineSchema, FOOTPRINT_FAMILIES, listFootprintsSchema, searchFootprintsSchema, getPadPositionsSchema, getFootprintSchema, ROTATION_CONVENTION, describePcbSchema, DESCRIBE_NET_NAME_CAP, addTraceSchema, addViaSchema, OZ_TO_MM, setStackupSchema, setPlacementSchema, addZoneSchema, deleteElementProps, deleteZoneSchema, deleteTraceSchema, deleteViaSchema, COPPER_KINDS, GET_COPPER_CAP, getCopperSchema, TIE_POSITION_TOL, addNetTieSchema, deleteNetTieSchema, undoSchema, setDesignRulesSchema, sizeTraceForCurrentSchema, addViaArraySchema, MAX_VIA_ARRAY, addMotorWindingSchema, calcMotorSchema, sig6, BEARING_FRICTION_MNM, checkSelfStartSchema, solidFromBoardSchema, FR4_DENSITY_KG_M3, COPPER_DENSITY_KG_M3, DEFAULT_COPPER_THICKNESS_MM, loopArea, MOUNTING_FP_RE, KEEPOUT_ECHO_CAP, INLINE_DOC_ECHO_CAP, searchElectronicPartsSchema, resolvePartSchema, findAlternativesSchema, verifySubstitutionSchema, buildReceiptSchema, verifyReceiptSchema, FIX_EPS, FIX_DRC_NEVER, fixDrcSchema, toolDefs20;
65081
+ var PCB_LAYER_SET, COPPER_LAYERS, COPPER_LAYER_SET, POSITION_TOLERANCE, PIN_SEP, pinKey, createSchematicSchema, docInputProperties, placeComponentsSchema, routeNetsSchema, runDrcSchema, runErcSchema, critiqueRouteSchema, routeDiffPairSchema, lengthMatchTracesSchema, exportGerberSchema, validateForFabSchema, fabPrepSchema, exportKicadSchema, addCoilSchema, boardFromSolidSchema, addCoilArraySchema, windingLayoutSchema, calcImpedanceSchema, sizeImpedanceSchema, sizePdnSchema, calcCoilSchema, sizeCoilSchema, calcRfSchema, COIL_GEOMETRY, MU0, RAIL_PIN_NAMES, PIN_TYPES, CHIP_SIZE_CODES, CONNECTIVITY_GUARD_MAX_NETS, POWER_NET_RE, DRC_CATEGORY, DRC_DELTA_SAMPLE_CAP, DRC_DELTA_FULL_BOARD_MAX, LINT_CRYSTAL_MAX_MM, LINT_DECAP_MAX_MM, LINT_CONNECTOR_EDGE_MAX_MM, LINT_HIGH_CURRENT_SEP_MIN_MM, LINT_GROUND_NET_RE, LINT_SUPPLY_NET_RE, SENSITIVE_NET_RE, HIGH_CURRENT_CLASS_RE, refPrefix, dist2d, round2, sumSq, round33, vec2Schema, setBoardOutlineSchema, FOOTPRINT_FAMILIES, listFootprintsSchema, searchFootprintsSchema, getPadPositionsSchema, getFootprintSchema, ROTATION_CONVENTION, describePcbSchema, DESCRIBE_NET_NAME_CAP, addTraceSchema, addViaSchema, OZ_TO_MM, setStackupSchema, setPlacementSchema, addZoneSchema, deleteElementProps, deleteZoneSchema, deleteTraceSchema, deleteViaSchema, COPPER_KINDS, GET_COPPER_CAP, getCopperSchema, TIE_POSITION_TOL, addNetTieSchema, deleteNetTieSchema, undoSchema, setDesignRulesSchema, sizeTraceForCurrentSchema, addViaArraySchema, MAX_VIA_ARRAY, addMotorWindingSchema, calcMotorSchema, sig6, BEARING_FRICTION_MNM, checkSelfStartSchema, solidFromBoardSchema, FR4_DENSITY_KG_M3, COPPER_DENSITY_KG_M3, DEFAULT_COPPER_THICKNESS_MM, loopArea, MOUNTING_FP_RE, KEEPOUT_ECHO_CAP, INLINE_DOC_ECHO_CAP, searchElectronicPartsSchema, resolvePartSchema, findAlternativesSchema, verifySubstitutionSchema, buildReceiptSchema, verifyReceiptSchema, FIX_EPS, FIX_DRC_NEVER, fixDrcSchema, toolDefs20;
64788
65082
  var init_ecad2 = __esm({
64789
65083
  "src/tools/ecad.ts"() {
64790
65084
  "use strict";
@@ -65150,6 +65444,46 @@ var init_ecad2 = __esm({
65150
65444
  },
65151
65445
  required: []
65152
65446
  };
65447
+ fabPrepSchema = {
65448
+ type: "object",
65449
+ properties: {
65450
+ ...docInputProperties,
65451
+ calibrate_rules: {
65452
+ type: "boolean",
65453
+ description: "Derive and apply design-rule calibration from the board's OWN declared via classes and pre-existing footprint holes (default FALSE). Imported boards routinely carry global minima that forbid the via class they themselves declare \u2014 e.g. a 0.21/0.12mm class under a 0.2mm minDrill, which flags every via on the board. Calibration only ever relaxes a rule to the point where the board's own GIVEN geometry stops being illegal, is floored at laser-microvia limits, and records every change with its derivation in the receipt. Off by default because silently relaxing DRC rules to make a board pass is how an unbuildable board ships."
65454
+ },
65455
+ route_remaining: {
65456
+ type: "boolean",
65457
+ description: "Route or certify the connections the board arrived without, before the fix loop (default TRUE). Each unrouted connection ends as Routed, ProvedInfeasible (with a bottleneck-cut certificate), or an honest unknown \u2014 never silently dropped."
65458
+ },
65459
+ max_rounds: {
65460
+ type: "number",
65461
+ description: "Maximum strip-and-re-route rounds (default 8). Each round censuses the violations the ROUTING is answerable for, strips those nets, and hands them back to the session-probed router."
65462
+ },
65463
+ budget: {
65464
+ type: "number",
65465
+ description: "Per-cluster search budget in node expansions for the complete router (default 5,000,000). Lower is faster and yields more honest unknowns."
65466
+ },
65467
+ max_cluster: {
65468
+ type: "number",
65469
+ description: "Maximum connections coalesced into one joint search window (default 6)."
65470
+ },
65471
+ prune_dangling: {
65472
+ type: "boolean",
65473
+ description: "Remove copper that reaches no pad or pour of its net before the final DRC (default TRUE)."
65474
+ },
65475
+ accept_rules: {
65476
+ type: "array",
65477
+ items: { type: "string" },
65478
+ description: 'DRC rule names whose route-attributable violations are ACCEPTED rather than fixed (e.g. ["MinTraceWidth"]). Real fab packages ship with real, named exceptions; the difference between that and a footgun is whether the exception is written down. Waived violations are still counted, still listed, and the waiver is named in the receipt \u2014 it stops blocking the verdict, it does not hide anything. An unrecognised rule name refuses the run rather than silently accepting nothing.'
65479
+ },
65480
+ dry_run: {
65481
+ type: "boolean",
65482
+ description: "Compute the receipt without writing the fixed board back to the session."
65483
+ }
65484
+ },
65485
+ required: []
65486
+ };
65153
65487
  exportKicadSchema = {
65154
65488
  type: "object",
65155
65489
  properties: {
@@ -66766,6 +67100,14 @@ var init_ecad2 = __esm({
66766
67100
  handler: (a) => exportKicad(a),
66767
67101
  behavior: behavior({})
66768
67102
  },
67103
+ {
67104
+ name: "fab_prep",
67105
+ pack: "ecad",
67106
+ description: "Take a routed board to fab-ready in one call, and return the DRC-delta receipt that says what was actually achieved. Runs the whole pipeline: optional rule calibration (opt-in, every change logged with its derivation), the verdict ladder over unrouted connections (each ends Routed / ProvedInfeasible / honest-unknown), then a strip-and-re-route fix loop until the violations the ROUTING is answerable for reach zero, then a dangling-copper prune. Mutates the session document. THE RECEIPT IS THE POINT: on an imported fixture absolute zero is not achievable \u2014 the same board stripped of all routing already violates its own rules \u2014 so the report always gives BOTH numbers (stripped-board baseline and finished board) and charges only the difference to the router. Fails closed: a loop that does not converge returns `converged:false` with the remaining offenders and does not pretend otherwise, and `export_gerber`'s clean-DRC gate still stands \u2014 this is the supported way to GET clean, not a way around it.",
67107
+ inputSchema: fabPrepSchema,
67108
+ handler: (a) => fabPrep(a),
67109
+ behavior: behavior({ writesDoc: true, geometry: true, mount: true })
67110
+ },
66769
67111
  {
66770
67112
  name: "validate_for_fab",
66771
67113
  pack: "ecad",
@@ -77719,8 +78061,8 @@ var init_server3 = __esm({
77719
78061
  init_order_feed();
77720
78062
  init_animate();
77721
78063
  PKG_VERSION = (() => {
77722
- if ("0.9.4-main.18") {
77723
- return "0.9.4-main.18";
78064
+ if ("0.9.4-main.20") {
78065
+ return "0.9.4-main.20";
77724
78066
  }
77725
78067
  try {
77726
78068
  const req = createRequire2(import.meta.url);
@@ -77729,8 +78071,8 @@ var init_server3 = __esm({
77729
78071
  return "0.0.0";
77730
78072
  }
77731
78073
  })();
77732
- BUILD_SHA = "48276771946e67b08cb31a66d123bdc2d7af926d";
77733
- BUILD_TIME = "2026-07-24T22:30:13.865Z";
78074
+ BUILD_SHA = "c5938bcd3843b4e600b28e18b5472d41337bd478";
78075
+ BUILD_TIME = "2026-07-25T14:49:15.914Z";
77734
78076
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
77735
78077
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
77736
78078
  INSTANCE_ID = randomUUID6().slice(0, 8);
@@ -77990,6 +78332,7 @@ var init_server3 = __esm({
77990
78332
  "run_erc",
77991
78333
  "export_gerber",
77992
78334
  "export_kicad",
78335
+ "fab_prep",
77993
78336
  "validate_for_fab",
77994
78337
  "calc_impedance",
77995
78338
  "size_impedance",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.18",
3
+ "version": "0.9.4-main.20",
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": "48276771946e67b08cb31a66d123bdc2d7af926d",
23
- "builtAt": "2026-07-24T22:30:13.865Z"
22
+ "sha": "c5938bcd3843b4e600b28e18b5472d41337bd478",
23
+ "builtAt": "2026-07-25T14:49:15.914Z"
24
24
  }
25
25
  }
Binary file