@vcad/mcp 0.11.0-main.138 → 0.11.0-main.139

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 +203 -101
  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 2df8f6d220243cc013bc875337b0aceb4b6b25b3 at 2026-10-28T01:27:09.038Z. Source: https://github.com/ecto/vcad
9
+ Built from e8048d392e648e962e9df6ae719559a9322ba0d8 at 2026-11-07T02:30:11.369Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -11039,9 +11039,9 @@ function workerTransport(worker) {
11039
11039
  pending2.delete(id);
11040
11040
  });
11041
11041
  return {
11042
- request: (req) => new Promise((resolve7) => {
11042
+ request: (req) => new Promise((resolve8) => {
11043
11043
  const id = next++;
11044
- pending2.set(id, resolve7);
11044
+ pending2.set(id, resolve8);
11045
11045
  worker.postMessage({ id, req });
11046
11046
  }),
11047
11047
  dispose: () => worker.terminate()
@@ -11920,11 +11920,11 @@ var init_analyze = __esm({
11920
11920
  type: "module"
11921
11921
  });
11922
11922
  this.worker = worker;
11923
- this.ready = new Promise((resolve7, reject) => {
11923
+ this.ready = new Promise((resolve8, reject) => {
11924
11924
  const onMessage = (e) => {
11925
11925
  const { type, id, message, analysis } = e.data;
11926
11926
  if (type === "ready") {
11927
- resolve7();
11927
+ resolve8();
11928
11928
  return;
11929
11929
  }
11930
11930
  if (type === "error" && id === null) {
@@ -11948,9 +11948,9 @@ var init_analyze = __esm({
11948
11948
  return this.ready;
11949
11949
  }
11950
11950
  request(msg, transfer = []) {
11951
- return this.ensureWorker().then(() => new Promise((resolve7, reject) => {
11951
+ return this.ensureWorker().then(() => new Promise((resolve8, reject) => {
11952
11952
  const id = this.nextId++;
11953
- this.pending.set(id, { resolve: resolve7, reject });
11953
+ this.pending.set(id, { resolve: resolve8, reject });
11954
11954
  this.worker.postMessage({ ...msg, id }, transfer);
11955
11955
  }));
11956
11956
  }
@@ -12567,11 +12567,11 @@ var init_dist = __esm({
12567
12567
  return;
12568
12568
  try {
12569
12569
  const worker = new Worker(new URL("./eval-worker.js", import.meta.url), { type: "module" });
12570
- this.workerReady = new Promise((resolve7, reject) => {
12570
+ this.workerReady = new Promise((resolve8, reject) => {
12571
12571
  const onMessage = (e) => {
12572
12572
  if (e.data.type === "ready") {
12573
12573
  worker.removeEventListener("message", onMessage);
12574
- resolve7();
12574
+ resolve8();
12575
12575
  } else if (e.data.type === "error" && e.data.id === null) {
12576
12576
  worker.removeEventListener("message", onMessage);
12577
12577
  console.warn("[ENGINE] Worker WASM init failed:", e.data.message);
@@ -12794,7 +12794,7 @@ var init_dist = __esm({
12794
12794
  const worker = this.worker;
12795
12795
  const id = Math.random().toString(36).slice(2);
12796
12796
  const skipClash = options.skipClashDetection ?? false;
12797
- return new Promise((resolve7, reject) => {
12797
+ return new Promise((resolve8, reject) => {
12798
12798
  const onMessage = (e) => {
12799
12799
  if (e.data.id !== id)
12800
12800
  return;
@@ -12804,7 +12804,7 @@ var init_dist = __esm({
12804
12804
  if (scene.timing && _Engine.DEV) {
12805
12805
  _Engine.logTiming(scene.timing, e.data.workerTotalMs);
12806
12806
  }
12807
- resolve7(scene);
12807
+ resolve8(scene);
12808
12808
  } else if (e.data.type === "error") {
12809
12809
  reject(new Error(e.data.message));
12810
12810
  }
@@ -19237,12 +19237,12 @@ var init_stdio2 = __esm({
19237
19237
  this.onclose?.();
19238
19238
  }
19239
19239
  send(message) {
19240
- return new Promise((resolve7) => {
19240
+ return new Promise((resolve8) => {
19241
19241
  const json = serializeMessage(message);
19242
19242
  if (this._stdout.write(json)) {
19243
- resolve7();
19243
+ resolve8();
19244
19244
  } else {
19245
- this._stdout.once("drain", resolve7);
19245
+ this._stdout.once("drain", resolve8);
19246
19246
  }
19247
19247
  });
19248
19248
  }
@@ -20286,7 +20286,7 @@ var init_protocol = __esm({
20286
20286
  return;
20287
20287
  }
20288
20288
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
20289
- await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
20289
+ await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
20290
20290
  options?.signal?.throwIfAborted();
20291
20291
  }
20292
20292
  } catch (error2) {
@@ -20303,7 +20303,7 @@ var init_protocol = __esm({
20303
20303
  */
20304
20304
  request(request, resultSchema, options) {
20305
20305
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
20306
- return new Promise((resolve7, reject) => {
20306
+ return new Promise((resolve8, reject) => {
20307
20307
  const earlyReject = (error2) => {
20308
20308
  reject(error2);
20309
20309
  };
@@ -20381,7 +20381,7 @@ var init_protocol = __esm({
20381
20381
  if (!parseResult.success) {
20382
20382
  reject(parseResult.error);
20383
20383
  } else {
20384
- resolve7(parseResult.data);
20384
+ resolve8(parseResult.data);
20385
20385
  }
20386
20386
  } catch (error2) {
20387
20387
  reject(error2);
@@ -20642,12 +20642,12 @@ var init_protocol = __esm({
20642
20642
  }
20643
20643
  } catch {
20644
20644
  }
20645
- return new Promise((resolve7, reject) => {
20645
+ return new Promise((resolve8, reject) => {
20646
20646
  if (signal.aborted) {
20647
20647
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
20648
20648
  return;
20649
20649
  }
20650
- const timeoutId = setTimeout(resolve7, interval);
20650
+ const timeoutId = setTimeout(resolve8, interval);
20651
20651
  signal.addEventListener("abort", () => {
20652
20652
  clearTimeout(timeoutId);
20653
20653
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -23674,7 +23674,7 @@ var require_compile = __commonJS({
23674
23674
  const schOrFunc = root.refs[ref];
23675
23675
  if (schOrFunc)
23676
23676
  return schOrFunc;
23677
- let _sch = resolve7.call(this, root, ref);
23677
+ let _sch = resolve8.call(this, root, ref);
23678
23678
  if (_sch === void 0) {
23679
23679
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
23680
23680
  const { schemaId } = this.opts;
@@ -23701,7 +23701,7 @@ var require_compile = __commonJS({
23701
23701
  function sameSchemaEnv(s1, s2) {
23702
23702
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
23703
23703
  }
23704
- function resolve7(root, ref) {
23704
+ function resolve8(root, ref) {
23705
23705
  let sch;
23706
23706
  while (typeof (sch = this.refs[ref]) == "string")
23707
23707
  ref = sch;
@@ -24276,7 +24276,7 @@ var require_fast_uri = __commonJS({
24276
24276
  }
24277
24277
  return uri;
24278
24278
  }
24279
- function resolve7(baseURI, relativeURI, options) {
24279
+ function resolve8(baseURI, relativeURI, options) {
24280
24280
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
24281
24281
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
24282
24282
  schemelessOptions.skipEscape = true;
@@ -24503,7 +24503,7 @@ var require_fast_uri = __commonJS({
24503
24503
  var fastUri = {
24504
24504
  SCHEMES,
24505
24505
  normalize: normalize2,
24506
- resolve: resolve7,
24506
+ resolve: resolve8,
24507
24507
  resolveComponent,
24508
24508
  equal,
24509
24509
  serialize,
@@ -30675,21 +30675,21 @@ var require_react_development = __commonJS({
30675
30675
  );
30676
30676
  actScopeDepth = prevActScopeDepth;
30677
30677
  }
30678
- function recursivelyFlushAsyncActWork(returnValue, resolve7, reject) {
30678
+ function recursivelyFlushAsyncActWork(returnValue, resolve8, reject) {
30679
30679
  var queue = ReactSharedInternals.actQueue;
30680
30680
  if (null !== queue)
30681
30681
  if (0 !== queue.length)
30682
30682
  try {
30683
30683
  flushActQueue(queue);
30684
30684
  enqueueTask(function() {
30685
- return recursivelyFlushAsyncActWork(returnValue, resolve7, reject);
30685
+ return recursivelyFlushAsyncActWork(returnValue, resolve8, reject);
30686
30686
  });
30687
30687
  return;
30688
30688
  } catch (error2) {
30689
30689
  ReactSharedInternals.thrownErrors.push(error2);
30690
30690
  }
30691
30691
  else ReactSharedInternals.actQueue = null;
30692
- 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve7(returnValue);
30692
+ 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve8(returnValue);
30693
30693
  }
30694
30694
  function flushActQueue(queue) {
30695
30695
  if (!isFlushing) {
@@ -30876,7 +30876,7 @@ var require_react_development = __commonJS({
30876
30876
  ));
30877
30877
  });
30878
30878
  return {
30879
- then: function(resolve7, reject) {
30879
+ then: function(resolve8, reject) {
30880
30880
  didAwaitActCall = true;
30881
30881
  thenable.then(
30882
30882
  function(returnValue) {
@@ -30886,7 +30886,7 @@ var require_react_development = __commonJS({
30886
30886
  flushActQueue(queue), enqueueTask(function() {
30887
30887
  return recursivelyFlushAsyncActWork(
30888
30888
  returnValue,
30889
- resolve7,
30889
+ resolve8,
30890
30890
  reject
30891
30891
  );
30892
30892
  });
@@ -30900,7 +30900,7 @@ var require_react_development = __commonJS({
30900
30900
  ReactSharedInternals.thrownErrors.length = 0;
30901
30901
  reject(_thrownError);
30902
30902
  }
30903
- } else resolve7(returnValue);
30903
+ } else resolve8(returnValue);
30904
30904
  },
30905
30905
  function(error2) {
30906
30906
  popActScope(prevActQueue, prevActScopeDepth);
@@ -30922,15 +30922,15 @@ var require_react_development = __commonJS({
30922
30922
  if (0 < ReactSharedInternals.thrownErrors.length)
30923
30923
  throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
30924
30924
  return {
30925
- then: function(resolve7, reject) {
30925
+ then: function(resolve8, reject) {
30926
30926
  didAwaitActCall = true;
30927
30927
  0 === prevActScopeDepth ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
30928
30928
  return recursivelyFlushAsyncActWork(
30929
30929
  returnValue$jscomp$0,
30930
- resolve7,
30930
+ resolve8,
30931
30931
  reject
30932
30932
  );
30933
- })) : resolve7(returnValue$jscomp$0);
30933
+ })) : resolve8(returnValue$jscomp$0);
30934
30934
  }
30935
30935
  };
30936
30936
  };
@@ -41804,9 +41804,53 @@ var init_CHANGELOG = __esm({
41804
41804
  CHANGELOG_default = {
41805
41805
  $schema: "./changelog.schema.json",
41806
41806
  entries: [
41807
+ {
41808
+ id: "2026-11-06-export-all-documents",
41809
+ version: "0.11.0",
41810
+ date: "2026-11-06",
41811
+ category: "feat",
41812
+ title: "Export all your documents as a zip",
41813
+ summary: "New /export page downloads every cloud document (and this browser's local ones) as .vcad files in one zip, ahead of the move to the vcad 2 Mac app.",
41814
+ features: [
41815
+ "export",
41816
+ "cloud-sync",
41817
+ "vcad-2"
41818
+ ],
41819
+ mcpTools: [
41820
+ "open_in_app",
41821
+ "open_in_browser"
41822
+ ]
41823
+ },
41824
+ {
41825
+ id: "2026-10-28-render-timings-perf",
41826
+ version: "0.11.0",
41827
+ date: "2026-10-28",
41828
+ category: "perf",
41829
+ title: "Faster assembly renders, plus a --timings breakdown",
41830
+ summary: "Assembly renders evaluate parts in parallel, never evaluate a part twice, and serve warm re-renders straight from the cache (rose full body 613 s -> 55 s cold, 330 s -> 6 s warm); --timings shows where time goes.",
41831
+ features: [
41832
+ "render",
41833
+ "performance",
41834
+ "cli"
41835
+ ],
41836
+ mcpTools: []
41837
+ },
41838
+ {
41839
+ id: "2026-10-27-sphere-cube-seams",
41840
+ version: "0.11.0",
41841
+ date: "2026-10-27",
41842
+ category: "fix",
41843
+ title: "Clean sphere/box boolean seams at every resolution",
41844
+ summary: "Sphere-box unions, differences and intersections no longer grow jagged seams, slivers or lose volume depending on segment count; sphere patches are split and tessellated exactly.",
41845
+ features: [
41846
+ "booleans",
41847
+ "tessellation"
41848
+ ],
41849
+ mcpTools: []
41850
+ },
41807
41851
  {
41808
41852
  id: "2026-10-27-section-hatch-all-loops",
41809
- version: "0.10.0",
41853
+ version: "0.11.0",
41810
41854
  date: "2026-10-27",
41811
41855
  category: "fix",
41812
41856
  title: "Section views hatch every cut region",
@@ -41817,6 +41861,32 @@ var init_CHANGELOG = __esm({
41817
41861
  ],
41818
41862
  mcpTools: []
41819
41863
  },
41864
+ {
41865
+ id: "2026-10-27-onshape-step-import",
41866
+ version: "0.11.0",
41867
+ date: "2026-10-27",
41868
+ category: "fix",
41869
+ title: "STEP import: Onshape files import and render solid",
41870
+ summary: "Reads BREP_WITH_VOIDS, honours metre units, walks same_sense=.F. edges the right way, and meshes seamless cylinder/cone walls, drill tips and reversed planes facing outward.",
41871
+ features: [
41872
+ "step-import",
41873
+ "tessellation",
41874
+ "render"
41875
+ ]
41876
+ },
41877
+ {
41878
+ id: "2026-10-27-exact-render-step-curved-faces",
41879
+ version: "0.11.0",
41880
+ date: "2026-10-27",
41881
+ category: "fix",
41882
+ title: "Exact render shows curved faces of imported STEP parts",
41883
+ summary: "vcad-render --photoreal --exact no longer drops the cylinder, cone, sphere and torus faces of Onshape STEP imports, and frames them like the mesh render.",
41884
+ features: [
41885
+ "render",
41886
+ "raytrace",
41887
+ "step-import"
41888
+ ]
41889
+ },
41820
41890
  {
41821
41891
  id: "2026-10-27-cargo-install-vcad-cli",
41822
41892
  version: "0.11.0",
@@ -56840,8 +56910,8 @@ async function openBridge(createServer2, clientInfo, clientCapabilities, hooks)
56840
56910
  await server.connect(serverSide);
56841
56911
  const rpc = (method, params) => {
56842
56912
  const id = nextId3++;
56843
- return new Promise((resolve7) => {
56844
- pending2.set(id, (m) => resolve7(m));
56913
+ return new Promise((resolve8) => {
56914
+ pending2.set(id, (m) => resolve8(m));
56845
56915
  void clientSide.send({
56846
56916
  jsonrpc: "2.0",
56847
56917
  id,
@@ -57178,8 +57248,8 @@ function taskElicitation(bridge, reqParams) {
57178
57248
  method: "elicitation/create",
57179
57249
  params: cleanParams
57180
57250
  };
57181
- return new Promise((resolve7) => {
57182
- record2.pendingInputs.set(key3, resolve7);
57251
+ return new Promise((resolve8) => {
57252
+ record2.pendingInputs.set(key3, resolve8);
57183
57253
  touchTask(record2, "input_required", "waiting for client input");
57184
57254
  });
57185
57255
  }
@@ -57204,11 +57274,11 @@ function handleTaskMethod(id, method, params) {
57204
57274
  if (method === "tasks/update") {
57205
57275
  const responses = params.inputResponses && typeof params.inputResponses === "object" ? params.inputResponses : {};
57206
57276
  for (const [key3, response] of Object.entries(responses)) {
57207
- const resolve7 = record2.pendingInputs.get(key3);
57208
- if (!resolve7) continue;
57277
+ const resolve8 = record2.pendingInputs.get(key3);
57278
+ if (!resolve8) continue;
57209
57279
  record2.pendingInputs.delete(key3);
57210
57280
  delete record2.inputRequests[key3];
57211
- resolve7(response);
57281
+ resolve8(response);
57212
57282
  }
57213
57283
  if (record2.status === "input_required" && Object.keys(record2.inputRequests).length === 0) {
57214
57284
  touchTask(record2, "working", `running ${record2.toolName}`);
@@ -57220,10 +57290,10 @@ function handleTaskMethod(id, method, params) {
57220
57290
  }
57221
57291
  record2.cancelRequested = true;
57222
57292
  if (record2.status === "working" || record2.status === "input_required") {
57223
- for (const [key3, resolve7] of record2.pendingInputs) {
57293
+ for (const [key3, resolve8] of record2.pendingInputs) {
57224
57294
  record2.pendingInputs.delete(key3);
57225
57295
  delete record2.inputRequests[key3];
57226
- resolve7({ action: "cancel" });
57296
+ resolve8({ action: "cancel" });
57227
57297
  }
57228
57298
  touchTask(record2, "cancelled", "cancelled by client");
57229
57299
  }
@@ -57860,7 +57930,8 @@ var init_tool_metadata = __esm({
57860
57930
  import_altium: { title: "Import Altium", annotations: RW },
57861
57931
  import_altium_library: { title: "Import Altium Library", annotations: RO },
57862
57932
  import_eagle: { title: "Import EAGLE", annotations: RW },
57863
- open_in_browser: { title: "Open in Browser", annotations: RW },
57933
+ open_in_app: { title: "Open in vcad", annotations: RW },
57934
+ open_in_browser: { title: "Open in vcad (legacy name)", annotations: RW },
57864
57935
  // ── Physics gym ────────────────────────────────────────────────────────
57865
57936
  create_robot_env: { title: "Create Robot Env", annotations: RW },
57866
57937
  gym_step: { title: "Gym Step", annotations: RW },
@@ -58099,7 +58170,7 @@ function normalizeOp(op, index) {
58099
58170
  return { name, args };
58100
58171
  }
58101
58172
  function resolveSymbolicRefs(args, createdIds, index) {
58102
- const resolve7 = (v) => {
58173
+ const resolve8 = (v) => {
58103
58174
  if (typeof v !== "string") return v;
58104
58175
  const m = SYMBOLIC_REF.exec(v);
58105
58176
  if (!m) return v;
@@ -58119,15 +58190,15 @@ function resolveSymbolicRefs(args, createdIds, index) {
58119
58190
  };
58120
58191
  const out = { ...args };
58121
58192
  for (const key3 of ["part_id", "node_id"]) {
58122
- if (key3 in out) out[key3] = resolve7(out[key3]);
58193
+ if (key3 in out) out[key3] = resolve8(out[key3]);
58123
58194
  }
58124
58195
  if (out.params && typeof out.params === "object" && !Array.isArray(out.params)) {
58125
58196
  const params = { ...out.params };
58126
58197
  for (const key3 of ["child", "left", "right", "sketch"]) {
58127
- if (key3 in params) params[key3] = resolve7(params[key3]);
58198
+ if (key3 in params) params[key3] = resolve8(params[key3]);
58128
58199
  }
58129
58200
  if (Array.isArray(params.sketches)) {
58130
- params.sketches = params.sketches.map(resolve7);
58201
+ params.sketches = params.sketches.map(resolve8);
58131
58202
  }
58132
58203
  out.params = params;
58133
58204
  }
@@ -64060,7 +64131,7 @@ async function flattenSchematic(sheet, netlister) {
64060
64131
  if (def.body.hierarchy) {
64061
64132
  err9(`Sheet '${def.name}' body has its own hierarchy \u2014 nest sheets with \`instances\` instead (ignored)`);
64062
64133
  }
64063
- const resolve7 = (name) => {
64134
+ const resolve8 = (name) => {
64064
64135
  if (ports.has(name)) {
64065
64136
  const bound = binding.get(name);
64066
64137
  if (bound !== void 0) return bound;
@@ -64088,7 +64159,7 @@ async function flattenSchematic(sheet, netlister) {
64088
64159
  if (pins.length === 0) continue;
64089
64160
  if (isAutoName(name) && pins.length < 2) continue;
64090
64161
  usedNames.add(name);
64091
- const flat = resolve7(name);
64162
+ const flat = resolve8(name);
64092
64163
  if (!ports.has(name) && !globals.has(name) && rootNames.has(flat)) {
64093
64164
  err9(
64094
64165
  `Net scope collision: local net '${name}' of sheet instance '${path}' flattens to '${flat}', which the root sheet also uses`
@@ -64112,9 +64183,9 @@ async function flattenSchematic(sheet, netlister) {
64112
64183
  warn(`Hierarchical port '${port}' of sheet '${def.name}' connects to nothing inside the sheet`);
64113
64184
  }
64114
64185
  }
64115
- for (const flag of def.body.power_flags ?? []) outFlags.push({ ...flag, net: resolve7(flag.net) });
64186
+ for (const flag of def.body.power_flags ?? []) outFlags.push({ ...flag, net: resolve8(flag.net) });
64116
64187
  instances.push({ path, sheet: def.name, refs });
64117
- await elaborateAll(def.instances ?? [], path, offset, resolve7, stack);
64188
+ await elaborateAll(def.instances ?? [], path, offset, resolve8, stack);
64118
64189
  };
64119
64190
  await elaborateAll(h.instances ?? [], "", { x: 0, y: 0 }, (n) => n, []);
64120
64191
  const nets = {};
@@ -82277,6 +82348,8 @@ var init_import_pcb = __esm({
82277
82348
 
82278
82349
  // src/tools/share.ts
82279
82350
  import { gzipSync } from "node:zlib";
82351
+ import * as os2 from "node:os";
82352
+ import * as nodePath2 from "node:path";
82280
82353
  function validateAppUrl(raw) {
82281
82354
  try {
82282
82355
  const url = new URL(raw);
@@ -82308,56 +82381,75 @@ function compressForUrl(compact2) {
82308
82381
  const compressed = gzipSync(Buffer.from(compact2, "utf-8"), { level: 9 });
82309
82382
  return base64urlEncode(compressed);
82310
82383
  }
82311
- function openInBrowser(input, engine) {
82312
- const { document_id, document: docInput, name } = input;
82313
- if (!document_id && !docInput) {
82314
- throw new Error(
82315
- "Pass `document_id` (a live session) or `document` (inline IR)."
82316
- );
82384
+ function buildAppLink(doc, opts = {}) {
82385
+ const params = new URLSearchParams();
82386
+ const fileStore = opts.fileStore ?? useFileSessionStore();
82387
+ let via;
82388
+ if (opts.documentId && fileStore && SESSION_ID_RE.test(opts.documentId)) {
82389
+ params.set("session", opts.documentId);
82390
+ const dir = sessionDir();
82391
+ const defaultDir = nodePath2.join(os2.homedir(), ".vcad", "mcp-sessions");
82392
+ if (nodePath2.resolve(dir) !== nodePath2.resolve(defaultDir)) {
82393
+ params.set("dir", dir);
82394
+ }
82395
+ via = "session";
82396
+ } else {
82397
+ params.set("doc", compressForUrl(JSON.stringify(doc)));
82398
+ via = "inline";
82317
82399
  }
82318
- const live = document_id ? getSession(document_id) : parseDocument(docInput);
82319
- const doc = engine ? engine.bakeStepImports(live) : live;
82400
+ if (opts.name) params.set("name", opts.name);
82401
+ return { url: `vcad://open?${params.toString()}`, via };
82402
+ }
82403
+ function legacyWebLink(doc, name) {
82320
82404
  let payload;
82321
- let format;
82322
82405
  try {
82323
82406
  payload = toVCode2(doc);
82324
- format = "vcode";
82325
82407
  } catch {
82326
82408
  payload = JSON.stringify(doc);
82327
- format = "json";
82328
82409
  }
82329
- const encoded = compressForUrl(payload);
82330
82410
  const baseUrl = validateAppUrl(process.env.VCAD_APP_URL || "https://vcad.io");
82331
82411
  const params = new URLSearchParams();
82332
- params.set("doc", encoded);
82333
- if (name) {
82334
- params.set("name", name);
82412
+ params.set("doc", compressForUrl(payload));
82413
+ if (name) params.set("name", name);
82414
+ return `${baseUrl}/#/new?${params.toString()}`;
82415
+ }
82416
+ function openInApp(input, engine) {
82417
+ const { document_id, document: docInput, name } = input;
82418
+ if (!document_id && !docInput) {
82419
+ throw new Error(
82420
+ "Pass `document_id` (a live session) or `document` (inline IR)."
82421
+ );
82335
82422
  }
82336
- const url = `${baseUrl}/#/new?${params.toString()}`;
82337
- const urlLength = url.length;
82338
- const warning = urlLength > 2e3 ? `
82339
-
82340
- Warning: URL is ${urlLength} characters. Some browsers may truncate URLs over ~2000 characters. Consider exporting to a file instead for larger documents.` : "";
82341
- return {
82342
- content: [
82343
- {
82344
- type: "text",
82345
- text: `Open in vcad.io:
82346
- ${url}${warning}
82347
-
82348
- Payload (${format}): ${payload.length} bytes
82349
- Compressed URL param: ${encoded.length} bytes`
82350
- }
82351
- ]
82352
- };
82423
+ const live = document_id ? getSession(document_id) : parseDocument(docInput);
82424
+ const doc = engine ? engine.bakeStepImports(live) : live;
82425
+ const baked = doc !== live;
82426
+ const link = buildAppLink(doc, {
82427
+ documentId: baked ? void 0 : document_id,
82428
+ name
82429
+ });
82430
+ const lines = [
82431
+ `Open in vcad:
82432
+ ${link.url}`,
82433
+ "",
82434
+ `Don't have the vcad app? Download it: ${VCAD_DOWNLOAD_URL}`,
82435
+ link.via === "session" ? `(Link carries session id ${document_id}; the app reads it from this machine's session store, so it works on the machine running this MCP server.)` : `(Document embedded in the link: ${link.url.length} characters.)`
82436
+ ];
82437
+ if (/^(1|true|on)$/i.test(process.env.VCAD_LEGACY_WEB_LINKS || "")) {
82438
+ lines.push("", `Legacy web link (vcad.io, retiring):
82439
+ ${legacyWebLink(doc, name)}`);
82440
+ }
82441
+ return { content: [{ type: "text", text: lines.join("\n") }] };
82353
82442
  }
82354
- var openInBrowserSchema, toolDefs49;
82443
+ var VCAD_DOWNLOAD_URL, SESSION_ID_RE, openInBrowserSchema, DESCRIPTION, toolDefs49;
82355
82444
  var init_share = __esm({
82356
82445
  "src/tools/share.ts"() {
82357
82446
  "use strict";
82358
82447
  init_dist2();
82359
82448
  init_tool_def();
82360
82449
  init_session_core();
82450
+ init_session_store();
82451
+ VCAD_DOWNLOAD_URL = "https://vcad.io/download";
82452
+ SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
82361
82453
  openInBrowserSchema = {
82362
82454
  type: "object",
82363
82455
  properties: {
@@ -82375,13 +82467,22 @@ var init_share = __esm({
82375
82467
  }
82376
82468
  }
82377
82469
  };
82470
+ DESCRIPTION = "Open a CAD document in the native vcad app via a `vcad://` deep link. Pass a live session's `document_id` (the usual ship path), or an inline IR `document` (JSON or VCode format). Local sessions travel by id; otherwise the document is embedded (gzip + base64url). The result also carries the vcad download page as the fallback for users without the app.";
82378
82471
  toolDefs49 = [
82472
+ {
82473
+ name: "open_in_app",
82474
+ pack: null,
82475
+ description: DESCRIPTION,
82476
+ inputSchema: openInBrowserSchema,
82477
+ handler: (a, c) => openInApp(a, c.engine),
82478
+ behavior: behavior({})
82479
+ },
82379
82480
  {
82380
82481
  name: "open_in_browser",
82381
82482
  pack: null,
82382
- description: "Generate a shareable URL to open a CAD document in vcad.io. Pass a live session's `document_id` (the usual ship path), or an inline IR `document` (JSON or VCode format). Documents are compressed (gzip + base64url) for URL embedding. Note: Very large documents may exceed URL length limits (~2KB).",
82483
+ description: "Legacy alias of `open_in_app` (the vcad.io web editor is retiring). " + DESCRIPTION,
82383
82484
  inputSchema: openInBrowserSchema,
82384
- handler: (a, c) => openInBrowser(a, c.engine),
82485
+ handler: (a, c) => openInApp(a, c.engine),
82385
82486
  behavior: behavior({})
82386
82487
  }
82387
82488
  ];
@@ -85791,13 +85892,13 @@ ${issues.map((i) => i.problem).join("\n")}`
85791
85892
  }
85792
85893
  async function hasFfmpeg() {
85793
85894
  if (ffmpegAvailable !== null) return ffmpegAvailable;
85794
- ffmpegAvailable = await new Promise((resolve7) => {
85895
+ ffmpegAvailable = await new Promise((resolve8) => {
85795
85896
  try {
85796
85897
  const p = spawn("ffmpeg", ["-version"], { stdio: "ignore" });
85797
- p.on("error", () => resolve7(false));
85798
- p.on("exit", (code) => resolve7(code === 0));
85898
+ p.on("error", () => resolve8(false));
85899
+ p.on("exit", (code) => resolve8(code === 0));
85799
85900
  } catch {
85800
- resolve7(false);
85901
+ resolve8(false);
85801
85902
  }
85802
85903
  });
85803
85904
  return ffmpegAvailable;
@@ -85836,13 +85937,13 @@ function startMp4Encoder(width, height, fps) {
85836
85937
  const exitError = (code) => new Error(
85837
85938
  `ffmpeg exited ${code}: ${Buffer.concat(errChunks).toString().slice(-400)}`
85838
85939
  );
85839
- const done = new Promise((resolve7, reject) => {
85940
+ const done = new Promise((resolve8, reject) => {
85840
85941
  p.on("error", (e) => {
85841
85942
  failure = e;
85842
85943
  reject(e);
85843
85944
  });
85844
85945
  p.on("close", (code) => {
85845
- if (code === 0) resolve7(Buffer.concat(out));
85946
+ if (code === 0) resolve8(Buffer.concat(out));
85846
85947
  else {
85847
85948
  failure ??= exitError(code);
85848
85949
  reject(failure);
@@ -85853,15 +85954,15 @@ function startMp4Encoder(width, height, fps) {
85853
85954
  });
85854
85955
  return {
85855
85956
  writeFrame(rgba) {
85856
- return new Promise((resolve7, reject) => {
85957
+ return new Promise((resolve8, reject) => {
85857
85958
  if (failure) return reject(failure);
85858
85959
  if (p.exitCode !== null || p.stdin.destroyed) {
85859
85960
  return reject(failure ?? exitError(p.exitCode));
85860
85961
  }
85861
- if (p.stdin.write(rgba)) return resolve7();
85962
+ if (p.stdin.write(rgba)) return resolve8();
85862
85963
  const onDrain = () => {
85863
85964
  p.removeListener("close", onClose);
85864
- resolve7();
85965
+ resolve8();
85865
85966
  };
85866
85967
  const onClose = () => {
85867
85968
  p.stdin.removeListener("drain", onDrain);
@@ -86403,7 +86504,7 @@ function buildInstructions(kernelPrompt) {
86403
86504
  "Workflow: `open_document` \u2192 author \u2192 see \u2192 measure \u2192 ship.",
86404
86505
  "- Author whole parts with `create_cad_loon` (the full modeling vocabulary in one call); make surgical edits with `create`/`update`/`delete` (one node per call \u2014 mutation results report a compact `changed` diff of affected parts).",
86405
86506
  "- See your work with `render_view` (isometric PNG); measure with `inspect_cad` (whole-document volume, area, bbox, center of mass), `inspect_part` / `describe_scene` (per-part bbox, size, center, material), and `measure` (min distance/overlap between two parts, or one part's bbox+volume+center of mass).",
86406
- "- Ship with `export_cad` (STL/GLB/STEP) or `open_in_browser` (vcad.io deep link).",
86507
+ "- Ship with `export_cad` (STL/GLB/STEP) or `open_in_app` (vcad:// deep link into the native app).",
86407
86508
  "- Fix in place: when geometry is wrong, prefer `update` on the offending node over deleting parts and starting over.",
86408
86509
  "- Cut sheet parts you already modelled: `flat_pattern_from_solid` turns any constant-thickness solid \u2014 extruded sketch, boolean, imported STEP \u2014 into a fab-ready DXF plus a bend table, no sheet-metal re-authoring. Called without `part_id` it does the whole document and groups identical parts into one pattern \xD7 quantity; hand its `nest_input` to `sheet_metal_nest`, then `sheet_metal_cost`, for a quote. It fails closed: the emitted profile re-extruded by the detected thickness must reproduce the solid's volume.",
86409
86510
  "- Deliver the project bill of materials with `bom_create` \u2192 `bom_export` (markdown/CSV/JSON with landed-cost totals): link quote_manufacturing quotes on manufactured lines, and source COTS hardware (bearings, shafts, standoffs, screws, ferrite magnets) with `search_mechanical_parts`. Check electronic lines against live LCSC/JLCPCB stock and price with `bom_check_stock` (or one part with `check_part_stock`). All BOM prices are estimates and flagged as such.",
@@ -87303,8 +87404,8 @@ var init_server3 = __esm({
87303
87404
  init_order_feed();
87304
87405
  init_animate();
87305
87406
  PKG_VERSION = (() => {
87306
- if ("0.11.0-main.138") {
87307
- return "0.11.0-main.138";
87407
+ if ("0.11.0-main.139") {
87408
+ return "0.11.0-main.139";
87308
87409
  }
87309
87410
  try {
87310
87411
  const req = createRequire2(import.meta.url);
@@ -87313,8 +87414,8 @@ var init_server3 = __esm({
87313
87414
  return "0.0.0";
87314
87415
  }
87315
87416
  })();
87316
- BUILD_SHA = "2df8f6d220243cc013bc875337b0aceb4b6b25b3";
87317
- BUILD_TIME = "2026-10-28T01:27:09.038Z";
87417
+ BUILD_SHA = "e8048d392e648e962e9df6ae719559a9322ba0d8";
87418
+ BUILD_TIME = "2026-11-07T02:30:11.369Z";
87318
87419
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
87319
87420
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
87320
87421
  INSTANCE_ID = randomUUID7().slice(0, 8);
@@ -87518,6 +87619,7 @@ var init_server3 = __esm({
87518
87619
  "import_altium",
87519
87620
  "import_altium_library",
87520
87621
  "import_eagle",
87622
+ "open_in_app",
87521
87623
  "open_in_browser",
87522
87624
  // ── Physics gym ────────────────────────────────────────────
87523
87625
  "create_robot_env",
@@ -87687,11 +87789,11 @@ var init_index = __esm({
87687
87789
  // src/npx-entry.ts
87688
87790
  init_dist();
87689
87791
  import { readFileSync as readFileSync10 } from "node:fs";
87690
- import { dirname as dirname5, join as join5 } from "node:path";
87792
+ import { dirname as dirname5, join as join6 } from "node:path";
87691
87793
  import { fileURLToPath as fileURLToPath3 } from "node:url";
87692
87794
  primeKernelWasm(
87693
87795
  readFileSync10(
87694
- join5(
87796
+ join6(
87695
87797
  dirname5(fileURLToPath3(import.meta.url)),
87696
87798
  "vcad_kernel_wasm_bg.wasm"
87697
87799
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.11.0-main.138",
3
+ "version": "0.11.0-main.139",
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": "2df8f6d220243cc013bc875337b0aceb4b6b25b3",
23
- "builtAt": "2026-10-28T01:27:09.038Z"
22
+ "sha": "e8048d392e648e962e9df6ae719559a9322ba0d8",
23
+ "builtAt": "2026-11-07T02:30:11.369Z"
24
24
  }
25
25
  }