@vcad/mcp 0.11.0-main.156 → 0.11.0-main.158
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 +4 -1
- package/index.mjs +256 -66
- package/package.json +5 -5
- package/skill/SKILL.md +89 -0
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
|
-
|
|
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 7b415c4523c95991e41ee59c2aff357ff5f7205d at 2026-11-11T15:52:11.773Z. 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
|
|
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
|
|
15184
|
+
return parse5(schema, value, ctx, finalizeParams(fn, _params));
|
|
15185
15185
|
};
|
|
15186
15186
|
return fn;
|
|
15187
15187
|
};
|
|
15188
15188
|
_decode = (_Err) => {
|
|
15189
|
-
const
|
|
15189
|
+
const parse5 = _parse(_Err);
|
|
15190
15190
|
const fn = (schema, value, _ctx, _params) => {
|
|
15191
|
-
return
|
|
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
|
|
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
|
|
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(
|
|
22336
|
-
return
|
|
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,
|
|
23635
|
-
await taskStore.storeTaskResult(taskId,
|
|
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,
|
|
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 "${
|
|
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,
|
|
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(
|
|
27227
|
+
serialize(parse5(uri, options), options);
|
|
27228
27228
|
} else if (typeof uri === "object") {
|
|
27229
27229
|
uri = /** @type {T} */
|
|
27230
|
-
|
|
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(
|
|
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 =
|
|
27244
|
-
relative =
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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,
|
|
31250
|
-
if (
|
|
31249
|
+
function blameOf(cause, status2) {
|
|
31250
|
+
if (status2 === "fixed")
|
|
31251
31251
|
return "credit";
|
|
31252
|
-
if (
|
|
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,
|
|
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,
|
|
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,
|
|
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 (
|
|
37195
|
-
updated.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,98 @@ var init_CHANGELOG = __esm({
|
|
|
44759
44759
|
CHANGELOG_default = {
|
|
44760
44760
|
$schema: "./changelog.schema.json",
|
|
44761
44761
|
entries: [
|
|
44762
|
+
{
|
|
44763
|
+
id: "2026-11-11-vcad-mcp-rust-core",
|
|
44764
|
+
version: "0.11.0",
|
|
44765
|
+
date: "2026-11-11",
|
|
44766
|
+
category: "feat",
|
|
44767
|
+
title: "`vcad mcp`: native Rust MCP server",
|
|
44768
|
+
summary: "New `vcad mcp` runs one Rust MCP core (2026-07-28 and legacy eras) over stdio, loopback HTTP or a unix socket. Core tools run natively; others proxy to the node server.",
|
|
44769
|
+
features: [
|
|
44770
|
+
"mcp",
|
|
44771
|
+
"cli"
|
|
44772
|
+
],
|
|
44773
|
+
mcpTools: [
|
|
44774
|
+
"server_info",
|
|
44775
|
+
"open_document",
|
|
44776
|
+
"get_document",
|
|
44777
|
+
"close_document",
|
|
44778
|
+
"create_cad_loon",
|
|
44779
|
+
"load_document",
|
|
44780
|
+
"inspect_cad",
|
|
44781
|
+
"render_view",
|
|
44782
|
+
"export_cad"
|
|
44783
|
+
]
|
|
44784
|
+
},
|
|
44785
|
+
{
|
|
44786
|
+
id: "2026-11-11-one-breath-install",
|
|
44787
|
+
version: "0.11.0",
|
|
44788
|
+
date: "2026-11-11",
|
|
44789
|
+
category: "feat",
|
|
44790
|
+
title: "One-breath agent install + stale-server warning",
|
|
44791
|
+
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.",
|
|
44792
|
+
features: [
|
|
44793
|
+
"mcp",
|
|
44794
|
+
"mac-app",
|
|
44795
|
+
"skill"
|
|
44796
|
+
],
|
|
44797
|
+
mcpTools: [
|
|
44798
|
+
"server_info"
|
|
44799
|
+
]
|
|
44800
|
+
},
|
|
44801
|
+
{
|
|
44802
|
+
id: "2026-11-11-live-receipt-badges",
|
|
44803
|
+
version: "0.11.0",
|
|
44804
|
+
date: "2026-11-11",
|
|
44805
|
+
category: "feat",
|
|
44806
|
+
title: "Live receipt badges and in-viewport agent questions",
|
|
44807
|
+
summary: 'Clearance claims re-verify on every edit as green/gray/red badges in the Mac app; agents ask "which edge?" and confirms answered by clicking in the viewport.',
|
|
44808
|
+
features: [
|
|
44809
|
+
"receipts",
|
|
44810
|
+
"mcp",
|
|
44811
|
+
"elicitation",
|
|
44812
|
+
"native-app"
|
|
44813
|
+
],
|
|
44814
|
+
mcpTools: [
|
|
44815
|
+
"assert_clearance",
|
|
44816
|
+
"verify_receipts",
|
|
44817
|
+
"accept_receipts",
|
|
44818
|
+
"pick_in_viewport",
|
|
44819
|
+
"ask_user"
|
|
44820
|
+
]
|
|
44821
|
+
},
|
|
44822
|
+
{
|
|
44823
|
+
id: "2026-11-11-counterbore-breakout",
|
|
44824
|
+
version: "0.11.0",
|
|
44825
|
+
date: "2026-11-11",
|
|
44826
|
+
category: "fix",
|
|
44827
|
+
title: "Counterbores breaking into a bore cut correctly",
|
|
44828
|
+
summary: "A counterbore or hole breaking out into a parallel bore was silently dropped or left an open mesh away from the origin; it now cuts analytically and closed.",
|
|
44829
|
+
features: [
|
|
44830
|
+
"booleans",
|
|
44831
|
+
"kernel"
|
|
44832
|
+
]
|
|
44833
|
+
},
|
|
44834
|
+
{
|
|
44835
|
+
id: "2026-11-11-agent-cursor",
|
|
44836
|
+
version: "0.11.0",
|
|
44837
|
+
date: "2026-11-11",
|
|
44838
|
+
category: "feat",
|
|
44839
|
+
title: "Agent cursor in the Mac app",
|
|
44840
|
+
summary: "Agents see your selection through MCP resources, a labeled cursor shows where they edit, and each agent turn is one named undo step.",
|
|
44841
|
+
features: [
|
|
44842
|
+
"mcp",
|
|
44843
|
+
"native-app",
|
|
44844
|
+
"agent"
|
|
44845
|
+
],
|
|
44846
|
+
mcpTools: [
|
|
44847
|
+
"agent_turn",
|
|
44848
|
+
"fillet_edges",
|
|
44849
|
+
"select_edges",
|
|
44850
|
+
"viewer_frame",
|
|
44851
|
+
"open_in_app"
|
|
44852
|
+
]
|
|
44853
|
+
},
|
|
44762
44854
|
{
|
|
44763
44855
|
id: "2026-11-10-window-drag-no-orbit",
|
|
44764
44856
|
version: "0.11.0",
|
|
@@ -57130,15 +57222,15 @@ async function reconcileBeforeWrite(documentId, documents2, call = callApp) {
|
|
|
57130
57222
|
const link = links.get(documentId);
|
|
57131
57223
|
if (!link?.shown || link.viewOnly) return null;
|
|
57132
57224
|
if (pushes.get(documentId)?.running) return null;
|
|
57133
|
-
const
|
|
57225
|
+
const status2 = await Promise.race([
|
|
57134
57226
|
call("link_status", { link_id: documentId }),
|
|
57135
57227
|
wait(RECONCILE_WAIT_MS).then(() => null)
|
|
57136
57228
|
]);
|
|
57137
|
-
if (
|
|
57229
|
+
if (status2?.linked === false) {
|
|
57138
57230
|
loseWindow(documentId, LOST_WINDOW);
|
|
57139
57231
|
return null;
|
|
57140
57232
|
}
|
|
57141
|
-
if (!
|
|
57233
|
+
if (!status2?.linked || !status2.edited) return null;
|
|
57142
57234
|
const pulled = await call("link_status", { link_id: documentId, pull: true });
|
|
57143
57235
|
if (!pulled?.document) return null;
|
|
57144
57236
|
const document = pulled.document;
|
|
@@ -57568,9 +57660,9 @@ async function saveDocument(args, store) {
|
|
|
57568
57660
|
if (savesToFiles(store)) {
|
|
57569
57661
|
const path = resolveWithinRoot(`${name}.vcad`, stateRoot());
|
|
57570
57662
|
writeFileSync2(path, JSON.stringify(doc));
|
|
57571
|
-
const
|
|
57663
|
+
const status2 = sourceStatus(doc);
|
|
57572
57664
|
let sourcePath;
|
|
57573
|
-
if (doc.source && !
|
|
57665
|
+
if (doc.source && !status2.source_stale) {
|
|
57574
57666
|
sourcePath = resolveWithinRoot(`${name}.loon`, stateRoot());
|
|
57575
57667
|
writeFileSync2(sourcePath, doc.source.text);
|
|
57576
57668
|
doc.source.path = sourcePath;
|
|
@@ -57584,8 +57676,8 @@ async function saveDocument(args, store) {
|
|
|
57584
57676
|
name,
|
|
57585
57677
|
path,
|
|
57586
57678
|
...sourcePath ? { source_path: sourcePath } : {},
|
|
57587
|
-
...doc.source &&
|
|
57588
|
-
source_not_written: `${
|
|
57679
|
+
...doc.source && status2.source_stale ? {
|
|
57680
|
+
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.`
|
|
57589
57681
|
} : {}
|
|
57590
57682
|
})
|
|
57591
57683
|
}
|
|
@@ -57616,7 +57708,7 @@ async function saveDocument(args, store) {
|
|
|
57616
57708
|
store.scope === "user" ? {
|
|
57617
57709
|
saved: true,
|
|
57618
57710
|
name: slug,
|
|
57619
|
-
hint: `Saved durably to your account \u2014 reopen anytime with load_document({name: "${slug}"})
|
|
57711
|
+
hint: `Saved durably to your account \u2014 reopen anytime with load_document({name: "${slug}"}).`
|
|
57620
57712
|
} : {
|
|
57621
57713
|
saved: true,
|
|
57622
57714
|
name: key3,
|
|
@@ -57878,7 +57970,7 @@ var init_session2 = __esm({
|
|
|
57878
57970
|
{
|
|
57879
57971
|
name: "save_document",
|
|
57880
57972
|
pack: null,
|
|
57881
|
-
description: "Persist a live session under a name so it can be reopened with load_document.
|
|
57973
|
+
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.",
|
|
57882
57974
|
inputSchema: saveDocumentSchema,
|
|
57883
57975
|
handler: (a, c) => saveDocument(a, c.sessionStore),
|
|
57884
57976
|
behavior: behavior({})
|
|
@@ -59152,7 +59244,7 @@ async function getPreviewGlb(doc, engine, instances = false) {
|
|
|
59152
59244
|
_vcad_glb: null,
|
|
59153
59245
|
version: preview.version,
|
|
59154
59246
|
error: "preview_too_large",
|
|
59155
|
-
detail: `Model too large for inline preview (${mb} MB of GLB, budget ${(PREVIEW_MAX_BASE64 / 1e6).toFixed(1)} MB) \u2014 open it in vcad
|
|
59247
|
+
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.`
|
|
59156
59248
|
})
|
|
59157
59249
|
}
|
|
59158
59250
|
]
|
|
@@ -59309,10 +59401,10 @@ var init_store = __esm({
|
|
|
59309
59401
|
async getAuthorization(id, userId) {
|
|
59310
59402
|
return memAuthz.get(memKey(userId, id)) ?? null;
|
|
59311
59403
|
}
|
|
59312
|
-
async setAuthorizationStatus(id, userId,
|
|
59404
|
+
async setAuthorizationStatus(id, userId, status2) {
|
|
59313
59405
|
const a = memAuthz.get(memKey(userId, id));
|
|
59314
59406
|
if (!a) return;
|
|
59315
|
-
memAuthz.set(memKey(userId, id), { ...a, status });
|
|
59407
|
+
memAuthz.set(memKey(userId, id), { ...a, status: status2 });
|
|
59316
59408
|
}
|
|
59317
59409
|
/**
|
|
59318
59410
|
* Local mirror of the debit_wallet RPC contract — enough to exercise the
|
|
@@ -61448,8 +61540,8 @@ function pruneTasks() {
|
|
|
61448
61540
|
}
|
|
61449
61541
|
}
|
|
61450
61542
|
}
|
|
61451
|
-
function touchTask(t2,
|
|
61452
|
-
if (
|
|
61543
|
+
function touchTask(t2, status2, message) {
|
|
61544
|
+
if (status2) t2.status = status2;
|
|
61453
61545
|
if (message !== void 0) t2.statusMessage = message;
|
|
61454
61546
|
t2.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
61455
61547
|
bus.emit(EVT_TASK, detailedTask(t2));
|
|
@@ -63005,7 +63097,7 @@ var init_continue_doc = __esm({
|
|
|
63005
63097
|
properties: {
|
|
63006
63098
|
token: {
|
|
63007
63099
|
type: "string",
|
|
63008
|
-
description: "Legacy
|
|
63100
|
+
description: "Legacy hosted share token. Not available in the local build (hosted services were retired) \u2014 returns an error; use `doc`."
|
|
63009
63101
|
},
|
|
63010
63102
|
doc: {
|
|
63011
63103
|
type: "string",
|
|
@@ -64689,7 +64781,7 @@ var init_part_stock = __esm({
|
|
|
64689
64781
|
LCSC_RE = /^C\d{2,}$/i;
|
|
64690
64782
|
JLC_SEARCH_URL = "https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList";
|
|
64691
64783
|
LCSC_DETAIL_URL = "https://wmsc.lcsc.com/ftps/wm/product/detail?productCode=";
|
|
64692
|
-
UA = "vcad-mcp parts-stock (+https://vcad
|
|
64784
|
+
UA = "vcad-mcp parts-stock (+https://github.com/muni-works/vcad)";
|
|
64693
64785
|
PartStockClient = class {
|
|
64694
64786
|
fetchImpl;
|
|
64695
64787
|
now;
|
|
@@ -68212,8 +68304,8 @@ function verifyClearanceClaims(doc, engine, receipt) {
|
|
|
68212
68304
|
checks.push({ ...common, status: "Holds" });
|
|
68213
68305
|
}
|
|
68214
68306
|
}
|
|
68215
|
-
const
|
|
68216
|
-
return { status, checks };
|
|
68307
|
+
const status2 = checks.some((c) => c.status === "Violated") ? "Violated" : checks.some((c) => c.status === "Stale") ? "Stale" : "Holds";
|
|
68308
|
+
return { status: status2, checks };
|
|
68217
68309
|
}
|
|
68218
68310
|
function hasClearanceClaims(receipt) {
|
|
68219
68311
|
return (receipt.claims ?? []).some((c) => c.id.startsWith(CLEARANCE_CLAIM_PREFIX));
|
|
@@ -77532,14 +77624,14 @@ async function verifyReceipt2(args, engine) {
|
|
|
77532
77624
|
if (!validity.valid) {
|
|
77533
77625
|
return pcbValidationError("verify_receipt", validity, args.document_id ? String(args.document_id) : void 0);
|
|
77534
77626
|
}
|
|
77535
|
-
const
|
|
77536
|
-
if (!
|
|
77627
|
+
const status3 = await verifyReceipt(pcb, legacy);
|
|
77628
|
+
if (!status3) {
|
|
77537
77629
|
return {
|
|
77538
77630
|
content: [{ type: "text", text: "Error: ECAD engine unavailable" }],
|
|
77539
77631
|
isError: true
|
|
77540
77632
|
};
|
|
77541
77633
|
}
|
|
77542
|
-
statuses.push(
|
|
77634
|
+
statuses.push(status3);
|
|
77543
77635
|
boardHash = legacy.board_hash;
|
|
77544
77636
|
}
|
|
77545
77637
|
let clearance;
|
|
@@ -77566,19 +77658,19 @@ async function verifyReceipt2(args, engine) {
|
|
|
77566
77658
|
const boundHash = [raw, legacy, unified].map((r) => r?.[DOCUMENT_STATE_FIELD]).find((h) => typeof h === "string" && h.length > 0);
|
|
77567
77659
|
const documentChanged = boundHash !== void 0 && boundHash !== await documentStateHash(ctx.doc);
|
|
77568
77660
|
const measuredStatus = worstStatus(statuses);
|
|
77569
|
-
const
|
|
77661
|
+
const status2 = measuredStatus === "Holds" && documentChanged ? "Stale" : measuredStatus;
|
|
77570
77662
|
const documentState = boundHash === void 0 ? {
|
|
77571
77663
|
bound: false,
|
|
77572
77664
|
note: "This receipt predates document binding, so Holds covers only the claims it re-measured, not the rest of the document."
|
|
77573
77665
|
} : {
|
|
77574
77666
|
bound: true,
|
|
77575
77667
|
changed: documentChanged,
|
|
77576
|
-
...
|
|
77668
|
+
...status2 === "Stale" && measuredStatus === "Holds" ? {
|
|
77577
77669
|
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."
|
|
77578
77670
|
} : {}
|
|
77579
77671
|
};
|
|
77580
77672
|
const payload = {
|
|
77581
|
-
status,
|
|
77673
|
+
status: status2,
|
|
77582
77674
|
document_state: documentState,
|
|
77583
77675
|
...boardHash ? { board_hash: boardHash } : {},
|
|
77584
77676
|
...clearance ? { clearance } : {},
|
|
@@ -88177,7 +88269,7 @@ var init_share = __esm({
|
|
|
88177
88269
|
init_tool_def();
|
|
88178
88270
|
init_session_core();
|
|
88179
88271
|
init_session_store();
|
|
88180
|
-
VCAD_DOWNLOAD_URL = "https://
|
|
88272
|
+
VCAD_DOWNLOAD_URL = "https://github.com/muni-works/vcad/releases/latest";
|
|
88181
88273
|
SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
88182
88274
|
openInAppSchema = {
|
|
88183
88275
|
type: "object",
|
|
@@ -91320,6 +91412,96 @@ var init_animate = __esm({
|
|
|
91320
91412
|
}
|
|
91321
91413
|
});
|
|
91322
91414
|
|
|
91415
|
+
// src/version-check.ts
|
|
91416
|
+
function parse4(v) {
|
|
91417
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/.exec(v.trim());
|
|
91418
|
+
if (!m) return null;
|
|
91419
|
+
return {
|
|
91420
|
+
core: [Number(m[1]), Number(m[2]), Number(m[3])],
|
|
91421
|
+
pre: m[4] ? m[4].split(".").map((p) => /^\d+$/.test(p) ? Number(p) : p) : []
|
|
91422
|
+
};
|
|
91423
|
+
}
|
|
91424
|
+
function compareVersions(a, b) {
|
|
91425
|
+
const pa = parse4(a);
|
|
91426
|
+
const pb = parse4(b);
|
|
91427
|
+
if (!pa || !pb) return null;
|
|
91428
|
+
for (let i = 0; i < 3; i++) {
|
|
91429
|
+
const d = pa.core[i] - pb.core[i];
|
|
91430
|
+
if (d !== 0) return d;
|
|
91431
|
+
}
|
|
91432
|
+
if (pa.pre.length === 0 || pb.pre.length === 0) {
|
|
91433
|
+
return pb.pre.length - pa.pre.length;
|
|
91434
|
+
}
|
|
91435
|
+
const n = Math.max(pa.pre.length, pb.pre.length);
|
|
91436
|
+
for (let i = 0; i < n; i++) {
|
|
91437
|
+
const x = pa.pre[i];
|
|
91438
|
+
const y = pb.pre[i];
|
|
91439
|
+
if (x === void 0) return -1;
|
|
91440
|
+
if (y === void 0) return 1;
|
|
91441
|
+
if (x === y) continue;
|
|
91442
|
+
if (typeof x === "number" && typeof y === "number") return x - y;
|
|
91443
|
+
if (typeof x === "number") return -1;
|
|
91444
|
+
if (typeof y === "number") return 1;
|
|
91445
|
+
return x < y ? -1 : 1;
|
|
91446
|
+
}
|
|
91447
|
+
return 0;
|
|
91448
|
+
}
|
|
91449
|
+
function judge(running, latest) {
|
|
91450
|
+
const cmp = compareVersions(running, latest);
|
|
91451
|
+
if (cmp === null) {
|
|
91452
|
+
return { state: "unknown", running, reason: `unparseable version (${running} vs ${latest})` };
|
|
91453
|
+
}
|
|
91454
|
+
if (cmp >= 0) return { state: "current", running, latest };
|
|
91455
|
+
return {
|
|
91456
|
+
state: "stale",
|
|
91457
|
+
running,
|
|
91458
|
+
latest,
|
|
91459
|
+
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).`
|
|
91460
|
+
};
|
|
91461
|
+
}
|
|
91462
|
+
function getUpdateStatus() {
|
|
91463
|
+
return status;
|
|
91464
|
+
}
|
|
91465
|
+
async function startUpdateCheck(running, fetchImpl = globalThis.fetch, timeoutMs = 3e3) {
|
|
91466
|
+
if (process.env.VCAD_MCP_NO_UPDATE_CHECK === "1") {
|
|
91467
|
+
status = { state: "unknown", running, reason: "disabled (VCAD_MCP_NO_UPDATE_CHECK=1)" };
|
|
91468
|
+
return status;
|
|
91469
|
+
}
|
|
91470
|
+
status = { state: "unknown", running, reason: "checking" };
|
|
91471
|
+
try {
|
|
91472
|
+
const ctrl = new AbortController();
|
|
91473
|
+
const t2 = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
91474
|
+
t2.unref?.();
|
|
91475
|
+
const res = await fetchImpl(NPM_LATEST_URL, {
|
|
91476
|
+
signal: ctrl.signal,
|
|
91477
|
+
headers: { accept: "application/json" }
|
|
91478
|
+
});
|
|
91479
|
+
clearTimeout(t2);
|
|
91480
|
+
if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
|
|
91481
|
+
const body = await res.json();
|
|
91482
|
+
if (typeof body.version !== "string") throw new Error("registry reply has no version");
|
|
91483
|
+
status = judge(running, body.version);
|
|
91484
|
+
if (status.state === "stale") console.error(`[mcp] ${status.message}`);
|
|
91485
|
+
} catch (e) {
|
|
91486
|
+
status = { state: "unknown", running, reason: `lookup failed: ${e.message}` };
|
|
91487
|
+
}
|
|
91488
|
+
return status;
|
|
91489
|
+
}
|
|
91490
|
+
function takeResultWarning() {
|
|
91491
|
+
if (status.state !== "stale" || warnedInResult) return null;
|
|
91492
|
+
warnedInResult = true;
|
|
91493
|
+
return `\u26A0 ${status.message}`;
|
|
91494
|
+
}
|
|
91495
|
+
var NPM_LATEST_URL, status, warnedInResult;
|
|
91496
|
+
var init_version_check = __esm({
|
|
91497
|
+
"src/version-check.ts"() {
|
|
91498
|
+
"use strict";
|
|
91499
|
+
NPM_LATEST_URL = "https://registry.npmjs.org/@vcad/mcp/latest";
|
|
91500
|
+
status = { state: "unknown", running: "", reason: "not checked yet" };
|
|
91501
|
+
warnedInResult = false;
|
|
91502
|
+
}
|
|
91503
|
+
});
|
|
91504
|
+
|
|
91323
91505
|
// src/server.ts
|
|
91324
91506
|
import { createRequire as createRequire3 } from "node:module";
|
|
91325
91507
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
@@ -91528,10 +91710,11 @@ async function createServer(existingEngine, context = { user: null }) {
|
|
|
91528
91710
|
console.error(
|
|
91529
91711
|
`[mcp] vcad ${VERSION_WITH_BUILD} (instance ${INSTANCE_ID}) \u2014 ${dispatchableTools.size} kernel tools; kernel WASM ${kernelWasmLoaded ? "ok" : "UNAVAILABLE"}`
|
|
91530
91712
|
);
|
|
91713
|
+
if (!process.env.VITEST) void startUpdateCheck(PKG_VERSION);
|
|
91531
91714
|
const serverInfoDef = {
|
|
91532
91715
|
name: "server_info",
|
|
91533
91716
|
pack: null,
|
|
91534
|
-
description:
|
|
91717
|
+
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.',
|
|
91535
91718
|
inputSchema: serverInfoSchema,
|
|
91536
91719
|
handler: async () => {
|
|
91537
91720
|
const buildInfo = getBuildInfo();
|
|
@@ -91547,7 +91730,8 @@ async function createServer(existingEngine, context = { user: null }) {
|
|
|
91547
91730
|
...kernelWasmMissing.length > 0 ? { kernel_wasm_missing_exports: kernelWasmMissing } : {},
|
|
91548
91731
|
kernel_tool_count: dispatchableTools.size,
|
|
91549
91732
|
disabled_tool_count: disabledTools.size,
|
|
91550
|
-
packs: packsSummary()
|
|
91733
|
+
packs: packsSummary(),
|
|
91734
|
+
update: getUpdateStatus()
|
|
91551
91735
|
})
|
|
91552
91736
|
}
|
|
91553
91737
|
]
|
|
@@ -91816,9 +92000,14 @@ async function createServer(existingEngine, context = { user: null }) {
|
|
|
91816
92000
|
// client that remembers the last pair learns about a restart on the
|
|
91817
92001
|
// very next call, instead of when a dead document_id finally fails.
|
|
91818
92002
|
boot_token: currentBootToken(),
|
|
91819
|
-
session_durable: isSessionStoreDurable()
|
|
92003
|
+
session_durable: isSessionStoreDurable(),
|
|
92004
|
+
update: getUpdateStatus().state
|
|
91820
92005
|
}
|
|
91821
92006
|
};
|
|
92007
|
+
const warning = takeResultWarning();
|
|
92008
|
+
if (warning && Array.isArray(result.content)) {
|
|
92009
|
+
result.content = [...result.content, { type: "text", text: warning }];
|
|
92010
|
+
}
|
|
91822
92011
|
} catch {
|
|
91823
92012
|
}
|
|
91824
92013
|
};
|
|
@@ -92147,11 +92336,11 @@ function noteSessionDurability(result, documentId) {
|
|
|
92147
92336
|
];
|
|
92148
92337
|
}
|
|
92149
92338
|
function noteSourceDivergence(result, documentId) {
|
|
92150
|
-
const
|
|
92151
|
-
if (!
|
|
92339
|
+
const status2 = sourceStatus(documents.get(documentId));
|
|
92340
|
+
if (!status2.source_stale) return;
|
|
92152
92341
|
result.content = [
|
|
92153
92342
|
...result.content ?? [],
|
|
92154
|
-
{ type: "text", text: JSON.stringify(
|
|
92343
|
+
{ type: "text", text: JSON.stringify(status2) }
|
|
92155
92344
|
];
|
|
92156
92345
|
}
|
|
92157
92346
|
function effectiveDocId(result, args) {
|
|
@@ -92257,12 +92446,13 @@ var init_server3 = __esm({
|
|
|
92257
92446
|
init_enclosure();
|
|
92258
92447
|
init_sim_replay();
|
|
92259
92448
|
init_animate();
|
|
92449
|
+
init_version_check();
|
|
92260
92450
|
setParameterAssetResolver((doc, base_dir, engine) => {
|
|
92261
92451
|
resolveLoonAssets(doc, { base_dir }, engine);
|
|
92262
92452
|
});
|
|
92263
92453
|
PKG_VERSION = (() => {
|
|
92264
|
-
if ("0.11.0-main.
|
|
92265
|
-
return "0.11.0-main.
|
|
92454
|
+
if ("0.11.0-main.158") {
|
|
92455
|
+
return "0.11.0-main.158";
|
|
92266
92456
|
}
|
|
92267
92457
|
try {
|
|
92268
92458
|
const req = createRequire3(import.meta.url);
|
|
@@ -92271,8 +92461,8 @@ var init_server3 = __esm({
|
|
|
92271
92461
|
return "0.0.0";
|
|
92272
92462
|
}
|
|
92273
92463
|
})();
|
|
92274
|
-
BUILD_SHA = "
|
|
92275
|
-
BUILD_TIME = "2026-11-
|
|
92464
|
+
BUILD_SHA = "7b415c4523c95991e41ee59c2aff357ff5f7205d";
|
|
92465
|
+
BUILD_TIME = "2026-11-11T15:52:11.773Z";
|
|
92276
92466
|
SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
|
|
92277
92467
|
VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
|
|
92278
92468
|
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.
|
|
3
|
+
"version": "0.11.0-main.158",
|
|
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/
|
|
18
|
+
"url": "https://github.com/muni-works/vcad"
|
|
19
19
|
},
|
|
20
|
-
"homepage": "https://vcad
|
|
20
|
+
"homepage": "https://github.com/muni-works/vcad",
|
|
21
21
|
"vcadBuild": {
|
|
22
|
-
"sha": "
|
|
23
|
-
"builtAt": "2026-11-
|
|
22
|
+
"sha": "7b415c4523c95991e41ee59c2aff357ff5f7205d",
|
|
23
|
+
"builtAt": "2026-11-11T15:52:11.773Z"
|
|
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.
|