@obenor/mcp 0.2.0 → 0.3.0

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.
@@ -25293,7 +25293,7 @@ var ENTRIES = [
25293
25293
  {
25294
25294
  name: "connect_model",
25295
25295
  summary: "Point this session at a model in the app, from a pasted link",
25296
- description: "Takes a share link the person copied out of the app and connects this session to the model behind it. From then on every change built here is published to that model, and a window with Follow set to Live shows it as it happens.\n\nTHE LINK IS THE WHOLE CONFIGURATION. It says which app and which model, so there is nothing else to ask for and nothing to set up: paste https://3d.obenor.com/s/XXXXXXXX and the connection is made.\n\nIT MUST BE AN EDIT LINK. A View, Measure or Comment link can read a model and cannot write one, and the refusal says which kind was given. Ask the person to use the Share sheet\u2019s Edit role.\n\nTHE CONNECTION LASTS AS LONG AS THIS PROCESS, and the link is never written to disk. Restarting the server means pasting again - which is the right trade for a credential, and the alternative is the OBENOR_LINK environment variable, which survives a restart because the person put it there themselves.\n\nA link is revoked and expires like any other. When one stops working the reply carries the app\u2019s own sentence - revoked, expired, or a model that is gone - so the fix is to make a new link rather than to debug this tool.",
25296
+ description: "Takes a share link the person copied out of the app and connects this session to the model behind it. From then on every change built here is published to that model, and a window with Follow set to Live shows it as it happens.\n\nConnecting loads the linked model into this session WITHOUT publishing the previous document into it. Save local work before switching models. Reconnecting the same link reuses the session. Switching releases only this process\u2019s previous agent slot. Use mirror status for connection/publication status, and mirror push to await publication.\n\nTHE LINK IS THE WHOLE CONFIGURATION. It says which app and which model, so there is nothing else to ask for and nothing to set up: paste https://3d.obenor.com/s/XXXXXXXX and the connection is made.\n\nIT MUST BE AN EDIT LINK. A View, Measure or Comment link can read a model and cannot write one, and the refusal says which kind was given. Ask the person to use the Share sheet\u2019s Edit role.\n\nTHE CONNECTION LASTS AS LONG AS THIS PROCESS, and the link is never written to disk. Restarting the server means pasting again - which is the right trade for a credential, and the alternative is the OBENOR_LINK environment variable, which survives a restart because the person put it there themselves.\n\nA link is revoked and expires like any other. When one stops working the reply carries the app\u2019s own sentence - revoked, expired, or a model that is gone - so the fix is to make a new link rather than to debug this tool.",
25297
25297
  declaredBy: ["mcp"]
25298
25298
  },
25299
25299
  {
package/bundle/server.mjs CHANGED
@@ -37159,6 +37159,11 @@ var CURVE2 = [
37159
37159
  "offset",
37160
37160
  "other"
37161
37161
  ];
37162
+ function reviewSoundness(topology) {
37163
+ const minEdge = defaultMinEdge(topology);
37164
+ const suspect = topology.edges.some((edge) => edge.faceIndices.length > 2 || !edge.seam && (edge.faceIndices.length === 1 || edge.length < minEdge));
37165
+ return suspect ? summariseSoundness(topology, { minEdge, maxGroups: 2, maxLines: 14 }) : [];
37166
+ }
37162
37167
  function defaultMinEdge(topology) {
37163
37168
  const { min, max } = topology.bbox;
37164
37169
  const size = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
@@ -37964,7 +37969,7 @@ var ENTRIES = [
37964
37969
  {
37965
37970
  name: "connect_model",
37966
37971
  summary: "Point this session at a model in the app, from a pasted link",
37967
- description: "Takes a share link the person copied out of the app and connects this session to the model behind it. From then on every change built here is published to that model, and a window with Follow set to Live shows it as it happens.\n\nTHE LINK IS THE WHOLE CONFIGURATION. It says which app and which model, so there is nothing else to ask for and nothing to set up: paste https://3d.obenor.com/s/XXXXXXXX and the connection is made.\n\nIT MUST BE AN EDIT LINK. A View, Measure or Comment link can read a model and cannot write one, and the refusal says which kind was given. Ask the person to use the Share sheet\u2019s Edit role.\n\nTHE CONNECTION LASTS AS LONG AS THIS PROCESS, and the link is never written to disk. Restarting the server means pasting again - which is the right trade for a credential, and the alternative is the OBENOR_LINK environment variable, which survives a restart because the person put it there themselves.\n\nA link is revoked and expires like any other. When one stops working the reply carries the app\u2019s own sentence - revoked, expired, or a model that is gone - so the fix is to make a new link rather than to debug this tool.",
37972
+ description: "Takes a share link the person copied out of the app and connects this session to the model behind it. From then on every change built here is published to that model, and a window with Follow set to Live shows it as it happens.\n\nConnecting loads the linked model into this session WITHOUT publishing the previous document into it. Save local work before switching models. Reconnecting the same link reuses the session. Switching releases only this process\u2019s previous agent slot. Use mirror status for connection/publication status, and mirror push to await publication.\n\nTHE LINK IS THE WHOLE CONFIGURATION. It says which app and which model, so there is nothing else to ask for and nothing to set up: paste https://3d.obenor.com/s/XXXXXXXX and the connection is made.\n\nIT MUST BE AN EDIT LINK. A View, Measure or Comment link can read a model and cannot write one, and the refusal says which kind was given. Ask the person to use the Share sheet\u2019s Edit role.\n\nTHE CONNECTION LASTS AS LONG AS THIS PROCESS, and the link is never written to disk. Restarting the server means pasting again - which is the right trade for a credential, and the alternative is the OBENOR_LINK environment variable, which survives a restart because the person put it there themselves.\n\nA link is revoked and expires like any other. When one stops working the reply carries the app\u2019s own sentence - revoked, expired, or a model that is gone - so the fix is to make a new link rather than to debug this tool.",
37968
37973
  declaredBy: ["mcp"]
37969
37974
  },
37970
37975
  {
@@ -39029,7 +39034,7 @@ var runScript = createScriptHost(nodeScriptWorker);
39029
39034
  // package.json
39030
39035
  var package_default = {
39031
39036
  name: "@obenor/mcp",
39032
- version: "0.2.0",
39037
+ version: "0.3.0",
39033
39038
  type: "module",
39034
39039
  bin: {
39035
39040
  "obenor-mcp": "bin/obenor-mcp.mjs"
@@ -39144,6 +39149,8 @@ var ModelSlotClient = class {
39144
39149
  owed = null;
39145
39150
  status() {
39146
39151
  return {
39152
+ publishedVersion: this.publishedVersion,
39153
+ statusError: this.statusError,
39147
39154
  model: this.attached === null ? null : { id: this.attached.modelId, name: this.attached.modelName },
39148
39155
  error: this.failure,
39149
39156
  inFlight: this.pending > 0,
@@ -39160,6 +39167,8 @@ var ModelSlotClient = class {
39160
39167
  sessionOf() {
39161
39168
  return this.attached?.sessionId ?? null;
39162
39169
  }
39170
+ publishedVersion = null;
39171
+ statusError = null;
39163
39172
  /**
39164
39173
  * The app's document, as the slot holds it now.
39165
39174
  *
@@ -39282,6 +39291,8 @@ var ModelSlotClient = class {
39282
39291
  );
39283
39292
  } catch {
39284
39293
  }
39294
+ this.close();
39295
+ this.attached = null;
39285
39296
  }
39286
39297
  /**
39287
39298
  * The model this session is scoped to, attaching if it has not yet.
@@ -39491,6 +39502,8 @@ var ModelSlotClient = class {
39491
39502
  return;
39492
39503
  }
39493
39504
  this.failure = null;
39505
+ const receipt = await response.json();
39506
+ if (typeof receipt.version === "number") this.publishedVersion = receipt.version;
39494
39507
  } catch (error2) {
39495
39508
  this.failure = `publish failed: ${message(error2)}`;
39496
39509
  }
@@ -39604,15 +39617,20 @@ var ModelSlotClient = class {
39604
39617
  headers: this.headers(),
39605
39618
  signal: AbortSignal.timeout(1e4)
39606
39619
  });
39607
- if (!response.ok) return { head: null, wantsSolid: false };
39620
+ if (!response.ok) {
39621
+ this.statusError = await describe(response, "read model status");
39622
+ return { head: null, wantsSolid: false };
39623
+ }
39608
39624
  const body = await response.json();
39625
+ this.statusError = null;
39609
39626
  return {
39610
39627
  head: typeof body.head === "number" ? body.head : null,
39611
39628
  // Absent reads as false, which is a Worker deployed before the field
39612
39629
  // existed and is the previous behaviour exactly.
39613
39630
  wantsSolid: body.wantsSolid === true
39614
39631
  };
39615
- } catch {
39632
+ } catch (error2) {
39633
+ this.statusError = `Could not read model status: ${message(error2)}`;
39616
39634
  return { head: null, wantsSolid: false };
39617
39635
  }
39618
39636
  }
@@ -78018,14 +78036,14 @@ async function ensureCurrentKernel() {
78018
78036
  const built = host.info();
78019
78037
  kernelReloaded = `Picked up a rebuilt kernel (${built.version} on OpenCASCADE ${built.occtVersion}) and replayed the model against it.`;
78020
78038
  }
78021
- function rebuild() {
78039
+ function rebuild(publishChanges = true) {
78022
78040
  lastResult = doc.rebuild();
78023
78041
  invalidateSoundness(lastResult);
78024
78042
  if (lastResult.ok) {
78025
78043
  host.checkpoint(doc.toJSON());
78026
78044
  void autosave();
78027
78045
  }
78028
- void publish();
78046
+ if (publishChanges) void publish();
78029
78047
  return lastResult;
78030
78048
  }
78031
78049
  var BRIDGE = process.env["OBENOR_BRIDGE"] ?? "http://localhost:5177";
@@ -78174,8 +78192,11 @@ This change is held. ${when}`;
78174
78192
  }
78175
78193
  if (v2.error !== null) return `Not published to the app: ${v2.error}`;
78176
78194
  if (v2.model !== null) {
78195
+ if (v2.publishedVersion === null) {
78196
+ return `Connected to "${v2.model.name}". No version published by this connection yet.` + namePrompt(v2.model.name);
78197
+ }
78177
78198
  const note = bakeNote();
78178
- return `Published to "${v2.model.name}" - set Follow to Live in the app to watch it.` + (note === null ? "" : `
78199
+ return `Published to "${v2.model.name}" as v${v2.publishedVersion} - set Follow to Live in the app to watch it.` + (note === null ? "" : `
78179
78200
  ${note}`) + namePrompt(v2.model.name);
78180
78201
  }
78181
78202
  return "Connecting to the app with OBENOR_LINK - the next reply will say which model.";
@@ -78268,10 +78289,7 @@ function soundness(featureId) {
78268
78289
  } catch {
78269
78290
  return [];
78270
78291
  }
78271
- const minEdge = defaultMinEdge(topology);
78272
- const defective = topology.edges.some((e) => e.faceIndices.length > 2 || !e.seam && (e.faceIndices.length === 1 || e.length < minEdge));
78273
- if (!defective) return [];
78274
- return summariseSoundness(topology, { minEdge, maxGroups: 2, maxLines: 14 });
78292
+ return reviewSoundness(topology);
78275
78293
  }
78276
78294
  var soundnessCache = /* @__PURE__ */ new Map();
78277
78295
  function invalidateSoundness(result) {
@@ -83262,6 +83280,32 @@ server.registerTool("mirror", {
83262
83280
  )
83263
83281
  }
83264
83282
  }, async ({ action }) => {
83283
+ if (slot2 !== null && action === "status") {
83284
+ await slot2.ensureModel();
83285
+ const state = slot2.status();
83286
+ return { content: [{ type: "text", text: mirrorNote() }], structuredContent: {
83287
+ connection: state.model === null ? "disconnected" : "connected",
83288
+ model: state.model,
83289
+ publication: state.deferred ? "queued" : state.inFlight ? "publishing" : state.error !== null ? "failed" : state.publishedVersion === null ? "idle" : "published",
83290
+ publishedVersion: state.publishedVersion,
83291
+ error: state.error,
83292
+ /* Reported and not gated on - see `SlotStatus.statusError`. A poll that
83293
+ * could not read the app's HEAD is worth saying out loud, because it is
83294
+ * why `pull` may answer with nothing, and it is NOT a reason to refuse
83295
+ * to leave this model. */
83296
+ statusError: state.statusError
83297
+ } };
83298
+ }
83299
+ if (slot2 !== null && action === "pull") {
83300
+ const pulled = await slot2.pullIfNewer(-1);
83301
+ if (pulled === null) return failure(slot2.status().error ?? "The connected model has no published document.");
83302
+ host.reset(pulled.document);
83303
+ knownVersion = pulled.version;
83304
+ appAhead = false;
83305
+ undoStack.length = 0;
83306
+ redoStack.length = 0;
83307
+ return report(`Loaded published version ${pulled.version}.`, void 0, rebuild(false));
83308
+ }
83265
83309
  if (action === "push") {
83266
83310
  await publishNow();
83267
83311
  return text(slot2 !== null ? mirrorNote() : publishFailed ? `Nothing is listening at ${BRIDGE}. Start the app with \`pnpm dev\`.` : "Pushed. Turn on Follow in the app to see it.");
@@ -83621,13 +83665,16 @@ server.registerTool("connect_model", {
83621
83665
  }, async ({ link }) => {
83622
83666
  const config2 = linkConfig(link);
83623
83667
  if (typeof config2 === "string") return failure(config2);
83668
+ if (slot2 !== null && slotSettings?.origin === config2.origin && slotSettings.token === config2.token) {
83669
+ const model = await slot2.ensureModel();
83670
+ if (model === null) return failure(slot2.status().error ?? "The connection could not be resumed.");
83671
+ return text(`Already connected to "${slot2.status().model?.name}". ${mirrorNote()}`);
83672
+ }
83673
+ await slot2?.flush();
83674
+ if (slot2?.status().error) return failure(`Could not finish the current model's publish: ${slot2.status().error}`);
83675
+ await slot2?.end();
83624
83676
  const next = new ModelSlotClient({ ...config2, collapse: collapseForShare });
83625
- const first = baking && lastResult !== null && lastResult.bodies.length > 0 ? await bakeOnce(lastResult.bodies) : null;
83626
- await next.publish(
83627
- doc.toJSON(),
83628
- void 0,
83629
- first === null ? void 0 : { poster: first.poster, lod2: first.lod2, lod0: first.lod0 }
83630
- );
83677
+ await next.ensureModel();
83631
83678
  const status = next.status();
83632
83679
  if (status.model === null) {
83633
83680
  next.close();
@@ -83635,10 +83682,31 @@ server.registerTool("connect_model", {
83635
83682
  `That link did not connect: ${status.error ?? "the app refused it, without saying why"}`
83636
83683
  );
83637
83684
  }
83638
- await slot2?.flush();
83685
+ const pulled = await next.pullIfNewer(-1);
83686
+ if (next.status().error !== null) {
83687
+ const error2 = next.status().error;
83688
+ await next.end();
83689
+ return failure(`Connected, but could not read the model: ${error2}`);
83690
+ }
83691
+ const previous = doc.toJSON();
83692
+ try {
83693
+ host.reset(pulled === null ? null : pulled.document);
83694
+ rebuild(false);
83695
+ } catch (error2) {
83696
+ host.reset(previous);
83697
+ rebuild(false);
83698
+ await next.end();
83699
+ return failure(`The model could not be loaded: ${String(error2)}`);
83700
+ }
83639
83701
  slot2?.close();
83640
83702
  slot2 = next;
83641
83703
  slotSettings = config2;
83704
+ slotMisconfigured = null;
83705
+ knownVersion = pulled?.version ?? 0;
83706
+ appAhead = false;
83707
+ undoStack.length = 0;
83708
+ redoStack.length = 0;
83709
+ namePromptSaid = false;
83642
83710
  appLink?.close();
83643
83711
  appLink = null;
83644
83712
  return {
@@ -83646,6 +83714,7 @@ server.registerTool("connect_model", {
83646
83714
  type: "text",
83647
83715
  text: [
83648
83716
  `Connected to "${status.model.name}" at ${config2.origin}.`,
83717
+ `Loaded ${pulled === null ? "the empty model" : `version ${pulled.version}`}. Connecting did not publish a version.`,
83649
83718
  /*
83650
83719
  * The attach can succeed and the first publish still be refused, and
83651
83720
  * the commonest reason is the good one: this session is empty and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obenor/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "obenor-mcp": "bin/obenor-mcp.mjs"