@happyvertical/smrt-web 0.46.0 → 0.47.1
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/AGENTS.md +61 -9
- package/README.md +24 -1
- package/dist/chunks/{src-D1ZtD6Bt.js → src-DHH8ptH8.js} +11 -2
- package/dist/chunks/{src-D1ZtD6Bt.js.map → src-DHH8ptH8.js.map} +1 -1
- package/dist/index.d.ts +154 -0
- package/dist/index.js +3 -2
- package/dist/webmcp-tool-names.d.ts +112 -0
- package/dist/webmcp-tool-names.js +73 -0
- package/dist/webmcp-tool-names.js.map +1 -0
- package/dist/webmcp.d.ts +154 -0
- package/dist/webmcp.js +3 -2
- package/package.json +8 -4
package/AGENTS.md
CHANGED
|
@@ -40,6 +40,7 @@ module you are editing. This file keeps what holds in every module.
|
|
|
40
40
|
| `sse-client.ts` | the client half of live cache invalidation — the app-wide subscriber, the wire contract it consumes, and SSE-vs-polling behaviour | [agents/live-invalidation.md](agents/live-invalidation.md) |
|
|
41
41
|
| `webmcp.ts` | framework-agnostic WebMCP registrar; validates an optionally bounded/namespaced, effect-filtered prospective set atomically, keeps legacy list-backed tools on collection state, and executes canonical tool-only definitions directly through REST fetchers. `registerWebMcpBespokeTool` is the single-tool sibling a UI layer's hand-written component tool routes through (`useWebMcpTool` in smrt-svelte, #2586): same fail-closed effect classification and `effects` policy, no `namespace`/`maxTools` | — |
|
|
42
42
|
| `persistence/` + `update-state.ts` | the read-cache rehydrate capability and the framework-free `updateAvailable` primitive (bundle + contract signals) | [agents/version-persistence.md](agents/version-persistence.md) |
|
|
43
|
+
| `webmcp-tool-names.ts` | the document-global tool-name lock every registration path reserves through (#2613). Dependency-free; ships as `@happyvertical/smrt-web/webmcp-tool-names` | see below |
|
|
43
44
|
| `intents.ts` + `capability-classification.ts` | the framework-agnostic declarative view-intent contract, its declaration registry, and the one capability classification rule both it and `webmcp.ts` apply (#2587, #2588). Ships as the dependency-free `@happyvertical/smrt-web/intents` entry — importing it pulls no client-data engine | — |
|
|
44
45
|
| `data-query.ts` | dependency-free browser mirror and defensive response normalizer for the canonical bounded data-query envelope (#2444) | — |
|
|
45
46
|
| `remote-query.ts` | query-shaped remote pages over a `SmrtWebCollection`, with keyed stale cache, execution modes, cancellation/latest-query-wins, and optional query-scoped live subscriptions (#2445) | — |
|
|
@@ -172,15 +173,13 @@ declared intent derives, at the one place both are visible. Two such intents
|
|
|
172
173
|
would otherwise fight over a single WebMCP tool name at mount, where the
|
|
173
174
|
failure is a shadowed or rejected registration rather than a clear error.
|
|
174
175
|
|
|
175
|
-
**
|
|
176
|
-
|
|
177
|
-
`webmcp.ui.prefix`, with one of the six fixed UI tools
|
|
178
|
-
declaration time
|
|
179
|
-
declaration never sees
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
shadows or loses to the other. Keep an intent's first id segment out of your
|
|
183
|
-
model names, or give `registerWebMcpTools` a `namespace`.
|
|
176
|
+
**That check covers intents only** — a derived name can still collide with a
|
|
177
|
+
GENERATED model tool (`${model}_${action}`) or, under a custom
|
|
178
|
+
`webmcp.ui.prefix`, with one of the six fixed UI tools, and neither is
|
|
179
|
+
knowable at declaration time because both depend on a runtime
|
|
180
|
+
`namespace`/`prefix` the declaration never sees. Those cross-path collisions
|
|
181
|
+
are caught at REGISTRATION instead, by the document-global tool-name lock
|
|
182
|
+
below (#2613).
|
|
184
183
|
|
|
185
184
|
### The no-REST invariant
|
|
186
185
|
|
|
@@ -195,6 +194,59 @@ tool's `execute` is then CONSTRUCTED by `compileViewIntentToolSpec` from
|
|
|
195
194
|
declaration, it must be data, and it must be reachable by the scanner without
|
|
196
195
|
evaluation.
|
|
197
196
|
|
|
197
|
+
## The document-global tool-name lock (#2613)
|
|
198
|
+
|
|
199
|
+
`webmcp-tool-names.ts` is the reservation table the three name-deriving paths
|
|
200
|
+
coordinate through, and ships as the dependency-free
|
|
201
|
+
`@happyvertical/smrt-web/webmcp-tool-names` entry so a UI layer can reserve
|
|
202
|
+
its own names without pulling the client-data engine.
|
|
203
|
+
|
|
204
|
+
The three paths do NOT share one registrar — verify this before assuming a
|
|
205
|
+
funnel. `registerWebMcpTools` and `registerWebMcpUiTools` (in
|
|
206
|
+
`@happyvertical/smrt-svelte`) each call `document.modelContext.registerTool`
|
|
207
|
+
directly; only intents and `useWebMcpTool` route through
|
|
208
|
+
`registerWebMcpBespokeTool`. So each path reserves for itself:
|
|
209
|
+
|
|
210
|
+
| Path | Owner | Reserves in |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| generated model tools | `generated` | `registerWebMcpTools`, after selection/budget validation, before the first `registerTool` |
|
|
213
|
+
| the six fixed `smrt_ui_*` tools | `ui` | `registerWebMcpUiTools` (`smrt-svelte`), after the prefix lock |
|
|
214
|
+
| declared view intents | `intent` | `registerWebMcpBespokeTool`'s shared body — via `registerViewIntent`, or via `registerWebMcpBespokeTool` with `owner: 'intent'` |
|
|
215
|
+
| bespoke `useWebMcpTool` tools | `bespoke` | the same shared body (the default owner) |
|
|
216
|
+
|
|
217
|
+
Reservation is all-or-nothing and rejects a duplicate SYNCHRONOUSLY with
|
|
218
|
+
`WebMcpToolNameCollisionError`, which carries the colliding name plus the
|
|
219
|
+
owner holding it and the owner that asked. Previously the host rejected the
|
|
220
|
+
later registration and the tool was silently absent. **This is a behavior
|
|
221
|
+
change**: `registerWebMcpBespokeTool` and `registerViewIntent` now throw where
|
|
222
|
+
they used to succeed-then-lose-the-tool.
|
|
223
|
+
|
|
224
|
+
Rules for anything that adds a fourth path or edits an existing one:
|
|
225
|
+
|
|
226
|
+
- **The table is keyed on the `document`, not on module state.** A registrar
|
|
227
|
+
with an injectable document must pass the SAME object it reads
|
|
228
|
+
`modelContext` from, or its reservations land in a different table. Module
|
|
229
|
+
state would fragment across bundle chunks, a duplicated dependency, and HMR;
|
|
230
|
+
the table is stored on the document under a `Symbol.for` key so every copy
|
|
231
|
+
of the module resolves to one slot. It is stamped with the `modelContext` it
|
|
232
|
+
was built for and resets when the host installs a new one, so tools the old
|
|
233
|
+
registry held never strand their names.
|
|
234
|
+
- **Dispose must release**, or a mount/unmount cycle wedges the name
|
|
235
|
+
permanently — the failure mode #2595 already hit as a re-registration race.
|
|
236
|
+
A reservation releases only names it still holds.
|
|
237
|
+
- **A tool the effects policy excluded reserves nothing**: it was never
|
|
238
|
+
registered, so its name stays available.
|
|
239
|
+
- **A binding that compiles an intent itself must pass `owner: 'intent'`.**
|
|
240
|
+
`registerViewIntent` is not the shipped intent path: `useViewIntent` in
|
|
241
|
+
`@happyvertical/smrt-svelte` compiles the spec and hands it to
|
|
242
|
+
`useWebMcpTool`, because that package must not duplicate the WebMCP
|
|
243
|
+
lifecycle. Without the explicit owner every intent collision would report
|
|
244
|
+
`bespoke` and send an author looking for a `useWebMcpTool` call that does
|
|
245
|
+
not exist. The label is a diagnostic only — it grants nothing.
|
|
246
|
+
- Consumer app code that registers directly against `document.modelContext`
|
|
247
|
+
(for example `packages/template-sveltekit`'s runtime-diagnostics tools) does
|
|
248
|
+
not participate and can still collide at the host.
|
|
249
|
+
|
|
198
250
|
## Reference consumer
|
|
199
251
|
|
|
200
252
|
`packages/products` consumes the runtime as its reference store across npm,
|
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ core.
|
|
|
131
131
|
| Persistence | `persistCollection`, `wipeDurableStore` |
|
|
132
132
|
| Live updates | `createSmrtWebEventSubscriber`, `liveInvalidation` |
|
|
133
133
|
| Version awareness | `createUpdateState` |
|
|
134
|
-
| WebMCP | `registerWebMcpTools` |
|
|
134
|
+
| WebMCP | `registerWebMcpTools`, `registerWebMcpBespokeTool`, `registerViewIntent`, `reserveWebMcpToolNames` |
|
|
135
135
|
|
|
136
136
|
## WebMCP capability exposure
|
|
137
137
|
|
|
@@ -191,6 +191,29 @@ collections. Their duplicate tool names or collection/action identities reject
|
|
|
191
191
|
the registration atomically. Prefer the canonical set for complete generated
|
|
192
192
|
coverage, or compose only disjoint legacy and canonical subsets.
|
|
193
193
|
|
|
194
|
+
### Tool names are locked per document
|
|
195
|
+
|
|
196
|
+
Generated model tools, the six fixed `smrt_ui_*` tools a UI layer registers
|
|
197
|
+
under its own prefix, declared view intents, and hand-written bespoke tools all
|
|
198
|
+
derive names independently and all end at one `document.modelContext`. None can
|
|
199
|
+
see the others at declaration time, because a `namespace` and a UI `prefix` are
|
|
200
|
+
runtime values.
|
|
201
|
+
|
|
202
|
+
Every path reserves its names through a document-global lock before the browser
|
|
203
|
+
sees them, so a cross-path collision is refused synchronously with a
|
|
204
|
+
`WebMcpToolNameCollisionError` naming the tool and the owner holding it
|
|
205
|
+
(`generated`, `ui`, `intent`, or `bespoke`). The browser used to reject the
|
|
206
|
+
later registration and the tool was simply absent.
|
|
207
|
+
|
|
208
|
+
Reservation is all-or-nothing, and disposing a registration releases its names,
|
|
209
|
+
so a mount / unmount / remount cycle under one name keeps working. A UI layer
|
|
210
|
+
that registers its own fixed tool set reserves through the dependency-free
|
|
211
|
+
`@happyvertical/smrt-web/webmcp-tool-names` entry, passing the same `document`
|
|
212
|
+
it reads `modelContext` from. A binding that compiles a declared view intent
|
|
213
|
+
itself and registers it with `registerWebMcpBespokeTool` — rather than through
|
|
214
|
+
`registerViewIntent` — passes `owner: 'intent'` so the diagnostic names the
|
|
215
|
+
intent. The owner is a label only; it grants nothing.
|
|
216
|
+
|
|
194
217
|
WebMCP policy controls which capabilities a page advertises; it is not an
|
|
195
218
|
authorization boundary. Execution still uses the page's authenticated REST
|
|
196
219
|
transport, whose auth, tenancy, writable-field, and sensitive-field guards must
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { c as capabilityAnnotations, l as resolveDeclaredCapability, n as compileViewIntentToolSpec, s as WEBMCP_TOOL_EFFECTS } from "./intents-BUTyN7YQ.js";
|
|
2
|
+
import { reserveWebMcpToolNames } from "../webmcp-tool-names.js";
|
|
2
3
|
import { createCollection } from "@tanstack/db";
|
|
3
4
|
import { QueryClient } from "@tanstack/query-core";
|
|
4
5
|
import { queryCollectionOptions } from "@tanstack/query-db-collection";
|
|
@@ -2127,11 +2128,13 @@ function registerWebMcpTools(definitions, options = {}) {
|
|
|
2127
2128
|
const controller = new AbortController();
|
|
2128
2129
|
const collections = /* @__PURE__ */ new Map();
|
|
2129
2130
|
const collectionFetchers = /* @__PURE__ */ new Map();
|
|
2131
|
+
const reservation = reserveWebMcpToolNames(tools.map((tool) => tool.name), "generated");
|
|
2130
2132
|
let disposed = false;
|
|
2131
2133
|
const dispose = () => {
|
|
2132
2134
|
if (disposed) return;
|
|
2133
2135
|
disposed = true;
|
|
2134
2136
|
controller.abort();
|
|
2137
|
+
reservation.release();
|
|
2135
2138
|
for (const collection of collections.values()) collection.cleanup().catch(() => void 0);
|
|
2136
2139
|
};
|
|
2137
2140
|
const registrations = [];
|
|
@@ -2196,16 +2199,22 @@ function registrationDisposer(dispose, ready) {
|
|
|
2196
2199
|
return Object.assign(dispose, { ready });
|
|
2197
2200
|
}
|
|
2198
2201
|
function registerWebMcpBespokeTool(spec, options = {}) {
|
|
2202
|
+
return registerSingleTool(spec, options, options.owner ?? "bespoke");
|
|
2203
|
+
}
|
|
2204
|
+
function registerSingleTool(spec, options, owner) {
|
|
2199
2205
|
const { allowedEffects } = validateExposurePolicy(options);
|
|
2200
2206
|
const ctx = getModelContext();
|
|
2201
2207
|
if (!ctx) return registrationDisposer(() => {}, Promise.resolve());
|
|
2202
2208
|
const semantics = actionSemantics("bespoke", bespokeDeclaredSemantics(spec.annotations));
|
|
2203
2209
|
if (!allowedEffects.has(semantics.effect)) return registrationDisposer(() => {}, Promise.resolve());
|
|
2210
|
+
const reservation = reserveWebMcpToolNames([spec.name], owner);
|
|
2204
2211
|
const controller = new AbortController();
|
|
2205
2212
|
let disposed = false;
|
|
2206
2213
|
const dispose = () => {
|
|
2214
|
+
if (disposed) return;
|
|
2207
2215
|
disposed = true;
|
|
2208
2216
|
controller.abort();
|
|
2217
|
+
reservation.release();
|
|
2209
2218
|
};
|
|
2210
2219
|
let registration;
|
|
2211
2220
|
try {
|
|
@@ -2231,7 +2240,7 @@ function registerWebMcpBespokeTool(spec, options = {}) {
|
|
|
2231
2240
|
return registrationDisposer(dispose, ready);
|
|
2232
2241
|
}
|
|
2233
2242
|
function registerViewIntent(intent, binding, options = {}) {
|
|
2234
|
-
return
|
|
2243
|
+
return registerSingleTool(compileViewIntentToolSpec(intent, binding), options, "intent");
|
|
2235
2244
|
}
|
|
2236
2245
|
var VALID_EFFECTS = WEBMCP_TOOL_EFFECTS;
|
|
2237
2246
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
@@ -3030,4 +3039,4 @@ function createSmrtCollection(definition, options) {
|
|
|
3030
3039
|
//#endregion
|
|
3031
3040
|
export { MAX_SMRT_WEB_DATA_QUERY_OFFSET as A, offlineOutbox as C, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as D, wipeDurableStore as E, MAX_SMRT_WEB_DATA_QUERY_WARNINGS as F, executeSmrtWebDataQuery as I, normalizeSmrtWebDataQueryResult as L, MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as M, MAX_SMRT_WEB_DATA_QUERY_ROWS as N, MAX_SMRT_WEB_DATA_QUERY_FACETS as O, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as P, runWrapMutation as R, getOutboxHandle as S, registerDurableResource as T, createSmrtWebEventSubscriber as _, createSmrtWebClient as a, DEFAULT_PERSIST_DEBOUNCE_MS as b, newLocalId as c, unwrapListResult as d, validateSmrtWebClient as f, createUpdateState as g, registerWebMcpTools as h, createSmrtCollection as i, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as j, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES as k, throwIfSmrtWebError as l, registerWebMcpBespokeTool as m, buildListQuery as n, getEngineCollection as o, registerViewIntent as p, createDefinitionFetchers as r, invalidateSmrtWebCollections as s, SmrtWebRequestError as t, unwrapItemResult as u, liveInvalidation as v, durableStoreNamespace as w, persistCollection as x, createSmrtWebQuery as y };
|
|
3032
3041
|
|
|
3033
|
-
//# sourceMappingURL=src-
|
|
3042
|
+
//# sourceMappingURL=src-DHH8ptH8.js.map
|