@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.
- package/bundle/kernel-worker.mjs +1 -1
- package/bundle/server.mjs +87 -18
- package/package.json +1 -1
package/bundle/kernel-worker.mjs
CHANGED
|
@@ -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.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|