@vcad/mcp 0.11.0-main.152 → 0.11.0-main.157

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,7 @@ 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 0634d6e4b97496f0fe41d6537c3d1aa432a45a8e at 2026-11-11T03:30:11.380Z. Source: https://github.com/ecto/vcad
9
+ Or install vcad.app, which registers this server with your agents on first launch.
10
+ The agent skill ships at `skill/SKILL.md`.
11
+
12
+ Built from ff75bab3498cf0ba58c9ccd231c7c5af5fae2d1f at 2026-11-11T14:27:31.345Z. Source: https://github.com/muni-works/vcad
package/index.mjs CHANGED
@@ -15178,17 +15178,17 @@ var init_parse = __esm({
15178
15178
  return result.issues.length === 0;
15179
15179
  };
15180
15180
  _encode = (_Err) => {
15181
- const parse4 = _parse(_Err);
15181
+ const parse5 = _parse(_Err);
15182
15182
  const fn = (schema, value, _ctx, _params) => {
15183
15183
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
15184
- return parse4(schema, value, ctx, finalizeParams(fn, _params));
15184
+ return parse5(schema, value, ctx, finalizeParams(fn, _params));
15185
15185
  };
15186
15186
  return fn;
15187
15187
  };
15188
15188
  _decode = (_Err) => {
15189
- const parse4 = _parse(_Err);
15189
+ const parse5 = _parse(_Err);
15190
15190
  const fn = (schema, value, _ctx, _params) => {
15191
- return parse4(schema, value, _ctx, finalizeParams(fn, _params));
15191
+ return parse5(schema, value, _ctx, finalizeParams(fn, _params));
15192
15192
  };
15193
15193
  return fn;
15194
15194
  };
@@ -15802,7 +15802,7 @@ function capSize(inst, size) {
15802
15802
  if (!caps.length)
15803
15803
  return;
15804
15804
  const cap = Math.min(...caps.map((c) => c[0]));
15805
- const parse4 = inst._zod.parse;
15805
+ const parse5 = inst._zod.parse;
15806
15806
  inst._zod.parse = (payload, ctx) => {
15807
15807
  const n = size(payload.value);
15808
15808
  if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
@@ -15816,7 +15816,7 @@ function capSize(inst, size) {
15816
15816
  }
15817
15817
  }
15818
15818
  }
15819
- return parse4(payload, ctx);
15819
+ return parse5(payload, ctx);
15820
15820
  };
15821
15821
  }
15822
15822
  function handleArrayResult(result, final, index) {
@@ -22332,8 +22332,8 @@ var init_zod_compat = __esm({
22332
22332
  });
22333
22333
 
22334
22334
  // node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
22335
- function isTerminal(status) {
22336
- return status === "completed" || status === "failed" || status === "cancelled";
22335
+ function isTerminal(status2) {
22336
+ return status2 === "completed" || status2 === "failed" || status2 === "cancelled";
22337
22337
  }
22338
22338
  var init_interfaces = __esm({
22339
22339
  "node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js"() {
@@ -23631,8 +23631,8 @@ var init_protocol = __esm({
23631
23631
  }
23632
23632
  return task;
23633
23633
  },
23634
- storeTaskResult: async (taskId, status, result) => {
23635
- await taskStore.storeTaskResult(taskId, status, result, sessionId);
23634
+ storeTaskResult: async (taskId, status2, result) => {
23635
+ await taskStore.storeTaskResult(taskId, status2, result, sessionId);
23636
23636
  const task = await taskStore.getTask(taskId, sessionId);
23637
23637
  if (task) {
23638
23638
  const notification = TaskStatusNotificationSchema.parse({
@@ -23648,15 +23648,15 @@ var init_protocol = __esm({
23648
23648
  getTaskResult: (taskId) => {
23649
23649
  return taskStore.getTaskResult(taskId, sessionId);
23650
23650
  },
23651
- updateTaskStatus: async (taskId, status, statusMessage) => {
23651
+ updateTaskStatus: async (taskId, status2, statusMessage) => {
23652
23652
  const task = await taskStore.getTask(taskId, sessionId);
23653
23653
  if (!task) {
23654
23654
  throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found - it may have been cleaned up`);
23655
23655
  }
23656
23656
  if (isTerminal(task.status)) {
23657
- throw new McpError(ErrorCode.InvalidParams, `Cannot update task "${taskId}" from terminal status "${task.status}" to "${status}". Terminal states (completed, failed, cancelled) cannot transition to other states.`);
23657
+ throw new McpError(ErrorCode.InvalidParams, `Cannot update task "${taskId}" from terminal status "${task.status}" to "${status2}". Terminal states (completed, failed, cancelled) cannot transition to other states.`);
23658
23658
  }
23659
- await taskStore.updateTaskStatus(taskId, status, statusMessage, sessionId);
23659
+ await taskStore.updateTaskStatus(taskId, status2, statusMessage, sessionId);
23660
23660
  const updatedTask = await taskStore.getTask(taskId, sessionId);
23661
23661
  if (updatedTask) {
23662
23662
  const notification = TaskStatusNotificationSchema.parse({
@@ -27224,24 +27224,24 @@ var require_fast_uri = __commonJS({
27224
27224
  function normalize2(uri, options) {
27225
27225
  if (typeof uri === "string") {
27226
27226
  uri = /** @type {T} */
27227
- serialize(parse4(uri, options), options);
27227
+ serialize(parse5(uri, options), options);
27228
27228
  } else if (typeof uri === "object") {
27229
27229
  uri = /** @type {T} */
27230
- parse4(serialize(uri, options), options);
27230
+ parse5(serialize(uri, options), options);
27231
27231
  }
27232
27232
  return uri;
27233
27233
  }
27234
27234
  function resolve8(baseURI, relativeURI, options) {
27235
27235
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
27236
- const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
27236
+ const resolved = resolveComponent(parse5(baseURI, schemelessOptions), parse5(relativeURI, schemelessOptions), schemelessOptions, true);
27237
27237
  schemelessOptions.skipEscape = true;
27238
27238
  return serialize(resolved, schemelessOptions);
27239
27239
  }
27240
27240
  function resolveComponent(base, relative, options, skipNormalization) {
27241
27241
  const target = {};
27242
27242
  if (!skipNormalization) {
27243
- base = parse4(serialize(base, options), options);
27244
- relative = parse4(serialize(relative, options), options);
27243
+ base = parse5(serialize(base, options), options);
27244
+ relative = parse5(serialize(relative, options), options);
27245
27245
  }
27246
27246
  options = options || {};
27247
27247
  if (!options.tolerant && relative.scheme) {
@@ -27293,13 +27293,13 @@ var require_fast_uri = __commonJS({
27293
27293
  function equal(uriA, uriB, options) {
27294
27294
  if (typeof uriA === "string") {
27295
27295
  uriA = unescape(uriA);
27296
- uriA = serialize(normalizeComponentEncoding(parse4(uriA, options), true), { ...options, skipEscape: true });
27296
+ uriA = serialize(normalizeComponentEncoding(parse5(uriA, options), true), { ...options, skipEscape: true });
27297
27297
  } else if (typeof uriA === "object") {
27298
27298
  uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
27299
27299
  }
27300
27300
  if (typeof uriB === "string") {
27301
27301
  uriB = unescape(uriB);
27302
- uriB = serialize(normalizeComponentEncoding(parse4(uriB, options), true), { ...options, skipEscape: true });
27302
+ uriB = serialize(normalizeComponentEncoding(parse5(uriB, options), true), { ...options, skipEscape: true });
27303
27303
  } else if (typeof uriB === "object") {
27304
27304
  uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
27305
27305
  }
@@ -27368,7 +27368,7 @@ var require_fast_uri = __commonJS({
27368
27368
  return uriTokens.join("");
27369
27369
  }
27370
27370
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
27371
- function parse4(uri, opts) {
27371
+ function parse5(uri, opts) {
27372
27372
  const options = Object.assign({}, opts);
27373
27373
  const parsed = {
27374
27374
  scheme: void 0,
@@ -27462,7 +27462,7 @@ var require_fast_uri = __commonJS({
27462
27462
  resolveComponent,
27463
27463
  equal,
27464
27464
  serialize,
27465
- parse: parse4
27465
+ parse: parse5
27466
27466
  };
27467
27467
  module.exports = fastUri;
27468
27468
  module.exports.default = fastUri;
@@ -31246,10 +31246,10 @@ function diffViolations(before, after) {
31246
31246
  }
31247
31247
  return { introduced, fixed };
31248
31248
  }
31249
- function blameOf(cause, status) {
31250
- if (status === "fixed")
31249
+ function blameOf(cause, status2) {
31250
+ if (status2 === "fixed")
31251
31251
  return "credit";
31252
- if (status === "introduced") {
31252
+ if (status2 === "introduced") {
31253
31253
  return "blame";
31254
31254
  }
31255
31255
  if (cause === "connectivity" || cause === "routing")
@@ -31270,13 +31270,13 @@ function bucketize(violations) {
31270
31270
  }
31271
31271
  return map;
31272
31272
  }
31273
- function toGroup(b, status, count) {
31273
+ function toGroup(b, status2, count) {
31274
31274
  const cause = b.cause;
31275
31275
  return {
31276
31276
  rule: b.rep.rule,
31277
31277
  cause,
31278
- status,
31279
- blame: blameOf(cause, status),
31278
+ status: status2,
31279
+ blame: blameOf(cause, status2),
31280
31280
  count,
31281
31281
  message: b.rep.message,
31282
31282
  position: b.rep.position,
@@ -37182,7 +37182,7 @@ var init_chat_store = __esm({
37182
37182
  }
37183
37183
  ]
37184
37184
  })),
37185
- updateLastAssistant: (content, toolCalls, parts, status) => set((s) => {
37185
+ updateLastAssistant: (content, toolCalls, parts, status2) => set((s) => {
37186
37186
  const last = s.messages[s.messages.length - 1];
37187
37187
  if (!last || last.role !== "assistant")
37188
37188
  return s;
@@ -37191,8 +37191,8 @@ var init_chat_store = __esm({
37191
37191
  updated.toolCalls = toolCalls;
37192
37192
  if (parts !== void 0)
37193
37193
  updated.parts = parts;
37194
- if (status !== void 0)
37195
- updated.status = status;
37194
+ if (status2 !== void 0)
37195
+ updated.status = status2;
37196
37196
  return { messages: [...s.messages.slice(0, -1), updated] };
37197
37197
  }),
37198
37198
  sendMessage: (content, context, attachments) => {
@@ -44759,6 +44759,22 @@ var init_CHANGELOG = __esm({
44759
44759
  CHANGELOG_default = {
44760
44760
  $schema: "./changelog.schema.json",
44761
44761
  entries: [
44762
+ {
44763
+ id: "2026-11-11-one-breath-install",
44764
+ version: "0.11.0",
44765
+ date: "2026-11-11",
44766
+ category: "feat",
44767
+ title: "One-breath agent install + stale-server warning",
44768
+ summary: "vcad.app connects Claude Code, Claude Desktop, Codex and Cursor in one consent dialog (undo in Settings); a new bundled skill; the MCP server warns when it's older than npm latest.",
44769
+ features: [
44770
+ "mcp",
44771
+ "mac-app",
44772
+ "skill"
44773
+ ],
44774
+ mcpTools: [
44775
+ "server_info"
44776
+ ]
44777
+ },
44762
44778
  {
44763
44779
  id: "2026-11-10-window-drag-no-orbit",
44764
44780
  version: "0.11.0",
@@ -44808,6 +44824,21 @@ var init_CHANGELOG = __esm({
44808
44824
  "undo"
44809
44825
  ]
44810
44826
  },
44827
+ {
44828
+ id: "2026-11-10-step-bspline-import",
44829
+ version: "0.11.0",
44830
+ date: "2026-11-10",
44831
+ category: "fix",
44832
+ title: "STEP files with B-spline surfaces import whole",
44833
+ summary: "Rational and non-square B-spline faces no longer drop out of STEP imports, curved faces are trimmed to their edges, and opening such a file no longer shows the sample cube while it solves.",
44834
+ features: [
44835
+ "step",
44836
+ "import",
44837
+ "tessellation",
44838
+ "native",
44839
+ "mac"
44840
+ ]
44841
+ },
44811
44842
  {
44812
44843
  id: "2026-11-10-spur-gear-tool",
44813
44844
  version: "0.11.0",
@@ -44904,6 +44935,21 @@ var init_CHANGELOG = __esm({
44904
44935
  "check_enclosure_fit"
44905
44936
  ]
44906
44937
  },
44938
+ {
44939
+ id: "2026-11-10-open-progress-card",
44940
+ version: "0.11.0",
44941
+ date: "2026-11-10",
44942
+ category: "feat",
44943
+ title: "Import and first-solve progress card in the Mac app",
44944
+ summary: "Opening a STEP, .vcad or loon file shows a glass card with real progress (faces built, parts meshed), elapsed time and Cancel, then folds to a pill as parts land.",
44945
+ features: [
44946
+ "native",
44947
+ "mac",
44948
+ "step",
44949
+ "import",
44950
+ "viewport"
44951
+ ]
44952
+ },
44907
44953
  {
44908
44954
  id: "2026-11-10-native-sketch-bar",
44909
44955
  version: "0.11.0",
@@ -45010,6 +45056,20 @@ var init_CHANGELOG = __esm({
45010
45056
  "gym_step"
45011
45057
  ]
45012
45058
  },
45059
+ {
45060
+ id: "2026-11-10-fusion-nav-contact-shadow",
45061
+ version: "0.11.0",
45062
+ date: "2026-11-10",
45063
+ category: "feat",
45064
+ title: "Fusion 360 mouse controls; grounded contact shadow",
45065
+ summary: "The Mac app defaults to Fusion 360 navigation (middle-drag pan, Shift orbit, zoom at cursor; classic in Settings), and parts cast a footprint-shaped contact shadow.",
45066
+ features: [
45067
+ "native",
45068
+ "mac",
45069
+ "viewport",
45070
+ "navigation"
45071
+ ]
45072
+ },
45013
45073
  {
45014
45074
  id: "2026-11-10-first-delete-reuses-evaluations",
45015
45075
  version: "0.11.0",
@@ -57086,15 +57146,15 @@ async function reconcileBeforeWrite(documentId, documents2, call = callApp) {
57086
57146
  const link = links.get(documentId);
57087
57147
  if (!link?.shown || link.viewOnly) return null;
57088
57148
  if (pushes.get(documentId)?.running) return null;
57089
- const status = await Promise.race([
57149
+ const status2 = await Promise.race([
57090
57150
  call("link_status", { link_id: documentId }),
57091
57151
  wait(RECONCILE_WAIT_MS).then(() => null)
57092
57152
  ]);
57093
- if (status?.linked === false) {
57153
+ if (status2?.linked === false) {
57094
57154
  loseWindow(documentId, LOST_WINDOW);
57095
57155
  return null;
57096
57156
  }
57097
- if (!status?.linked || !status.edited) return null;
57157
+ if (!status2?.linked || !status2.edited) return null;
57098
57158
  const pulled = await call("link_status", { link_id: documentId, pull: true });
57099
57159
  if (!pulled?.document) return null;
57100
57160
  const document = pulled.document;
@@ -57524,9 +57584,9 @@ async function saveDocument(args, store) {
57524
57584
  if (savesToFiles(store)) {
57525
57585
  const path = resolveWithinRoot(`${name}.vcad`, stateRoot());
57526
57586
  writeFileSync2(path, JSON.stringify(doc));
57527
- const status = sourceStatus(doc);
57587
+ const status2 = sourceStatus(doc);
57528
57588
  let sourcePath;
57529
- if (doc.source && !status.source_stale) {
57589
+ if (doc.source && !status2.source_stale) {
57530
57590
  sourcePath = resolveWithinRoot(`${name}.loon`, stateRoot());
57531
57591
  writeFileSync2(sourcePath, doc.source.text);
57532
57592
  doc.source.path = sourcePath;
@@ -57540,8 +57600,8 @@ async function saveDocument(args, store) {
57540
57600
  name,
57541
57601
  path,
57542
57602
  ...sourcePath ? { source_path: sourcePath } : {},
57543
- ...doc.source && status.source_stale ? {
57544
- source_not_written: `${status.reason} The .loon was NOT written \u2014 it would not reproduce this document. The .vcad IR is the source of truth.`
57603
+ ...doc.source && status2.source_stale ? {
57604
+ source_not_written: `${status2.reason} The .loon was NOT written \u2014 it would not reproduce this document. The .vcad IR is the source of truth.`
57545
57605
  } : {}
57546
57606
  })
57547
57607
  }
@@ -57572,7 +57632,7 @@ async function saveDocument(args, store) {
57572
57632
  store.scope === "user" ? {
57573
57633
  saved: true,
57574
57634
  name: slug,
57575
- hint: `Saved durably to your account \u2014 reopen anytime with load_document({name: "${slug}"}). It also appears in your documents at vcad.io.`
57635
+ hint: `Saved durably to your account \u2014 reopen anytime with load_document({name: "${slug}"}).`
57576
57636
  } : {
57577
57637
  saved: true,
57578
57638
  name: key3,
@@ -57834,7 +57894,7 @@ var init_session2 = __esm({
57834
57894
  {
57835
57895
  name: "save_document",
57836
57896
  pack: null,
57837
- description: "Persist a live session under a name so it can be reopened with load_document. On the hosted server the save is durable: a signed-in user's save goes to their vcad.io account under the (normalized) name; an anonymous save returns an unguessable `saved_\u2026` key to reopen with. On a local/stdio server it writes `<name>.vcad` under VCAD_MCP_STATE_DIR (or the working directory) \u2014 plus `<name>.loon` when the document knows the source it was authored from, so the file and the session can't diverge without someone choosing it.",
57897
+ description: "Persist a live session under a name so it can be reopened with load_document. It writes `<name>.vcad` under VCAD_MCP_STATE_DIR (or the working directory) \u2014 plus `<name>.loon` when the document knows the source it was authored from, so the file and the session can't diverge without someone choosing it.",
57838
57898
  inputSchema: saveDocumentSchema,
57839
57899
  handler: (a, c) => saveDocument(a, c.sessionStore),
57840
57900
  behavior: behavior({})
@@ -59108,7 +59168,7 @@ async function getPreviewGlb(doc, engine, instances = false) {
59108
59168
  _vcad_glb: null,
59109
59169
  version: preview.version,
59110
59170
  error: "preview_too_large",
59111
- detail: `Model too large for inline preview (${mb} MB of GLB, budget ${(PREVIEW_MAX_BASE64 / 1e6).toFixed(1)} MB) \u2014 open it in vcad.io or use export_cad for the full geometry.`
59171
+ detail: `Model too large for inline preview (${mb} MB of GLB, budget ${(PREVIEW_MAX_BASE64 / 1e6).toFixed(1)} MB) \u2014 open it in the vcad app (open_in_app) or use export_cad for the full geometry.`
59112
59172
  })
59113
59173
  }
59114
59174
  ]
@@ -59265,10 +59325,10 @@ var init_store = __esm({
59265
59325
  async getAuthorization(id, userId) {
59266
59326
  return memAuthz.get(memKey(userId, id)) ?? null;
59267
59327
  }
59268
- async setAuthorizationStatus(id, userId, status) {
59328
+ async setAuthorizationStatus(id, userId, status2) {
59269
59329
  const a = memAuthz.get(memKey(userId, id));
59270
59330
  if (!a) return;
59271
- memAuthz.set(memKey(userId, id), { ...a, status });
59331
+ memAuthz.set(memKey(userId, id), { ...a, status: status2 });
59272
59332
  }
59273
59333
  /**
59274
59334
  * Local mirror of the debit_wallet RPC contract — enough to exercise the
@@ -61404,8 +61464,8 @@ function pruneTasks() {
61404
61464
  }
61405
61465
  }
61406
61466
  }
61407
- function touchTask(t2, status, message) {
61408
- if (status) t2.status = status;
61467
+ function touchTask(t2, status2, message) {
61468
+ if (status2) t2.status = status2;
61409
61469
  if (message !== void 0) t2.statusMessage = message;
61410
61470
  t2.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
61411
61471
  bus.emit(EVT_TASK, detailedTask(t2));
@@ -62961,7 +63021,7 @@ var init_continue_doc = __esm({
62961
63021
  properties: {
62962
63022
  token: {
62963
63023
  type: "string",
62964
- description: "Legacy vcad.io share token. Not available in the local build (hosted services were retired) \u2014 returns an error; use `doc`."
63024
+ description: "Legacy hosted share token. Not available in the local build (hosted services were retired) \u2014 returns an error; use `doc`."
62965
63025
  },
62966
63026
  doc: {
62967
63027
  type: "string",
@@ -64645,7 +64705,7 @@ var init_part_stock = __esm({
64645
64705
  LCSC_RE = /^C\d{2,}$/i;
64646
64706
  JLC_SEARCH_URL = "https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList";
64647
64707
  LCSC_DETAIL_URL = "https://wmsc.lcsc.com/ftps/wm/product/detail?productCode=";
64648
- UA = "vcad-mcp parts-stock (+https://vcad.io)";
64708
+ UA = "vcad-mcp parts-stock (+https://github.com/muni-works/vcad)";
64649
64709
  PartStockClient = class {
64650
64710
  fetchImpl;
64651
64711
  now;
@@ -68168,8 +68228,8 @@ function verifyClearanceClaims(doc, engine, receipt) {
68168
68228
  checks.push({ ...common, status: "Holds" });
68169
68229
  }
68170
68230
  }
68171
- const status = checks.some((c) => c.status === "Violated") ? "Violated" : checks.some((c) => c.status === "Stale") ? "Stale" : "Holds";
68172
- return { status, checks };
68231
+ const status2 = checks.some((c) => c.status === "Violated") ? "Violated" : checks.some((c) => c.status === "Stale") ? "Stale" : "Holds";
68232
+ return { status: status2, checks };
68173
68233
  }
68174
68234
  function hasClearanceClaims(receipt) {
68175
68235
  return (receipt.claims ?? []).some((c) => c.id.startsWith(CLEARANCE_CLAIM_PREFIX));
@@ -77488,14 +77548,14 @@ async function verifyReceipt2(args, engine) {
77488
77548
  if (!validity.valid) {
77489
77549
  return pcbValidationError("verify_receipt", validity, args.document_id ? String(args.document_id) : void 0);
77490
77550
  }
77491
- const status2 = await verifyReceipt(pcb, legacy);
77492
- if (!status2) {
77551
+ const status3 = await verifyReceipt(pcb, legacy);
77552
+ if (!status3) {
77493
77553
  return {
77494
77554
  content: [{ type: "text", text: "Error: ECAD engine unavailable" }],
77495
77555
  isError: true
77496
77556
  };
77497
77557
  }
77498
- statuses.push(status2);
77558
+ statuses.push(status3);
77499
77559
  boardHash = legacy.board_hash;
77500
77560
  }
77501
77561
  let clearance;
@@ -77522,19 +77582,19 @@ async function verifyReceipt2(args, engine) {
77522
77582
  const boundHash = [raw, legacy, unified].map((r) => r?.[DOCUMENT_STATE_FIELD]).find((h) => typeof h === "string" && h.length > 0);
77523
77583
  const documentChanged = boundHash !== void 0 && boundHash !== await documentStateHash(ctx.doc);
77524
77584
  const measuredStatus = worstStatus(statuses);
77525
- const status = measuredStatus === "Holds" && documentChanged ? "Stale" : measuredStatus;
77585
+ const status2 = measuredStatus === "Holds" && documentChanged ? "Stale" : measuredStatus;
77526
77586
  const documentState = boundHash === void 0 ? {
77527
77587
  bound: false,
77528
77588
  note: "This receipt predates document binding, so Holds covers only the claims it re-measured, not the rest of the document."
77529
77589
  } : {
77530
77590
  bound: true,
77531
77591
  changed: documentChanged,
77532
- ...status === "Stale" && measuredStatus === "Holds" ? {
77592
+ ...status2 === "Stale" && measuredStatus === "Holds" ? {
77533
77593
  note: "The document changed since this receipt was built, but every claim re-measured unchanged: the edit is outside what the receipt measures. Rebuild the receipt to certify the current document."
77534
77594
  } : {}
77535
77595
  };
77536
77596
  const payload = {
77537
- status,
77597
+ status: status2,
77538
77598
  document_state: documentState,
77539
77599
  ...boardHash ? { board_hash: boardHash } : {},
77540
77600
  ...clearance ? { clearance } : {},
@@ -88133,7 +88193,7 @@ var init_share = __esm({
88133
88193
  init_tool_def();
88134
88194
  init_session_core();
88135
88195
  init_session_store();
88136
- VCAD_DOWNLOAD_URL = "https://vcad.io/download";
88196
+ VCAD_DOWNLOAD_URL = "https://github.com/muni-works/vcad/releases/latest";
88137
88197
  SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
88138
88198
  openInAppSchema = {
88139
88199
  type: "object",
@@ -91276,6 +91336,96 @@ var init_animate = __esm({
91276
91336
  }
91277
91337
  });
91278
91338
 
91339
+ // src/version-check.ts
91340
+ function parse4(v) {
91341
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/.exec(v.trim());
91342
+ if (!m) return null;
91343
+ return {
91344
+ core: [Number(m[1]), Number(m[2]), Number(m[3])],
91345
+ pre: m[4] ? m[4].split(".").map((p) => /^\d+$/.test(p) ? Number(p) : p) : []
91346
+ };
91347
+ }
91348
+ function compareVersions(a, b) {
91349
+ const pa = parse4(a);
91350
+ const pb = parse4(b);
91351
+ if (!pa || !pb) return null;
91352
+ for (let i = 0; i < 3; i++) {
91353
+ const d = pa.core[i] - pb.core[i];
91354
+ if (d !== 0) return d;
91355
+ }
91356
+ if (pa.pre.length === 0 || pb.pre.length === 0) {
91357
+ return pb.pre.length - pa.pre.length;
91358
+ }
91359
+ const n = Math.max(pa.pre.length, pb.pre.length);
91360
+ for (let i = 0; i < n; i++) {
91361
+ const x = pa.pre[i];
91362
+ const y = pb.pre[i];
91363
+ if (x === void 0) return -1;
91364
+ if (y === void 0) return 1;
91365
+ if (x === y) continue;
91366
+ if (typeof x === "number" && typeof y === "number") return x - y;
91367
+ if (typeof x === "number") return -1;
91368
+ if (typeof y === "number") return 1;
91369
+ return x < y ? -1 : 1;
91370
+ }
91371
+ return 0;
91372
+ }
91373
+ function judge(running, latest) {
91374
+ const cmp = compareVersions(running, latest);
91375
+ if (cmp === null) {
91376
+ return { state: "unknown", running, reason: `unparseable version (${running} vs ${latest})` };
91377
+ }
91378
+ if (cmp >= 0) return { state: "current", running, latest };
91379
+ return {
91380
+ state: "stale",
91381
+ running,
91382
+ latest,
91383
+ message: `This vcad MCP server is out of date: running ${running}, newest is ${latest}. Tools, fixes and previews may be missing. Restart the agent so \`npx -y @vcad/mcp\` picks up the new version (or update vcad.app).`
91384
+ };
91385
+ }
91386
+ function getUpdateStatus() {
91387
+ return status;
91388
+ }
91389
+ async function startUpdateCheck(running, fetchImpl = globalThis.fetch, timeoutMs = 3e3) {
91390
+ if (process.env.VCAD_MCP_NO_UPDATE_CHECK === "1") {
91391
+ status = { state: "unknown", running, reason: "disabled (VCAD_MCP_NO_UPDATE_CHECK=1)" };
91392
+ return status;
91393
+ }
91394
+ status = { state: "unknown", running, reason: "checking" };
91395
+ try {
91396
+ const ctrl = new AbortController();
91397
+ const t2 = setTimeout(() => ctrl.abort(), timeoutMs);
91398
+ t2.unref?.();
91399
+ const res = await fetchImpl(NPM_LATEST_URL, {
91400
+ signal: ctrl.signal,
91401
+ headers: { accept: "application/json" }
91402
+ });
91403
+ clearTimeout(t2);
91404
+ if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
91405
+ const body = await res.json();
91406
+ if (typeof body.version !== "string") throw new Error("registry reply has no version");
91407
+ status = judge(running, body.version);
91408
+ if (status.state === "stale") console.error(`[mcp] ${status.message}`);
91409
+ } catch (e) {
91410
+ status = { state: "unknown", running, reason: `lookup failed: ${e.message}` };
91411
+ }
91412
+ return status;
91413
+ }
91414
+ function takeResultWarning() {
91415
+ if (status.state !== "stale" || warnedInResult) return null;
91416
+ warnedInResult = true;
91417
+ return `\u26A0 ${status.message}`;
91418
+ }
91419
+ var NPM_LATEST_URL, status, warnedInResult;
91420
+ var init_version_check = __esm({
91421
+ "src/version-check.ts"() {
91422
+ "use strict";
91423
+ NPM_LATEST_URL = "https://registry.npmjs.org/@vcad/mcp/latest";
91424
+ status = { state: "unknown", running: "", reason: "not checked yet" };
91425
+ warnedInResult = false;
91426
+ }
91427
+ });
91428
+
91279
91429
  // src/server.ts
91280
91430
  import { createRequire as createRequire3 } from "node:module";
91281
91431
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -91484,10 +91634,11 @@ async function createServer(existingEngine, context = { user: null }) {
91484
91634
  console.error(
91485
91635
  `[mcp] vcad ${VERSION_WITH_BUILD} (instance ${INSTANCE_ID}) \u2014 ${dispatchableTools.size} kernel tools; kernel WASM ${kernelWasmLoaded ? "ok" : "UNAVAILABLE"}`
91486
91636
  );
91637
+ if (!process.env.VITEST) void startUpdateCheck(PKG_VERSION);
91487
91638
  const serverInfoDef = {
91488
91639
  name: "server_info",
91489
91640
  pack: null,
91490
- description: "Report the running build's identity: version, git sha (if stamped), tool count, enabled packs, whether the kernel WASM loaded, and whether sessions are durable (`durable` \u2014 saved to the local disk store and survive a restart, vs. in-memory only). Call this to confirm a tool exists in THIS build before assuming a stale or version-skewed deploy, and to check durable:true before relying on checkpoints across a long session.",
91641
+ description: 'Report the running build\'s identity: version, git sha (if stamped), tool count, enabled packs, whether the kernel WASM loaded, and whether sessions are durable (`durable` \u2014 saved to the local disk store and survive a restart, vs. in-memory only). Call this to confirm a tool exists in THIS build before assuming a stale or version-skewed deploy, and to check durable:true before relying on checkpoints across a long session. `update` compares this build with the newest @vcad/mcp on npm; when `update.state` is "stale", tell the user and suggest restarting the agent.',
91491
91642
  inputSchema: serverInfoSchema,
91492
91643
  handler: async () => {
91493
91644
  const buildInfo = getBuildInfo();
@@ -91503,7 +91654,8 @@ async function createServer(existingEngine, context = { user: null }) {
91503
91654
  ...kernelWasmMissing.length > 0 ? { kernel_wasm_missing_exports: kernelWasmMissing } : {},
91504
91655
  kernel_tool_count: dispatchableTools.size,
91505
91656
  disabled_tool_count: disabledTools.size,
91506
- packs: packsSummary()
91657
+ packs: packsSummary(),
91658
+ update: getUpdateStatus()
91507
91659
  })
91508
91660
  }
91509
91661
  ]
@@ -91772,9 +91924,14 @@ async function createServer(existingEngine, context = { user: null }) {
91772
91924
  // client that remembers the last pair learns about a restart on the
91773
91925
  // very next call, instead of when a dead document_id finally fails.
91774
91926
  boot_token: currentBootToken(),
91775
- session_durable: isSessionStoreDurable()
91927
+ session_durable: isSessionStoreDurable(),
91928
+ update: getUpdateStatus().state
91776
91929
  }
91777
91930
  };
91931
+ const warning = takeResultWarning();
91932
+ if (warning && Array.isArray(result.content)) {
91933
+ result.content = [...result.content, { type: "text", text: warning }];
91934
+ }
91778
91935
  } catch {
91779
91936
  }
91780
91937
  };
@@ -92103,11 +92260,11 @@ function noteSessionDurability(result, documentId) {
92103
92260
  ];
92104
92261
  }
92105
92262
  function noteSourceDivergence(result, documentId) {
92106
- const status = sourceStatus(documents.get(documentId));
92107
- if (!status.source_stale) return;
92263
+ const status2 = sourceStatus(documents.get(documentId));
92264
+ if (!status2.source_stale) return;
92108
92265
  result.content = [
92109
92266
  ...result.content ?? [],
92110
- { type: "text", text: JSON.stringify(status) }
92267
+ { type: "text", text: JSON.stringify(status2) }
92111
92268
  ];
92112
92269
  }
92113
92270
  function effectiveDocId(result, args) {
@@ -92213,12 +92370,13 @@ var init_server3 = __esm({
92213
92370
  init_enclosure();
92214
92371
  init_sim_replay();
92215
92372
  init_animate();
92373
+ init_version_check();
92216
92374
  setParameterAssetResolver((doc, base_dir, engine) => {
92217
92375
  resolveLoonAssets(doc, { base_dir }, engine);
92218
92376
  });
92219
92377
  PKG_VERSION = (() => {
92220
- if ("0.11.0-main.152") {
92221
- return "0.11.0-main.152";
92378
+ if ("0.11.0-main.157") {
92379
+ return "0.11.0-main.157";
92222
92380
  }
92223
92381
  try {
92224
92382
  const req = createRequire3(import.meta.url);
@@ -92227,8 +92385,8 @@ var init_server3 = __esm({
92227
92385
  return "0.0.0";
92228
92386
  }
92229
92387
  })();
92230
- BUILD_SHA = "0634d6e4b97496f0fe41d6537c3d1aa432a45a8e";
92231
- BUILD_TIME = "2026-11-11T03:30:11.380Z";
92388
+ BUILD_SHA = "ff75bab3498cf0ba58c9ccd231c7c5af5fae2d1f";
92389
+ BUILD_TIME = "2026-11-11T14:27:31.345Z";
92232
92390
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
92233
92391
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
92234
92392
  INSTANCE_ID = randomUUID5().slice(0, 8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.11.0-main.152",
3
+ "version": "0.11.0-main.157",
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",
@@ -15,11 +15,11 @@
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/ecto/vcad"
18
+ "url": "https://github.com/muni-works/vcad"
19
19
  },
20
- "homepage": "https://vcad.io",
20
+ "homepage": "https://github.com/muni-works/vcad",
21
21
  "vcadBuild": {
22
- "sha": "0634d6e4b97496f0fe41d6537c3d1aa432a45a8e",
23
- "builtAt": "2026-11-11T03:30:11.380Z"
22
+ "sha": "ff75bab3498cf0ba58c9ccd231c7c5af5fae2d1f",
23
+ "builtAt": "2026-11-11T14:27:31.345Z"
24
24
  }
25
25
  }
package/skill/SKILL.md ADDED
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: vcad
3
+ description: Design real, manufacturable parts with vcad — a local parametric CAD kernel your agent drives over MCP. Use when the user asks for a 3D part, bracket, enclosure, mechanism, assembly, PCB-adjacent mechanical work, or anything they want to 3D print, machine, or cut. Author in loon, verify with inspect/render/receipts, and the user watches edits land live in vcad.app.
4
+ ---
5
+
6
+ # vcad
7
+
8
+ vcad is a parametric CAD system with its own BRep kernel. It runs entirely on
9
+ the user's machine — no accounts, no cloud, no share links. You drive it through
10
+ the `vcad` MCP server; the user watches in **vcad.app**.
11
+
12
+ ## The one flow
13
+
14
+ 1. **Open vcad.** The user launches vcad.app. On first launch it registered
15
+ itself with their agents, so you are already attached — the `vcad` tools are
16
+ in your tool list. (No app? The same server runs as `npx -y @vcad/mcp`.)
17
+ 2. **Author in loon.** Write the whole part as loon source in one
18
+ `create_cad_loon` call. That returns a `document_id`; every later tool takes it.
19
+ 3. **Verify before you claim.** Measure with `inspect_cad`, look with
20
+ `render_view`, prove with receipts (below). Never describe a dimension you
21
+ haven't measured.
22
+ 4. **Edits appear live.** In vcad.app, each authoring call shows up in the
23
+ viewport as one named undo step the user can step back through. Under npx,
24
+ hand the result over with `open_in_app` (a `vcad://` link).
25
+ 5. **Ship it.** `export_cad` writes STL / GLB / STEP; `save_document` writes a
26
+ `.vcad` (plus the `.loon` source) the user owns.
27
+
28
+ ## Loon in 60 seconds
29
+
30
+ Lisp-like, square brackets, millimetres, **Z-up**. Operations are
31
+ *subject-last*, so they thread through `pipe`.
32
+
33
+ ```loon
34
+ [defparam wall 3.0]
35
+ [defparam bore 8.2]
36
+
37
+ [let plate [extrude 0 0 wall
38
+ [sketch 0 0 0 1 0 0 0 1 0
39
+ #[[polygon #[0 0 60 0 60 40 0 40]
40
+ #[#[8 8 2.2] #[52 8 2.2] #[8 32 2.2] #[52 32 2.2]]]]]]]
41
+
42
+ [root [pipe plate
43
+ [union [translate 30 20 0 [cylinder 9 20]]]
44
+ [difference [translate 30 20 -1 [cylinder bore 22]]]]
45
+ "aluminum"]
46
+ ```
47
+
48
+ - `[cube x y z]` sits with its corner at the origin; `[cylinder r h]` /
49
+ `[cone rb rt h]` stand on the XY plane along +Z; `[sphere r]` is centred.
50
+ - Booleans: `[difference tool subject]`, `[union other subject]`.
51
+ - Plates with holes: ONE `[polygon outline #[holes]]` extruded — not a stack
52
+ of difference cutters.
53
+ - `[defparam name value]` survives into the document — change it later with
54
+ `set_parameters` instead of re-authoring. `[let …]` does not.
55
+ - Never hand-mirror by negating coordinates: `[mirror-x s]`,
56
+ `[mirror-pattern-x s]`, `[mirror-group-x "-r" side]` for assemblies.
57
+ - The full vocabulary (sweeps, lofts, sheet metal, assemblies and joints,
58
+ imports, modules) is in the `create_cad_loon` tool description. Read it.
59
+
60
+ ## Verify
61
+
62
+ | Question | Tool |
63
+ |---|---|
64
+ | Size, volume, mass, centre of mass | `inspect_cad` |
65
+ | What does it look like? | `render_view` (look at it — every time) |
66
+ | Do these parts collide / keep a gap? | `check_clearance` (labeled assertions persist) |
67
+ | Will it print / machine? | `dfm_check` |
68
+ | Is every claim still true after edits? | `build_receipt`, then `verify_receipt` |
69
+
70
+ Receipts are fail-closed: a claim is **Holds**, **Stale** (the geometry changed
71
+ since it was checked) or **Violated**. Predicted values from simulation are
72
+ *provisional* until measured. Report the receipt verdict, not your own summary.
73
+
74
+ ## When something looks broken
75
+
76
+ Call `server_info` first.
77
+
78
+ - Low `uptime_s` or a changed `instance_id` → the server restarted; re-run the
79
+ last authoring call. It is not a renderer bug.
80
+ - `update.state: "stale"` → this server is older than the newest release.
81
+ Tell the user and suggest restarting their agent (or updating vcad.app).
82
+ - `durable: false` → sessions won't survive a restart; keep the loon source.
83
+
84
+ ## Ground rules
85
+
86
+ - vcad has no hosted services. Never send the user to a website to view,
87
+ share, or order a part; use `open_in_app` and local files.
88
+ - One `create_cad_loon` for a whole part beats dozens of tiny edits.
89
+ - Units are millimetres unless the user says otherwise; say which you used.