@vcad/mcp 0.9.4-main.71 → 0.9.4-main.73

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 db132e1acce8719160880940ad8f1445aaf18354 at 2026-08-16T20:09:58.430Z. Source: https://github.com/ecto/vcad
9
+ Built from c94448044a725e430c38faf507027d5a10b2c68d at 2026-08-17T23:42:23.328Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -10874,6 +10874,46 @@ async function parseKicadPcb2(content) {
10874
10874
  return null;
10875
10875
  }
10876
10876
  }
10877
+ function importBindings(wasm3) {
10878
+ const mod = wasm3;
10879
+ const bind = (name) => {
10880
+ const fn = mod[name];
10881
+ if (typeof fn !== "function") {
10882
+ throw new Error(`Altium import is unavailable: this kernel WASM build predates ${name}. Rebuild the kernel WASM (npm run build -w vcad-kernel-wasm) or update @vcad/mcp.`);
10883
+ }
10884
+ return fn;
10885
+ };
10886
+ return {
10887
+ parseAltiumAsciiPcb: (content) => bind("parseAltiumAsciiPcb")(content),
10888
+ parseAltiumPcbDoc: (bytes) => bind("parseAltiumPcbDoc")(bytes),
10889
+ parseAltiumPcbLib: (bytes) => bind("parseAltiumPcbLib")(bytes),
10890
+ parseEagleBrd: (content) => bind("parseEagleBrd")(content)
10891
+ };
10892
+ }
10893
+ async function parseEagleBrd(content) {
10894
+ const wasm3 = await loadEcadWasm();
10895
+ if (!wasm3)
10896
+ return null;
10897
+ return importBindings(wasm3).parseEagleBrd(content);
10898
+ }
10899
+ async function parseAltiumAsciiPcb(content) {
10900
+ const wasm3 = await loadEcadWasm();
10901
+ if (!wasm3)
10902
+ return null;
10903
+ return importBindings(wasm3).parseAltiumAsciiPcb(content);
10904
+ }
10905
+ async function parseAltiumPcbDoc(bytes) {
10906
+ const wasm3 = await loadEcadWasm();
10907
+ if (!wasm3)
10908
+ return null;
10909
+ return importBindings(wasm3).parseAltiumPcbDoc(bytes);
10910
+ }
10911
+ async function parseAltiumPcbLib(bytes) {
10912
+ const wasm3 = await loadEcadWasm();
10913
+ if (!wasm3)
10914
+ return null;
10915
+ return importBindings(wasm3).parseAltiumPcbLib(bytes);
10916
+ }
10877
10917
  async function exportKicadPcb2(pcb) {
10878
10918
  const wasm3 = await loadEcadWasm();
10879
10919
  if (!wasm3)
@@ -11719,7 +11759,11 @@ __export(dist_exports, {
11719
11759
  mountingHolesFromPcb: () => mountingHolesFromPcb,
11720
11760
  netContinuity: () => netContinuity,
11721
11761
  outlineAabb: () => outlineAabb,
11762
+ parseAltiumAsciiPcb: () => parseAltiumAsciiPcb,
11763
+ parseAltiumPcbDoc: () => parseAltiumPcbDoc,
11764
+ parseAltiumPcbLib: () => parseAltiumPcbLib,
11722
11765
  parseBindingKey: () => parseBindingKey,
11766
+ parseEagleBrd: () => parseEagleBrd,
11723
11767
  parseExpression: () => parse,
11724
11768
  parseKicadPcb: () => parseKicadPcb2,
11725
11769
  parseXyz: () => parseXyz,
@@ -41547,6 +41591,26 @@ var init_CHANGELOG = __esm({
41547
41591
  CHANGELOG_default = {
41548
41592
  $schema: "./changelog.schema.json",
41549
41593
  entries: [
41594
+ {
41595
+ id: "2026-08-17-altium-pcb-import",
41596
+ version: "0.9.4",
41597
+ date: "2026-08-17",
41598
+ category: "feat",
41599
+ title: "Altium and Eagle PCB import",
41600
+ summary: "Import Altium .PcbDoc boards (native binary or ASCII) and .PcbLib libraries, plus Eagle .brd boards, straight into a live session.",
41601
+ features: [
41602
+ "pcb",
41603
+ "import",
41604
+ "ecad",
41605
+ "altium",
41606
+ "eagle"
41607
+ ],
41608
+ mcpTools: [
41609
+ "import_altium",
41610
+ "import_altium_library",
41611
+ "import_eagle"
41612
+ ]
41613
+ },
41550
41614
  {
41551
41615
  id: "2026-08-16-render-z-up",
41552
41616
  version: "0.9.4",
@@ -41559,6 +41623,81 @@ var init_CHANGELOG = __esm({
41559
41623
  "rendering"
41560
41624
  ]
41561
41625
  },
41626
+ {
41627
+ id: "2026-08-16-render-hidden-lines",
41628
+ version: "0.9.4",
41629
+ date: "2026-08-16",
41630
+ category: "fix",
41631
+ title: "SVG renders draw complete, actually-dashed hidden lines",
41632
+ summary: "Hidden edges on curved features were mostly deleted and the survivors drew solid, leaving a stray line across the part; they now run the full occluded length as proper dashes.",
41633
+ features: [
41634
+ "rendering",
41635
+ "drafting"
41636
+ ]
41637
+ },
41638
+ {
41639
+ id: "2026-08-16-render-annotation-scaling",
41640
+ version: "0.9.4",
41641
+ date: "2026-08-16",
41642
+ category: "fix",
41643
+ title: "Render overlays stay readable at any size",
41644
+ summary: "The axes gizmo, part labels and dimensions now scale with the canvas instead of staying 1024px-sized, dimension lines clear round parts, and labels no longer land on the geometry they name.",
41645
+ features: [
41646
+ "rendering",
41647
+ "drafting"
41648
+ ],
41649
+ mcpTools: [
41650
+ "render_view"
41651
+ ]
41652
+ },
41653
+ {
41654
+ id: "2026-08-16-render-aa-and-depth-cue",
41655
+ version: "0.9.4",
41656
+ date: "2026-08-16",
41657
+ category: "fix",
41658
+ title: "Smoother renders: anti-aliasing and a per-pixel depth cue",
41659
+ summary: "Isometric renders anti-alias their silhouettes and linework, flat faces no longer break into hard tonal wedges at triangle boundaries, and edge strokes sit on their geometry instead of half a stroke off.",
41660
+ features: [
41661
+ "rendering",
41662
+ "tessellation"
41663
+ ],
41664
+ mcpTools: [
41665
+ "render_view"
41666
+ ]
41667
+ },
41668
+ {
41669
+ id: "2026-08-16-loon-sheet-metal-bindings",
41670
+ version: "0.9.4",
41671
+ date: "2026-08-16",
41672
+ category: "feat",
41673
+ title: "Loon can author sheet metal, faceting and imports",
41674
+ summary: "Sheet-metal chains, pinned segment counts and vendor STEP/STL imports are now expressible in loon, so a bent part carries its bends and unfolds exactly instead of having them inferred back out of the solid.",
41675
+ features: [
41676
+ "loon",
41677
+ "sheet-metal",
41678
+ "import",
41679
+ "tessellation"
41680
+ ],
41681
+ mcpTools: [
41682
+ "create_cad_loon",
41683
+ "sheet_metal_unfold",
41684
+ "flat_pattern_from_solid"
41685
+ ]
41686
+ },
41687
+ {
41688
+ id: "2026-08-16-cli-sheet-metal-fold",
41689
+ version: "0.9.4",
41690
+ date: "2026-08-16",
41691
+ category: "fix",
41692
+ title: "Sheet-metal parts export from the CLI again",
41693
+ summary: "vcad info/export and vcad-render now fold sheet-metal chains \u2014 including jogs and bend relief \u2014 instead of reporting zero triangles, and a root that fails to evaluate says why.",
41694
+ features: [
41695
+ "sheet-metal",
41696
+ "cli",
41697
+ "loon",
41698
+ "export"
41699
+ ]
41700
+ },
41562
41701
  {
41563
41702
  id: "2026-08-16-cli-loon-input-everywhere",
41564
41703
  version: "0.9.4",
@@ -49705,7 +49844,21 @@ var init_loon_macros = __esm({
49705
49844
  "type",
49706
49845
  "assembly",
49707
49846
  "part",
49708
- "instance"
49847
+ "instance",
49848
+ "cylinder-n",
49849
+ "sphere-n",
49850
+ "cone-n",
49851
+ "torus-n",
49852
+ "import-step",
49853
+ "import-step-body",
49854
+ "import-mesh",
49855
+ "import-mesh-scaled",
49856
+ "sheet-base-flange-rect",
49857
+ "sheet-base-flange",
49858
+ "sheet-edge-flange",
49859
+ "sheet-jog",
49860
+ "sheet-hem",
49861
+ "sheet-bend-relief"
49709
49862
  ]);
49710
49863
  registry2 = /* @__PURE__ */ new Map();
49711
49864
  hydrated = false;
@@ -50088,12 +50241,27 @@ function createCadLoon(input, engine) {
50088
50241
  const text2 = Object.keys(modules).length ? "Error: this kernel build cannot resolve loon modules ([use ...]) \u2014 update the kernel, or inline the modules into `source`" : "Error: Loon evaluation not supported by this engine build";
50089
50242
  return { content: [{ type: "text", text: text2 }] };
50090
50243
  }
50091
- const text = format === "json" ? JSON.stringify(doc, null, 2) : toVCode2(doc);
50244
+ const { text, fellBack } = serializeDocument2(doc, format);
50092
50245
  const content = [{ type: "text", text }];
50246
+ if (fellBack) {
50247
+ content.push({
50248
+ type: "text",
50249
+ text: `Note: returned the JSON IR instead of VCode \u2014 this document uses ops VCode cannot express (${fellBack}). The document itself is complete and every downstream tool works on it; only the compact text form is unavailable.`
50250
+ });
50251
+ }
50093
50252
  const note = parametricNote(source, modules, engine, doc);
50094
50253
  if (note) content.push({ type: "text", text: note });
50095
50254
  return { content };
50096
50255
  }
50256
+ function serializeDocument2(doc, format) {
50257
+ if (format === "json") return { text: JSON.stringify(doc, null, 2), fellBack: null };
50258
+ try {
50259
+ return { text: toVCode2(doc), fellBack: null };
50260
+ } catch (e) {
50261
+ const why = e instanceof Error ? e.message : String(e);
50262
+ return { text: JSON.stringify(doc, null, 2), fellBack: why };
50263
+ }
50264
+ }
50097
50265
  function parametricNote(source, modules, engine, doc) {
50098
50266
  const names = Object.keys(doc.parameters ?? {}).sort();
50099
50267
  if (!names.length) return null;
@@ -50189,7 +50357,7 @@ var init_loon = __esm({
50189
50357
  {
50190
50358
  name: "create_cad_loon",
50191
50359
  pack: null,
50192
- description: 'The preferred authoring tool for whole parts and multi-feature models \u2014 one call, full vocabulary. Create a CAD document from loon source code. Loon is a Lisp-like language for parametric CAD \u2014 the FULL modeling vocabulary (patterns, sketches, extrude/revolve/sweep/loft, assemblies) is available here even where no dedicated MCP tool exists. For incremental single-node edits to an open session, use create/update/delete instead.\n\nPrimitives: [cube x y z], [cylinder r h], [sphere r], [cone r-bottom r-top h], [torus major-r minor-r], [wedge x y z], [prism sides radius height]\nBooleans (subject-last): [difference tool subject], [union other subject], [intersection other subject]\nTransforms (subject-last): [translate x y z s], [rotate rx ry rz s], [scale sx sy sz s], [mirror ox oy oz nx ny nz s] (plane through the origin point with that normal) \u2014 plus the axis sugar [mirror-x s] / [mirror-y s] / [mirror-z s], which mirror through the origin, negating that one coordinate. NEVER hand-mirror by negating coordinates; use these.\nFeatures: [fillet r s], [chamfer d s], [shell t s]\nPatterns (subject-last): [linear-pattern dx dy dz count spacing s], [circular-pattern ox oy oz ax ay az count angle s] \u2014 e.g. a bolt circle is [circular-pattern 0 0 0 0 0 1 6 360 bolt-hole]\nSymmetric patterns (subject-last): [mirror-pattern nx ny nz s] and its sugar [mirror-pattern-x s] / -y / -z union a solid with its mirror image (a left/right pair in one expression, so the halves can\'t drift apart); [quad-pattern s] is the 4-fold X-and-Y version \u2014 a quadruped\'s legs, a 4-post frame, a vehicle chassis\nSketches: [sketch ox oy oz xx xy xz yx yy yz #[segments]] with [line x1 y1 x2 y2] and [arc x1 y1 x2 y2 cx cy ccw]\nSketch ops (sketch-last): [extrude dx dy dz sk], [revolve aox aoy aoz adx ady adz angle sk], [sweep-line sx sy sz ex ey ez sk], [sweep-helix radius pitch height turns sk], [loft #[sk1 sk2 \u2026]], [loft-closed #[sk1 sk2 \u2026]]\nAssemblies: [assembly #[parts] #[instances] #[joints] ground-id] with [part name solid "material"], [instance name part-name x y z], [revolute-joint \u2026], [prismatic-joint \u2026], [fixed-joint \u2026], [ball-joint \u2026]\nAssembly symmetry: author ONE side as an assembly, then [mirror-group-x "-r" side] (or -y / -z) returns it plus a mirrored, suffixed copy \u2014 parts reflected, placements and joint anchors mirrored, and joint axes flipped by the correct rule (a hinge across its own mirror normal keeps its axis; the other two flip), so the same joint state drives both sides symmetrically. Splice it back with [assembly-join chassis mirrored]. NEVER hand-mirror an assembly.\nPipe: [pipe [cube 50 30 5] [difference [cylinder 3 10]] [fillet 1.0]]\nLet bindings: [let body [cube 50 30 5]] \u2014 note these are inlined and do NOT survive into the document; use [defparam ...] for a value you want to change later\nParameters (survive into the document and are settable afterwards with set_parameters, and differentiable with parameter_gradient): [defparam pitch_axis_x 310.0], [defparam wall "bore * 0.2"] for derived values, plus optional :unit/:min/:max/:description. Names must be identifier-safe (underscores, not dashes)\nDatums \u2014 named reference geometry, so two parts cannot each hold their own copy of a shared plane: [datum-plane "femur_inner" y 131.0], [datum-axis "pitch" x 0 0 310], [datum-point "hip" 0 0 310], read back with [datum "femur_inner"], [datum+ "femur_inner" 3.0] (3 mm outboard), [datum-x/-y/-z "pitch"]\nStacks \u2014 declarative packing, where each running clearance is a named value instead of an arbitrary number: [stack y "leg" 131.0 [lane "femur_inner" 5.0] [gap "idler_run" 1.0] [lane "idler_boss" 3.0]] declares datum planes leg_femur_inner_lo/_hi, leg_idler_boss_lo/_hi and leg_end; widening leg_idler_run slides everything outboard of it\nScene: [root solid "material-name"], with [material name r g b metallic roughness] to define one\nModules: [use bracket] then [bracket.plate] \u2014 multi-file projects work here, with sources passed in `modules` (or read from `base_dir`); [use bracket :as b] aliases, [use bracket [plate]] imports selectively, and `pub` in a module picks what it exports',
50360
+ description: 'The preferred authoring tool for whole parts and multi-feature models \u2014 one call, full vocabulary. Create a CAD document from loon source code. Loon is a Lisp-like language for parametric CAD \u2014 the FULL modeling vocabulary (patterns, sketches, extrude/revolve/sweep/loft, assemblies) is available here even where no dedicated MCP tool exists. For incremental single-node edits to an open session, use create/update/delete instead.\n\nPrimitives: [cube x y z], [cylinder r h], [sphere r], [cone r-bottom r-top h], [torus major-r minor-r], [wedge x y z], [prism sides radius height]. Segment count is the kernel default (32); pin it with [cylinder-n r h n] / [sphere-n r n] / [cone-n rb rt h n] / [torus-n R r n] where the facets are load-bearing (a bore that must accept a real shaft)\nImports \u2014 place a purchased part instead of approximating it, so fit checks test the real envelope and not one you invented: [import-step "vendor/x6-60.step"], [import-step-body path index] for a multi-body file, [import-mesh "part.stl"], [import-mesh-scaled sx sy sz path]. Relative paths resolve against base_dir / the module source\nSheet metal (subject-last, so it threads through pipe) \u2014 author a cut-and-bent part as a bend chain and it KEEPS its bends, so sheet_metal_unfold is exact instead of inferred back out of a solid by flat_pattern_from_solid: [sheet-base-flange-rect width depth thickness "al-soft"] (or [sheet-base-flange #[x0 y0 \u2026] #[holes] t material] for an arbitrary outline) then [sheet-edge-flange edge length angle-deg s], [sheet-jog edge offset length s], [sheet-hem edge length s], [sheet-bend-relief s]. `edge` is an outline index, or "south"/"east"/"north"/"west" on a rectangular base flange (edge 0 is the -Y edge, CCW from there). Each has an -at form taking panel id, radius, direction ("up"/"down") and K-factor, where 0.0 means the default; -shop variants resolve every bend through a shop table. Sheet nodes are a bend graph, NOT solids \u2014 never union, transform or fillet them\nBooleans (subject-last): [difference tool subject], [union other subject], [intersection other subject]\nTransforms (subject-last): [translate x y z s], [rotate rx ry rz s], [scale sx sy sz s], [mirror ox oy oz nx ny nz s] (plane through the origin point with that normal) \u2014 plus the axis sugar [mirror-x s] / [mirror-y s] / [mirror-z s], which mirror through the origin, negating that one coordinate. NEVER hand-mirror by negating coordinates; use these.\nFeatures: [fillet r s], [chamfer d s], [shell t s]\nPatterns (subject-last): [linear-pattern dx dy dz count spacing s], [circular-pattern ox oy oz ax ay az count angle s] \u2014 e.g. a bolt circle is [circular-pattern 0 0 0 0 0 1 6 360 bolt-hole]\nSymmetric patterns (subject-last): [mirror-pattern nx ny nz s] and its sugar [mirror-pattern-x s] / -y / -z union a solid with its mirror image (a left/right pair in one expression, so the halves can\'t drift apart); [quad-pattern s] is the 4-fold X-and-Y version \u2014 a quadruped\'s legs, a 4-post frame, a vehicle chassis\nSketches: [sketch ox oy oz xx xy xz yx yy yz #[segments]] with [line x1 y1 x2 y2] and [arc x1 y1 x2 y2 cx cy ccw]\nSketch ops (sketch-last): [extrude dx dy dz sk], [revolve aox aoy aoz adx ady adz angle sk], [sweep-line sx sy sz ex ey ez sk], [sweep-helix radius pitch height turns sk], [loft #[sk1 sk2 \u2026]], [loft-closed #[sk1 sk2 \u2026]]\nAssemblies: [assembly #[parts] #[instances] #[joints] ground-id] with [part name solid "material"], [instance name part-name x y z], [revolute-joint \u2026], [prismatic-joint \u2026], [fixed-joint \u2026], [ball-joint \u2026]\nAssembly symmetry: author ONE side as an assembly, then [mirror-group-x "-r" side] (or -y / -z) returns it plus a mirrored, suffixed copy \u2014 parts reflected, placements and joint anchors mirrored, and joint axes flipped by the correct rule (a hinge across its own mirror normal keeps its axis; the other two flip), so the same joint state drives both sides symmetrically. Splice it back with [assembly-join chassis mirrored]. NEVER hand-mirror an assembly.\nPipe: [pipe [cube 50 30 5] [difference [cylinder 3 10]] [fillet 1.0]]\nLet bindings: [let body [cube 50 30 5]] \u2014 note these are inlined and do NOT survive into the document; use [defparam ...] for a value you want to change later\nParameters (survive into the document and are settable afterwards with set_parameters, and differentiable with parameter_gradient): [defparam pitch_axis_x 310.0], [defparam wall "bore * 0.2"] for derived values, plus optional :unit/:min/:max/:description. Names must be identifier-safe (underscores, not dashes)\nDatums \u2014 named reference geometry, so two parts cannot each hold their own copy of a shared plane: [datum-plane "femur_inner" y 131.0], [datum-axis "pitch" x 0 0 310], [datum-point "hip" 0 0 310], read back with [datum "femur_inner"], [datum+ "femur_inner" 3.0] (3 mm outboard), [datum-x/-y/-z "pitch"]\nStacks \u2014 declarative packing, where each running clearance is a named value instead of an arbitrary number: [stack y "leg" 131.0 [lane "femur_inner" 5.0] [gap "idler_run" 1.0] [lane "idler_boss" 3.0]] declares datum planes leg_femur_inner_lo/_hi, leg_idler_boss_lo/_hi and leg_end; widening leg_idler_run slides everything outboard of it\nScene: [root solid "material-name"], with [material name r g b metallic roughness] to define one\nModules: [use bracket] then [bracket.plate] \u2014 multi-file projects work here, with sources passed in `modules` (or read from `base_dir`); [use bracket :as b] aliases, [use bracket [plate]] imports selectively, and `pub` in a module picks what it exports',
50193
50361
  inputSchema: createCadLoonSchema,
50194
50362
  handler: async (args, ctx) => {
50195
50363
  const useLoons = Array.isArray(args.use_loons) ? args.use_loons : void 0;
@@ -55430,6 +55598,8 @@ var init_tool_metadata = __esm({
55430
55598
  import_step: { title: "Import STEP", annotations: RW },
55431
55599
  import_urdf: { title: "Import URDF", annotations: RW },
55432
55600
  import_kicad: { title: "Import KiCad", annotations: RW },
55601
+ import_altium: { title: "Import Altium", annotations: RW },
55602
+ import_altium_library: { title: "Import Altium Library", annotations: RO },
55433
55603
  import_eagle: { title: "Import EAGLE", annotations: RW },
55434
55604
  open_in_browser: { title: "Open in Browser", annotations: RW },
55435
55605
  // ── Physics gym ────────────────────────────────────────────────────────
@@ -78436,10 +78606,72 @@ function readSource(input, label) {
78436
78606
  }
78437
78607
  throw new Error("Provide either `filename` or `content_base64`");
78438
78608
  }
78609
+ function readSourceBytes(input, label) {
78610
+ const { filename, content_base64 } = input;
78611
+ if (content_base64) {
78612
+ const buf = Buffer.from(content_base64, "base64");
78613
+ if (buf.length === 0) throw new Error("content_base64 decoded to zero bytes");
78614
+ if (buf.length > MAX_PCB_BYTES) {
78615
+ throw new Error(`${label} content exceeds the ${MAX_PCB_BYTES} byte limit`);
78616
+ }
78617
+ return buf;
78618
+ }
78619
+ if (filename) {
78620
+ if (isRemoteDeployment()) {
78621
+ throw new Error(
78622
+ `This hosted server has no filesystem access \u2014 pass the ${label} contents as \`content_base64\` instead of \`filename\`.`
78623
+ );
78624
+ }
78625
+ const filepath = resolveWithinRoot(
78626
+ filename,
78627
+ process.env.VCAD_MCP_EXPORT_DIR ?? process.cwd()
78628
+ );
78629
+ if (!existsSync5(filepath)) throw new Error(`${label} file not found`);
78630
+ const stat = statSync3(filepath);
78631
+ if (!stat.isFile()) throw new Error(`${label} path is not a regular file`);
78632
+ if (stat.size > MAX_PCB_BYTES) {
78633
+ throw new Error(`${label} file exceeds the ${MAX_PCB_BYTES} byte limit`);
78634
+ }
78635
+ return readFileSync8(filepath);
78636
+ }
78637
+ throw new Error("Provide either `filename` or `content_base64`");
78638
+ }
78639
+ function registerBoard(pcb, name) {
78640
+ const doc = createDocument();
78641
+ doc.nodes["1"] = {
78642
+ id: 1,
78643
+ name,
78644
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78645
+ op: { type: "PcbBoard", board: pcb }
78646
+ };
78647
+ doc.roots.push({ root: 1, material: "__pcb_fr4__" });
78648
+ doc.materials["__pcb_fr4__"] = {
78649
+ name: "__pcb_fr4__",
78650
+ color: [0.05, 0.35, 0.15],
78651
+ roughness: 0.6,
78652
+ metallic: 0
78653
+ };
78654
+ return registerSession(doc);
78655
+ }
78656
+ function boardSummary(pcb) {
78657
+ return {
78658
+ footprints: pcb.footprints.length,
78659
+ nets: pcb.nets.length,
78660
+ outline_vertices: pcb.outline?.vertices?.length ?? 0,
78661
+ traces: pcb.traces.length,
78662
+ vias: pcb.vias.length,
78663
+ zones: pcb.zones.length,
78664
+ design_rules: {
78665
+ trace_width: pcb.rules?.defaultRules?.traceWidth,
78666
+ clearance: pcb.rules?.defaultRules?.clearance,
78667
+ via_diameter: pcb.rules?.defaultRules?.viaDiameter
78668
+ }
78669
+ };
78670
+ }
78439
78671
  function boardName(input) {
78440
78672
  if (input.name) return input.name;
78441
78673
  if (input.filename) {
78442
- return input.filename.replace(/^.*[/\\]/, "").replace(/\.kicad_pcb$/i, "");
78674
+ return input.filename.replace(/^.*[/\\]/, "").replace(/\.(kicad_pcb|pcbdoc|pcblib|brd)$/i, "");
78443
78675
  }
78444
78676
  return "Imported board";
78445
78677
  }
@@ -78464,21 +78696,7 @@ async function importKicad(input) {
78464
78696
  "KiCad parsing is unavailable (kernel WASM is missing parseKicadPcb) or the file produced no board."
78465
78697
  );
78466
78698
  }
78467
- const doc = createDocument();
78468
- doc.nodes["1"] = {
78469
- id: 1,
78470
- name: boardName(args),
78471
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
78472
- op: { type: "PcbBoard", board: pcb }
78473
- };
78474
- doc.roots.push({ root: 1, material: "__pcb_fr4__" });
78475
- doc.materials["__pcb_fr4__"] = {
78476
- name: "__pcb_fr4__",
78477
- color: [0.05, 0.35, 0.15],
78478
- roughness: 0.6,
78479
- metallic: 0
78480
- };
78481
- const documentId = registerSession(doc);
78699
+ const documentId = registerBoard(pcb, boardName(args));
78482
78700
  const onlyPlacement = pcb.traces.length === 0 && pcb.vias.length === 0;
78483
78701
  return {
78484
78702
  content: [
@@ -78487,19 +78705,7 @@ async function importKicad(input) {
78487
78705
  text: JSON.stringify({
78488
78706
  success: true,
78489
78707
  document_id: documentId,
78490
- summary: {
78491
- footprints: pcb.footprints.length,
78492
- nets: pcb.nets.length,
78493
- outline_vertices: pcb.outline?.vertices?.length ?? 0,
78494
- traces: pcb.traces.length,
78495
- vias: pcb.vias.length,
78496
- zones: pcb.zones.length,
78497
- design_rules: {
78498
- trace_width: pcb.rules?.defaultRules?.traceWidth,
78499
- clearance: pcb.rules?.defaultRules?.clearance,
78500
- via_diameter: pcb.rules?.defaultRules?.viaDiameter
78501
- }
78502
- },
78708
+ summary: boardSummary(pcb),
78503
78709
  ...onlyPlacement ? {
78504
78710
  note: "Placement + netlist only \u2014 the file carried no routed copper."
78505
78711
  } : {}
@@ -78508,12 +78714,138 @@ async function importKicad(input) {
78508
78714
  ]
78509
78715
  };
78510
78716
  }
78511
- function importEagle(_input) {
78512
- return importError(
78513
- "Eagle (.brd) import is not yet supported. Export your board from Eagle as KiCad (File > Export > KiCad .kicad_pcb) and use import_kicad instead."
78514
- );
78717
+ function isCompoundFile(bytes) {
78718
+ return bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([208, 207, 17, 224, 161, 177, 26, 225]));
78719
+ }
78720
+ async function importAltium(input) {
78721
+ const args = input ?? {};
78722
+ let bytes;
78723
+ try {
78724
+ bytes = readSourceBytes(args, "Altium");
78725
+ } catch (e) {
78726
+ return importError(e instanceof Error ? e.message : String(e));
78727
+ }
78728
+ const binary = isCompoundFile(bytes);
78729
+ let pcb;
78730
+ try {
78731
+ pcb = binary ? await parseAltiumPcbDoc(new Uint8Array(bytes)) : await parseAltiumAsciiPcb(bytes.toString("utf8"));
78732
+ } catch (e) {
78733
+ return importError(
78734
+ `Could not parse the Altium ${binary ? "binary" : "ASCII"} .PcbDoc: ` + (e instanceof Error ? e.message : String(e))
78735
+ );
78736
+ }
78737
+ if (!pcb) {
78738
+ return importError(
78739
+ "Altium parsing is unavailable (kernel WASM is missing parseAltiumPcbDoc)."
78740
+ );
78741
+ }
78742
+ const documentId = registerBoard(pcb, boardName(args));
78743
+ const onlyPlacement = pcb.traces.length === 0 && pcb.vias.length === 0;
78744
+ return {
78745
+ content: [
78746
+ {
78747
+ type: "text",
78748
+ text: JSON.stringify({
78749
+ success: true,
78750
+ document_id: documentId,
78751
+ source_format: binary ? "PcbDoc (binary)" : "PcbDoc (ASCII)",
78752
+ summary: boardSummary(pcb),
78753
+ ...onlyPlacement ? { note: "Placement + netlist only \u2014 the file carried no routed copper." } : {},
78754
+ ...binary ? {
78755
+ note_binary: "Binary .PcbDoc record layouts are reconstructed, not specified by Altium, and are validated against real open-hardware projects. Geometry that decoded implausibly fails the import outright rather than importing wrong, but spot-check with render_pcb before fabricating. Copper pours and text are not imported."
78756
+ } : {}
78757
+ })
78758
+ }
78759
+ ]
78760
+ };
78761
+ }
78762
+ async function importAltiumLibrary(input) {
78763
+ const args = input ?? {};
78764
+ let bytes;
78765
+ try {
78766
+ bytes = readSourceBytes(args, "Altium library");
78767
+ } catch (e) {
78768
+ return importError(e instanceof Error ? e.message : String(e));
78769
+ }
78770
+ let lib;
78771
+ try {
78772
+ lib = await parseAltiumPcbLib(new Uint8Array(bytes));
78773
+ } catch (e) {
78774
+ return importError(
78775
+ `Could not parse the Altium .PcbLib: ${e instanceof Error ? e.message : String(e)}`
78776
+ );
78777
+ }
78778
+ if (!lib) {
78779
+ return importError(
78780
+ "Altium parsing is unavailable (kernel WASM is missing parseAltiumPcbLib)."
78781
+ );
78782
+ }
78783
+ return {
78784
+ content: [
78785
+ {
78786
+ type: "text",
78787
+ text: JSON.stringify({
78788
+ success: true,
78789
+ footprint_count: lib.footprints.length,
78790
+ footprints: lib.footprints.map((f) => ({
78791
+ name: f.name,
78792
+ pad_count: f.pads.length,
78793
+ pads: f.pads.map((p) => ({
78794
+ number: p.number,
78795
+ type: p.pad_type,
78796
+ position: p.position,
78797
+ rotation: p.rotation,
78798
+ drill: p.drill?.diameter ?? null
78799
+ }))
78800
+ }))
78801
+ })
78802
+ }
78803
+ ]
78804
+ };
78805
+ }
78806
+ async function importEagle(input) {
78807
+ const args = input ?? {};
78808
+ let content;
78809
+ try {
78810
+ content = readSource(args, "Eagle");
78811
+ } catch (e) {
78812
+ return importError(e instanceof Error ? e.message : String(e));
78813
+ }
78814
+ if (!content.includes("<eagle") && !content.includes("<board")) {
78815
+ return importError(
78816
+ "This does not look like an Eagle XML .brd file. Eagle 5 and earlier wrote a binary .brd \u2014 open it in Eagle 6+ (which converts it to XML) and save, then import that."
78817
+ );
78818
+ }
78819
+ let pcb;
78820
+ try {
78821
+ pcb = await parseEagleBrd(content);
78822
+ } catch (e) {
78823
+ return importError(
78824
+ `Could not parse the Eagle .brd: ${e instanceof Error ? e.message : String(e)}`
78825
+ );
78826
+ }
78827
+ if (!pcb) {
78828
+ return importError(
78829
+ "Eagle parsing is unavailable (kernel WASM is missing parseEagleBrd)."
78830
+ );
78831
+ }
78832
+ const documentId = registerBoard(pcb, boardName(args));
78833
+ const onlyPlacement = pcb.traces.length === 0 && pcb.vias.length === 0;
78834
+ return {
78835
+ content: [
78836
+ {
78837
+ type: "text",
78838
+ text: JSON.stringify({
78839
+ success: true,
78840
+ document_id: documentId,
78841
+ summary: boardSummary(pcb),
78842
+ ...onlyPlacement ? { note: "Placement + netlist only \u2014 the file carried no routed copper." } : {}
78843
+ })
78844
+ }
78845
+ ]
78846
+ };
78515
78847
  }
78516
- var MAX_PCB_BYTES, importKicadSchema, importEagleSchema, toolDefs46;
78848
+ var MAX_PCB_BYTES, importKicadSchema, importAltiumSchema, importAltiumLibrarySchema, importEagleSchema, toolDefs46;
78517
78849
  var init_import_pcb = __esm({
78518
78850
  "src/tools/import-pcb.ts"() {
78519
78851
  "use strict";
@@ -78541,16 +78873,50 @@ var init_import_pcb = __esm({
78541
78873
  }
78542
78874
  }
78543
78875
  };
78876
+ importAltiumSchema = {
78877
+ type: "object",
78878
+ properties: {
78879
+ filename: {
78880
+ type: "string",
78881
+ description: "Path to a .PcbDoc file on the server filesystem (relative to VCAD_MCP_EXPORT_DIR or cwd). Native binary and ASCII exports are both accepted. On hosted servers pass content_base64 instead."
78882
+ },
78883
+ content_base64: {
78884
+ type: "string",
78885
+ description: "Base64-encoded .PcbDoc contents (preferred for hosted servers)."
78886
+ },
78887
+ name: {
78888
+ type: "string",
78889
+ description: "Board name for display (default: filename without extension)."
78890
+ }
78891
+ }
78892
+ };
78893
+ importAltiumLibrarySchema = {
78894
+ type: "object",
78895
+ properties: {
78896
+ filename: {
78897
+ type: "string",
78898
+ description: "Path to a .PcbLib footprint library (binary or ASCII)."
78899
+ },
78900
+ content_base64: {
78901
+ type: "string",
78902
+ description: "Base64-encoded .PcbLib contents."
78903
+ }
78904
+ }
78905
+ };
78544
78906
  importEagleSchema = {
78545
78907
  type: "object",
78546
78908
  properties: {
78547
78909
  filename: {
78548
78910
  type: "string",
78549
- description: "Path to a .brd file (Eagle XML format)."
78911
+ description: "Path to a .brd file (Eagle XML, Eagle 6+)."
78550
78912
  },
78551
78913
  content_base64: {
78552
78914
  type: "string",
78553
- description: "Base64-encoded .brd contents."
78915
+ description: "Base64-encoded .brd contents (preferred for hosted servers)."
78916
+ },
78917
+ name: {
78918
+ type: "string",
78919
+ description: "Board name for display (default: filename without extension)."
78554
78920
  }
78555
78921
  }
78556
78922
  };
@@ -78563,13 +78929,29 @@ var init_import_pcb = __esm({
78563
78929
  handler: async (a) => await importKicad(a),
78564
78930
  behavior: behavior({ writesDoc: true, geometry: true })
78565
78931
  },
78932
+ {
78933
+ name: "import_altium",
78934
+ pack: "ecad",
78935
+ description: "Import an Altium .PcbDoc board into a live session \u2014 board outline, footprints with pads + nets, design rules, and any routed traces/vias. Accepts both the native binary file and an ASCII export (detected automatically). Returns a document_id ready for render_pcb, run_drc, get_pad_positions, route_nets, and export_gerber. The binary path fails closed on record layouts it cannot decode rather than importing a partially-correct board. Pass content_base64 on hosted servers.",
78936
+ inputSchema: importAltiumSchema,
78937
+ handler: async (a) => await importAltium(a),
78938
+ behavior: behavior({ writesDoc: true, geometry: true })
78939
+ },
78940
+ {
78941
+ name: "import_altium_library",
78942
+ pack: "ecad",
78943
+ description: "Read an Altium .PcbLib footprint library (binary or ASCII) and return its footprint patterns with pads, shapes, and drills. A library is not a board, so no document_id is minted.",
78944
+ inputSchema: importAltiumLibrarySchema,
78945
+ handler: async (a) => await importAltiumLibrary(a),
78946
+ behavior: behavior({})
78947
+ },
78566
78948
  {
78567
78949
  name: "import_eagle",
78568
78950
  pack: "ecad",
78569
- description: "Import an Eagle .brd file (not yet supported). Export your board from Eagle as KiCad (.kicad_pcb) and use import_kicad instead.",
78951
+ description: "Import an Eagle .brd board (XML, Eagle 6+) into a live session \u2014 board outline, packages placed as footprints with pads, signals as nets, and any hand-routed traces/vias. Returns a document_id ready for render_pcb, run_drc, get_pad_positions, route_nets, and export_gerber. Pass content_base64 on hosted servers.",
78570
78952
  inputSchema: importEagleSchema,
78571
- handler: (a) => importEagle(a),
78572
- behavior: behavior({})
78953
+ handler: async (a) => await importEagle(a),
78954
+ behavior: behavior({ writesDoc: true, geometry: true })
78573
78955
  }
78574
78956
  ];
78575
78957
  }
@@ -83601,8 +83983,8 @@ var init_server3 = __esm({
83601
83983
  init_order_feed();
83602
83984
  init_animate();
83603
83985
  PKG_VERSION = (() => {
83604
- if ("0.9.4-main.71") {
83605
- return "0.9.4-main.71";
83986
+ if ("0.9.4-main.73") {
83987
+ return "0.9.4-main.73";
83606
83988
  }
83607
83989
  try {
83608
83990
  const req = createRequire2(import.meta.url);
@@ -83611,8 +83993,8 @@ var init_server3 = __esm({
83611
83993
  return "0.0.0";
83612
83994
  }
83613
83995
  })();
83614
- BUILD_SHA = "db132e1acce8719160880940ad8f1445aaf18354";
83615
- BUILD_TIME = "2026-08-16T20:09:58.430Z";
83996
+ BUILD_SHA = "c94448044a725e430c38faf507027d5a10b2c68d";
83997
+ BUILD_TIME = "2026-08-17T23:42:23.328Z";
83616
83998
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
83617
83999
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
83618
84000
  INSTANCE_ID = randomUUID7().slice(0, 8);
@@ -83808,6 +84190,8 @@ var init_server3 = __esm({
83808
84190
  "import_step",
83809
84191
  "import_urdf",
83810
84192
  "import_kicad",
84193
+ "import_altium",
84194
+ "import_altium_library",
83811
84195
  "import_eagle",
83812
84196
  "open_in_browser",
83813
84197
  // ── Physics gym ────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.71",
3
+ "version": "0.9.4-main.73",
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": "db132e1acce8719160880940ad8f1445aaf18354",
23
- "builtAt": "2026-08-16T20:09:58.430Z"
22
+ "sha": "c94448044a725e430c38faf507027d5a10b2c68d",
23
+ "builtAt": "2026-08-17T23:42:23.328Z"
24
24
  }
25
25
  }
Binary file