@happyvertical/smrt-web 0.45.3 → 0.47.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/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/dist/index.d.ts
CHANGED
|
@@ -565,6 +565,16 @@ export declare function registerDurableResource(namespace: string, resource: Dur
|
|
|
565
565
|
* `compileViewIntentToolSpec` from `intent.target`. No author-supplied code
|
|
566
566
|
* runs, and the only thing it can do is dispatch one browser registry
|
|
567
567
|
* command — the runtime half of the no-REST invariant.
|
|
568
|
+
*
|
|
569
|
+
* The intent's derived tool name is claimed against the document-global
|
|
570
|
+
* tool-name lock (#2613), so an id whose flattened name collides with a
|
|
571
|
+
* generated model tool or a fixed UI tool throws
|
|
572
|
+
* {@link WebMcpToolNameCollisionError} here rather than silently losing the
|
|
573
|
+
* tool at the host. `defineIntent`'s own intent-vs-intent check cannot see
|
|
574
|
+
* either, because both depend on a runtime `namespace` / `prefix` the
|
|
575
|
+
* declaration never sees.
|
|
576
|
+
*
|
|
577
|
+
* @throws {WebMcpToolNameCollisionError} when the derived name is already held.
|
|
568
578
|
*/
|
|
569
579
|
export declare function registerViewIntent(intent: ViewIntent, binding: ViewIntentBinding, options?: RegisterWebMcpBespokeToolOptions): WebMcpRegistrationDisposer;
|
|
570
580
|
|
|
@@ -578,9 +588,19 @@ export declare function registerViewIntent(intent: ViewIntent, binding: ViewInte
|
|
|
578
588
|
* `maxTools` are out of scope for a bespoke tool; see
|
|
579
589
|
* {@link RegisterWebMcpBespokeToolOptions}.
|
|
580
590
|
*
|
|
591
|
+
* The tool's name is also claimed against the
|
|
592
|
+
* document-global tool-name lock before the browser sees it. A name a
|
|
593
|
+
* generated model tool, a fixed UI tool, or another live bespoke tool /
|
|
594
|
+
* intent already holds throws {@link WebMcpToolNameCollisionError}
|
|
595
|
+
* SYNCHRONOUSLY, naming the colliding name and the owner that holds it.
|
|
596
|
+
* Previously the host accepted the call and silently dropped the losing
|
|
597
|
+
* registration. Disposing releases the name, so register/dispose/re-register
|
|
598
|
+
* under one name still succeeds.
|
|
599
|
+
*
|
|
581
600
|
* @returns a disposer that deregisters the tool this call registered (a
|
|
582
601
|
* no-op double-call). On a browser without WebMCP, or when policy excludes
|
|
583
602
|
* the tool's effect, the call is a no-op and the disposer is inert.
|
|
603
|
+
* @throws {WebMcpToolNameCollisionError} when the name is already held.
|
|
584
604
|
*/
|
|
585
605
|
export declare function registerWebMcpBespokeTool(spec: WebMcpBespokeToolSpec, options?: RegisterWebMcpBespokeToolOptions): WebMcpRegistrationDisposer;
|
|
586
606
|
|
|
@@ -593,13 +613,36 @@ export declare interface RegisterWebMcpBespokeToolOptions {
|
|
|
593
613
|
* an unrelated generated tool set fail to register.
|
|
594
614
|
*/
|
|
595
615
|
effects?: readonly WebMcpToolEffect[];
|
|
616
|
+
/**
|
|
617
|
+
* Which path this registration belongs to, used ONLY to label its
|
|
618
|
+
* tool-name reservation (#2613) so a later collision names the right
|
|
619
|
+
* source. Defaults to `bespoke`.
|
|
620
|
+
*
|
|
621
|
+
* A UI-framework binding that compiles a declared view intent itself and
|
|
622
|
+
* registers it here — rather than through {@link registerViewIntent} —
|
|
623
|
+
* must pass `'intent'`. `useViewIntent` in `@happyvertical/smrt-svelte`
|
|
624
|
+
* does exactly that, because reusing `useWebMcpTool`'s single WebMCP
|
|
625
|
+
* lifecycle is a documented invariant of that package; without this it
|
|
626
|
+
* would be the only shipped intent path and every intent collision would
|
|
627
|
+
* blame a `useWebMcpTool` call that does not exist.
|
|
628
|
+
*
|
|
629
|
+
* Purely a diagnostic label: it grants no capability, narrows no policy,
|
|
630
|
+
* and changes nothing about how the tool registers or executes.
|
|
631
|
+
*/
|
|
632
|
+
owner?: Extract<WebMcpToolNameOwner, 'intent' | 'bespoke'>;
|
|
596
633
|
}
|
|
597
634
|
|
|
598
635
|
/**
|
|
599
636
|
* Register every collection's generated tool descriptors with WebMCP.
|
|
600
637
|
*
|
|
638
|
+
* Every selected tool name is claimed against the document-global tool-name
|
|
639
|
+
* lock (#2613) before the first browser registration, so a collision with a
|
|
640
|
+
* UI-layer or bespoke tool throws {@link WebMcpToolNameCollisionError} with
|
|
641
|
+
* the offending name and its current owner instead of reaching the host.
|
|
642
|
+
*
|
|
601
643
|
* @returns a disposer that deregisters all tools this call registered. On a
|
|
602
644
|
* browser without WebMCP the call is a no-op and the disposer is inert.
|
|
645
|
+
* @throws {WebMcpToolNameCollisionError} when a selected name is already held.
|
|
603
646
|
*/
|
|
604
647
|
export declare function registerWebMcpTools(definitions: readonly WebMcpRegistrationDefinition[], options?: RegisterWebMcpToolsOptions): WebMcpRegistrationDisposer;
|
|
605
648
|
|
|
@@ -630,6 +673,34 @@ export declare interface RegisterWebMcpToolsOptions extends WebMcpExposurePolicy
|
|
|
630
673
|
filterTool?: (definition: WebMcpToolDefinition) => boolean;
|
|
631
674
|
}
|
|
632
675
|
|
|
676
|
+
/**
|
|
677
|
+
* Reserve `names` for `owner` against the document's WebMCP registry.
|
|
678
|
+
*
|
|
679
|
+
* Reservation is ALL-OR-NOTHING: if any name is already held — by this path
|
|
680
|
+
* or another — nothing is reserved and a {@link WebMcpToolNameCollisionError}
|
|
681
|
+
* naming the colliding name and its current owner is thrown synchronously,
|
|
682
|
+
* before any tool reaches `document.modelContext`. Duplicates within `names`
|
|
683
|
+
* itself collide the same way.
|
|
684
|
+
*
|
|
685
|
+
* Off-WebMCP (no document) the call is a no-op returning an inert
|
|
686
|
+
* reservation, matching every registrar's own no-op guarantee.
|
|
687
|
+
*
|
|
688
|
+
* A reservation releases only the names it took: `release()` never drops a
|
|
689
|
+
* name a later reservation has since acquired, so a dispose that lands after
|
|
690
|
+
* a same-name re-registration cannot revoke the new holder's claim.
|
|
691
|
+
*/
|
|
692
|
+
export declare function reserveWebMcpToolNames(names: readonly string[], owner: WebMcpToolNameOwner, options?: ReserveWebMcpToolNamesOptions): WebMcpToolNameReservation;
|
|
693
|
+
|
|
694
|
+
declare interface ReserveWebMcpToolNamesOptions {
|
|
695
|
+
/**
|
|
696
|
+
* The document whose `modelContext` the names are registered against.
|
|
697
|
+
* Defaults to `globalThis.document`. Registrars that accept an injectable
|
|
698
|
+
* document (tests, non-window hosts) must pass the SAME object they read
|
|
699
|
+
* `modelContext` from, or their reservations land in a different table.
|
|
700
|
+
*/
|
|
701
|
+
document?: unknown;
|
|
702
|
+
}
|
|
703
|
+
|
|
633
704
|
/**
|
|
634
705
|
* Run the `wrapMutation` hook across `capabilities` in array order for one
|
|
635
706
|
* mutation, short-circuiting on the FIRST capability that returns `{ handled:
|
|
@@ -1667,6 +1738,89 @@ export declare interface WebMcpToolDefinition extends WebToolDescriptor {
|
|
|
1667
1738
|
*/
|
|
1668
1739
|
export declare type WebMcpToolEffect = 'read' | 'write' | 'destructive';
|
|
1669
1740
|
|
|
1741
|
+
/**
|
|
1742
|
+
* Thrown synchronously when a name is already held. Carries the colliding
|
|
1743
|
+
* name and the owner that holds it so a host can report both without parsing
|
|
1744
|
+
* the message.
|
|
1745
|
+
*/
|
|
1746
|
+
export declare class WebMcpToolNameCollisionError extends Error {
|
|
1747
|
+
readonly toolName: string;
|
|
1748
|
+
/** The path that already holds {@link toolName}. */
|
|
1749
|
+
readonly owner: WebMcpToolNameOwner;
|
|
1750
|
+
/** The path that tried to take it. */
|
|
1751
|
+
readonly requestedBy: WebMcpToolNameOwner;
|
|
1752
|
+
constructor(toolName: string,
|
|
1753
|
+
/** The path that already holds {@link toolName}. */
|
|
1754
|
+
owner: WebMcpToolNameOwner,
|
|
1755
|
+
/** The path that tried to take it. */
|
|
1756
|
+
requestedBy: WebMcpToolNameOwner);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Document-global WebMCP tool-name lock (#2613).
|
|
1761
|
+
*
|
|
1762
|
+
* A browser tool name is derived independently on three paths that all end at
|
|
1763
|
+
* the same `document.modelContext`:
|
|
1764
|
+
*
|
|
1765
|
+
* 1. **generated** model tools — `<namespace>_<model>_<action>`, built by
|
|
1766
|
+
* `registerWebMcpTools` from the exposure policy;
|
|
1767
|
+
* 2. **ui** — the six fixed `smrt_ui_*` tools a UI layer registers under a
|
|
1768
|
+
* configurable prefix (`registerWebMcpUiTools` in `@happyvertical/smrt-svelte`);
|
|
1769
|
+
* 3. **intent** / **bespoke** — declared view intents (#2588) and hand-written
|
|
1770
|
+
* `useWebMcpTool` tools, whose names come from the declaration `id` or the
|
|
1771
|
+
* spec `name`.
|
|
1772
|
+
*
|
|
1773
|
+
* None of the three can see the others at declaration time: a namespace and a
|
|
1774
|
+
* UI prefix are runtime values a declaration never sees. `registerWebMcpTools`
|
|
1775
|
+
* rejects duplicates WITHIN its own prospective set, but nothing checked
|
|
1776
|
+
* ACROSS the paths, so a collision reached the host — which rejects the later
|
|
1777
|
+
* registration and leaves the tool silently absent.
|
|
1778
|
+
*
|
|
1779
|
+
* This module is the shared reservation table those paths coordinate through.
|
|
1780
|
+
* It is an AVAILABILITY guard, not a trust boundary: every path stays
|
|
1781
|
+
* fail-closed and browser-only regardless, and a collision loses a tool rather
|
|
1782
|
+
* than granting one. The authenticated REST surface remains the auth, tenant,
|
|
1783
|
+
* and field-write boundary.
|
|
1784
|
+
*
|
|
1785
|
+
* ## Why the table lives on the document, not in this module
|
|
1786
|
+
*
|
|
1787
|
+
* The lock must be shared by every registrar reachable from one page, which
|
|
1788
|
+
* spans package boundaries (`smrt-web` and a UI layer) and therefore possibly
|
|
1789
|
+
* more than one copy of this module — a second bundle chunk, a duplicated
|
|
1790
|
+
* dependency, or an HMR reload that replaces module state while the host
|
|
1791
|
+
* registry keeps every tool it already accepted. Module-level state loses the
|
|
1792
|
+
* reservations in all of those cases, and a lost reservation is worse than no
|
|
1793
|
+
* lock: it lets a duplicate through, or (on the release side) strands a name
|
|
1794
|
+
* forever. The table is therefore stored on the `document` object itself under
|
|
1795
|
+
* a cross-realm `Symbol.for` key, which every copy of this module resolves to
|
|
1796
|
+
* the same slot.
|
|
1797
|
+
*
|
|
1798
|
+
* The table is additionally stamped with the `modelContext` it was built for.
|
|
1799
|
+
* When a host installs a NEW model context on the same document — a fresh
|
|
1800
|
+
* origin-trial registry, or a test harness swapping the double — every tool
|
|
1801
|
+
* the old registry held is gone, so the table resets rather than stranding
|
|
1802
|
+
* those names.
|
|
1803
|
+
*
|
|
1804
|
+
* This module is dependency-free by design (see the package's dependency-DAG
|
|
1805
|
+
* guardrails) and ships as the `@happyvertical/smrt-web/webmcp-tool-names`
|
|
1806
|
+
* entry so a UI layer can reserve its fixed tool names without pulling in the
|
|
1807
|
+
* client-data engine.
|
|
1808
|
+
*/
|
|
1809
|
+
/** Which registration path holds a reserved WebMCP tool name. */
|
|
1810
|
+
export declare type WebMcpToolNameOwner = 'generated' | 'ui' | 'intent' | 'bespoke';
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* The owner currently holding `name` on this document, or undefined. For
|
|
1814
|
+
* diagnostics and tests; registration paths use
|
|
1815
|
+
* {@link reserveWebMcpToolNames}, which is atomic.
|
|
1816
|
+
*/
|
|
1817
|
+
export declare function webMcpToolNameOwner(name: string, options?: ReserveWebMcpToolNamesOptions): WebMcpToolNameOwner | undefined;
|
|
1818
|
+
|
|
1819
|
+
/** Releases every name one reservation holds. Idempotent. */
|
|
1820
|
+
export declare interface WebMcpToolNameReservation {
|
|
1821
|
+
release(): void;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1670
1824
|
/**
|
|
1671
1825
|
* One generated collection definition: everything needed to construct a client
|
|
1672
1826
|
* collection over the generated REST surface. The `_row` property is a phantom
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { A as MAX_SMRT_WEB_DATA_QUERY_OFFSET, C as offlineOutbox, D as MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS, E as wipeDurableStore, F as MAX_SMRT_WEB_DATA_QUERY_WARNINGS, I as executeSmrtWebDataQuery, L as normalizeSmrtWebDataQueryResult, M as MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES, N as MAX_SMRT_WEB_DATA_QUERY_ROWS, O as MAX_SMRT_WEB_DATA_QUERY_FACETS, P as MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH, R as runWrapMutation, S as getOutboxHandle, T as registerDurableResource, _ as createSmrtWebEventSubscriber, a as createSmrtWebClient, b as DEFAULT_PERSIST_DEBOUNCE_MS, c as newLocalId, d as unwrapListResult, f as validateSmrtWebClient, g as createUpdateState, h as registerWebMcpTools, i as createSmrtCollection, j as MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT, k as MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES, l as throwIfSmrtWebError, m as registerWebMcpBespokeTool, n as buildListQuery, o as getEngineCollection, p as registerViewIntent, r as createDefinitionFetchers, s as invalidateSmrtWebCollections, t as SmrtWebRequestError, u as unwrapItemResult, v as liveInvalidation, w as durableStoreNamespace, x as persistCollection, y as createSmrtWebQuery } from "./chunks/src-
|
|
1
|
+
import { A as MAX_SMRT_WEB_DATA_QUERY_OFFSET, C as offlineOutbox, D as MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS, E as wipeDurableStore, F as MAX_SMRT_WEB_DATA_QUERY_WARNINGS, I as executeSmrtWebDataQuery, L as normalizeSmrtWebDataQueryResult, M as MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES, N as MAX_SMRT_WEB_DATA_QUERY_ROWS, O as MAX_SMRT_WEB_DATA_QUERY_FACETS, P as MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH, R as runWrapMutation, S as getOutboxHandle, T as registerDurableResource, _ as createSmrtWebEventSubscriber, a as createSmrtWebClient, b as DEFAULT_PERSIST_DEBOUNCE_MS, c as newLocalId, d as unwrapListResult, f as validateSmrtWebClient, g as createUpdateState, h as registerWebMcpTools, i as createSmrtCollection, j as MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT, k as MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES, l as throwIfSmrtWebError, m as registerWebMcpBespokeTool, n as buildListQuery, o as getEngineCollection, p as registerViewIntent, r as createDefinitionFetchers, s as invalidateSmrtWebCollections, t as SmrtWebRequestError, u as unwrapItemResult, v as liveInvalidation, w as durableStoreNamespace, x as persistCollection, y as createSmrtWebQuery } from "./chunks/src-DHH8ptH8.js";
|
|
2
2
|
import { n as compileViewIntentToolSpec, o as viewIntentToolName } from "./chunks/intents-BUTyN7YQ.js";
|
|
3
|
-
|
|
3
|
+
import { WebMcpToolNameCollisionError, reserveWebMcpToolNames, webMcpToolNameOwner } from "./webmcp-tool-names.js";
|
|
4
|
+
export { DEFAULT_PERSIST_DEBOUNCE_MS, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS, MAX_SMRT_WEB_DATA_QUERY_FACETS, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES, MAX_SMRT_WEB_DATA_QUERY_OFFSET, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT, MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES, MAX_SMRT_WEB_DATA_QUERY_ROWS, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH, MAX_SMRT_WEB_DATA_QUERY_WARNINGS, SmrtWebRequestError, WebMcpToolNameCollisionError, buildListQuery, compileViewIntentToolSpec, createDefinitionFetchers, createSmrtCollection, createSmrtWebClient, createSmrtWebEventSubscriber, createSmrtWebQuery, createUpdateState, durableStoreNamespace, executeSmrtWebDataQuery, getEngineCollection, getOutboxHandle, invalidateSmrtWebCollections, liveInvalidation, newLocalId, normalizeSmrtWebDataQueryResult, offlineOutbox, persistCollection, registerDurableResource, registerViewIntent, registerWebMcpBespokeTool, registerWebMcpTools, reserveWebMcpToolNames, runWrapMutation, throwIfSmrtWebError, unwrapItemResult, unwrapListResult, validateSmrtWebClient, viewIntentToolName, webMcpToolNameOwner, wipeDurableStore };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserve `names` for `owner` against the document's WebMCP registry.
|
|
3
|
+
*
|
|
4
|
+
* Reservation is ALL-OR-NOTHING: if any name is already held — by this path
|
|
5
|
+
* or another — nothing is reserved and a {@link WebMcpToolNameCollisionError}
|
|
6
|
+
* naming the colliding name and its current owner is thrown synchronously,
|
|
7
|
+
* before any tool reaches `document.modelContext`. Duplicates within `names`
|
|
8
|
+
* itself collide the same way.
|
|
9
|
+
*
|
|
10
|
+
* Off-WebMCP (no document) the call is a no-op returning an inert
|
|
11
|
+
* reservation, matching every registrar's own no-op guarantee.
|
|
12
|
+
*
|
|
13
|
+
* A reservation releases only the names it took: `release()` never drops a
|
|
14
|
+
* name a later reservation has since acquired, so a dispose that lands after
|
|
15
|
+
* a same-name re-registration cannot revoke the new holder's claim.
|
|
16
|
+
*/
|
|
17
|
+
export declare function reserveWebMcpToolNames(names: readonly string[], owner: WebMcpToolNameOwner, options?: ReserveWebMcpToolNamesOptions): WebMcpToolNameReservation;
|
|
18
|
+
|
|
19
|
+
export declare interface ReserveWebMcpToolNamesOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The document whose `modelContext` the names are registered against.
|
|
22
|
+
* Defaults to `globalThis.document`. Registrars that accept an injectable
|
|
23
|
+
* document (tests, non-window hosts) must pass the SAME object they read
|
|
24
|
+
* `modelContext` from, or their reservations land in a different table.
|
|
25
|
+
*/
|
|
26
|
+
document?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Thrown synchronously when a name is already held. Carries the colliding
|
|
31
|
+
* name and the owner that holds it so a host can report both without parsing
|
|
32
|
+
* the message.
|
|
33
|
+
*/
|
|
34
|
+
export declare class WebMcpToolNameCollisionError extends Error {
|
|
35
|
+
readonly toolName: string;
|
|
36
|
+
/** The path that already holds {@link toolName}. */
|
|
37
|
+
readonly owner: WebMcpToolNameOwner;
|
|
38
|
+
/** The path that tried to take it. */
|
|
39
|
+
readonly requestedBy: WebMcpToolNameOwner;
|
|
40
|
+
constructor(toolName: string,
|
|
41
|
+
/** The path that already holds {@link toolName}. */
|
|
42
|
+
owner: WebMcpToolNameOwner,
|
|
43
|
+
/** The path that tried to take it. */
|
|
44
|
+
requestedBy: WebMcpToolNameOwner);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Document-global WebMCP tool-name lock (#2613).
|
|
49
|
+
*
|
|
50
|
+
* A browser tool name is derived independently on three paths that all end at
|
|
51
|
+
* the same `document.modelContext`:
|
|
52
|
+
*
|
|
53
|
+
* 1. **generated** model tools — `<namespace>_<model>_<action>`, built by
|
|
54
|
+
* `registerWebMcpTools` from the exposure policy;
|
|
55
|
+
* 2. **ui** — the six fixed `smrt_ui_*` tools a UI layer registers under a
|
|
56
|
+
* configurable prefix (`registerWebMcpUiTools` in `@happyvertical/smrt-svelte`);
|
|
57
|
+
* 3. **intent** / **bespoke** — declared view intents (#2588) and hand-written
|
|
58
|
+
* `useWebMcpTool` tools, whose names come from the declaration `id` or the
|
|
59
|
+
* spec `name`.
|
|
60
|
+
*
|
|
61
|
+
* None of the three can see the others at declaration time: a namespace and a
|
|
62
|
+
* UI prefix are runtime values a declaration never sees. `registerWebMcpTools`
|
|
63
|
+
* rejects duplicates WITHIN its own prospective set, but nothing checked
|
|
64
|
+
* ACROSS the paths, so a collision reached the host — which rejects the later
|
|
65
|
+
* registration and leaves the tool silently absent.
|
|
66
|
+
*
|
|
67
|
+
* This module is the shared reservation table those paths coordinate through.
|
|
68
|
+
* It is an AVAILABILITY guard, not a trust boundary: every path stays
|
|
69
|
+
* fail-closed and browser-only regardless, and a collision loses a tool rather
|
|
70
|
+
* than granting one. The authenticated REST surface remains the auth, tenant,
|
|
71
|
+
* and field-write boundary.
|
|
72
|
+
*
|
|
73
|
+
* ## Why the table lives on the document, not in this module
|
|
74
|
+
*
|
|
75
|
+
* The lock must be shared by every registrar reachable from one page, which
|
|
76
|
+
* spans package boundaries (`smrt-web` and a UI layer) and therefore possibly
|
|
77
|
+
* more than one copy of this module — a second bundle chunk, a duplicated
|
|
78
|
+
* dependency, or an HMR reload that replaces module state while the host
|
|
79
|
+
* registry keeps every tool it already accepted. Module-level state loses the
|
|
80
|
+
* reservations in all of those cases, and a lost reservation is worse than no
|
|
81
|
+
* lock: it lets a duplicate through, or (on the release side) strands a name
|
|
82
|
+
* forever. The table is therefore stored on the `document` object itself under
|
|
83
|
+
* a cross-realm `Symbol.for` key, which every copy of this module resolves to
|
|
84
|
+
* the same slot.
|
|
85
|
+
*
|
|
86
|
+
* The table is additionally stamped with the `modelContext` it was built for.
|
|
87
|
+
* When a host installs a NEW model context on the same document — a fresh
|
|
88
|
+
* origin-trial registry, or a test harness swapping the double — every tool
|
|
89
|
+
* the old registry held is gone, so the table resets rather than stranding
|
|
90
|
+
* those names.
|
|
91
|
+
*
|
|
92
|
+
* This module is dependency-free by design (see the package's dependency-DAG
|
|
93
|
+
* guardrails) and ships as the `@happyvertical/smrt-web/webmcp-tool-names`
|
|
94
|
+
* entry so a UI layer can reserve its fixed tool names without pulling in the
|
|
95
|
+
* client-data engine.
|
|
96
|
+
*/
|
|
97
|
+
/** Which registration path holds a reserved WebMCP tool name. */
|
|
98
|
+
export declare type WebMcpToolNameOwner = 'generated' | 'ui' | 'intent' | 'bespoke';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The owner currently holding `name` on this document, or undefined. For
|
|
102
|
+
* diagnostics and tests; registration paths use
|
|
103
|
+
* {@link reserveWebMcpToolNames}, which is atomic.
|
|
104
|
+
*/
|
|
105
|
+
export declare function webMcpToolNameOwner(name: string, options?: ReserveWebMcpToolNamesOptions): WebMcpToolNameOwner | undefined;
|
|
106
|
+
|
|
107
|
+
/** Releases every name one reservation holds. Idempotent. */
|
|
108
|
+
export declare interface WebMcpToolNameReservation {
|
|
109
|
+
release(): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/webmcp-tool-names.ts
|
|
2
|
+
var WebMcpToolNameCollisionError = class extends Error {
|
|
3
|
+
constructor(toolName, owner, requestedBy) {
|
|
4
|
+
super(`WebMCP tool name "${toolName}" is already registered by the ${owner} path; the ${requestedBy} path cannot register it. Rename the ${requestedBy} tool, or give the generated tools a webmcp namespace / the UI tools a different prefix.`);
|
|
5
|
+
this.toolName = toolName;
|
|
6
|
+
this.owner = owner;
|
|
7
|
+
this.requestedBy = requestedBy;
|
|
8
|
+
this.name = "WebMcpToolNameCollisionError";
|
|
9
|
+
}
|
|
10
|
+
toolName;
|
|
11
|
+
owner;
|
|
12
|
+
requestedBy;
|
|
13
|
+
};
|
|
14
|
+
var TABLE_KEY = /* @__PURE__ */ Symbol.for("@happyvertical/smrt-web:webmcp-tool-names");
|
|
15
|
+
var INERT_RESERVATION = { release: () => {} };
|
|
16
|
+
function resolveDocument(documentLike) {
|
|
17
|
+
const doc = documentLike ?? globalThis.document ?? void 0;
|
|
18
|
+
return doc && typeof doc === "object" ? doc : void 0;
|
|
19
|
+
}
|
|
20
|
+
function tableFor(doc) {
|
|
21
|
+
const slot = doc;
|
|
22
|
+
const modelContext = doc.modelContext;
|
|
23
|
+
const existing = slot[TABLE_KEY];
|
|
24
|
+
if (existing && existing.modelContext === modelContext) return existing;
|
|
25
|
+
const table = {
|
|
26
|
+
modelContext,
|
|
27
|
+
names: /* @__PURE__ */ new Map()
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(doc, TABLE_KEY, {
|
|
30
|
+
value: table,
|
|
31
|
+
configurable: true,
|
|
32
|
+
enumerable: false,
|
|
33
|
+
writable: true
|
|
34
|
+
});
|
|
35
|
+
return table;
|
|
36
|
+
}
|
|
37
|
+
function reserveWebMcpToolNames(names, owner, options = {}) {
|
|
38
|
+
const doc = resolveDocument(options.document);
|
|
39
|
+
if (!doc) return INERT_RESERVATION;
|
|
40
|
+
const table = tableFor(doc);
|
|
41
|
+
const token = {};
|
|
42
|
+
const taken = [];
|
|
43
|
+
const drop = () => {
|
|
44
|
+
for (const name of taken) if (table.names.get(name)?.token === token) table.names.delete(name);
|
|
45
|
+
};
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const held = table.names.get(name);
|
|
48
|
+
if (held) {
|
|
49
|
+
drop();
|
|
50
|
+
throw new WebMcpToolNameCollisionError(name, held.owner, owner);
|
|
51
|
+
}
|
|
52
|
+
table.names.set(name, {
|
|
53
|
+
owner,
|
|
54
|
+
token
|
|
55
|
+
});
|
|
56
|
+
taken.push(name);
|
|
57
|
+
}
|
|
58
|
+
let released = false;
|
|
59
|
+
return { release: () => {
|
|
60
|
+
if (released) return;
|
|
61
|
+
released = true;
|
|
62
|
+
drop();
|
|
63
|
+
} };
|
|
64
|
+
}
|
|
65
|
+
function webMcpToolNameOwner(name, options = {}) {
|
|
66
|
+
const doc = resolveDocument(options.document);
|
|
67
|
+
if (!doc) return void 0;
|
|
68
|
+
return tableFor(doc).names.get(name)?.owner;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { WebMcpToolNameCollisionError, reserveWebMcpToolNames, webMcpToolNameOwner };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=webmcp-tool-names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp-tool-names.js","names":[],"sources":["../src/webmcp-tool-names.ts"],"sourcesContent":["/**\n * Document-global WebMCP tool-name lock (#2613).\n *\n * A browser tool name is derived independently on three paths that all end at\n * the same `document.modelContext`:\n *\n * 1. **generated** model tools — `<namespace>_<model>_<action>`, built by\n * `registerWebMcpTools` from the exposure policy;\n * 2. **ui** — the six fixed `smrt_ui_*` tools a UI layer registers under a\n * configurable prefix (`registerWebMcpUiTools` in `@happyvertical/smrt-svelte`);\n * 3. **intent** / **bespoke** — declared view intents (#2588) and hand-written\n * `useWebMcpTool` tools, whose names come from the declaration `id` or the\n * spec `name`.\n *\n * None of the three can see the others at declaration time: a namespace and a\n * UI prefix are runtime values a declaration never sees. `registerWebMcpTools`\n * rejects duplicates WITHIN its own prospective set, but nothing checked\n * ACROSS the paths, so a collision reached the host — which rejects the later\n * registration and leaves the tool silently absent.\n *\n * This module is the shared reservation table those paths coordinate through.\n * It is an AVAILABILITY guard, not a trust boundary: every path stays\n * fail-closed and browser-only regardless, and a collision loses a tool rather\n * than granting one. The authenticated REST surface remains the auth, tenant,\n * and field-write boundary.\n *\n * ## Why the table lives on the document, not in this module\n *\n * The lock must be shared by every registrar reachable from one page, which\n * spans package boundaries (`smrt-web` and a UI layer) and therefore possibly\n * more than one copy of this module — a second bundle chunk, a duplicated\n * dependency, or an HMR reload that replaces module state while the host\n * registry keeps every tool it already accepted. Module-level state loses the\n * reservations in all of those cases, and a lost reservation is worse than no\n * lock: it lets a duplicate through, or (on the release side) strands a name\n * forever. The table is therefore stored on the `document` object itself under\n * a cross-realm `Symbol.for` key, which every copy of this module resolves to\n * the same slot.\n *\n * The table is additionally stamped with the `modelContext` it was built for.\n * When a host installs a NEW model context on the same document — a fresh\n * origin-trial registry, or a test harness swapping the double — every tool\n * the old registry held is gone, so the table resets rather than stranding\n * those names.\n *\n * This module is dependency-free by design (see the package's dependency-DAG\n * guardrails) and ships as the `@happyvertical/smrt-web/webmcp-tool-names`\n * entry so a UI layer can reserve its fixed tool names without pulling in the\n * client-data engine.\n */\n\n/** Which registration path holds a reserved WebMCP tool name. */\nexport type WebMcpToolNameOwner = 'generated' | 'ui' | 'intent' | 'bespoke';\n\n/**\n * Thrown synchronously when a name is already held. Carries the colliding\n * name and the owner that holds it so a host can report both without parsing\n * the message.\n */\nexport class WebMcpToolNameCollisionError extends Error {\n constructor(\n readonly toolName: string,\n /** The path that already holds {@link toolName}. */\n readonly owner: WebMcpToolNameOwner,\n /** The path that tried to take it. */\n readonly requestedBy: WebMcpToolNameOwner,\n ) {\n super(\n `WebMCP tool name \"${toolName}\" is already registered by the ${owner} path; the ${requestedBy} path cannot register it. Rename the ${requestedBy} tool, or give the generated tools a webmcp namespace / the UI tools a different prefix.`,\n );\n this.name = 'WebMcpToolNameCollisionError';\n }\n}\n\n/** Releases every name one reservation holds. Idempotent. */\nexport interface WebMcpToolNameReservation {\n release(): void;\n}\n\nexport interface ReserveWebMcpToolNamesOptions {\n /**\n * The document whose `modelContext` the names are registered against.\n * Defaults to `globalThis.document`. Registrars that accept an injectable\n * document (tests, non-window hosts) must pass the SAME object they read\n * `modelContext` from, or their reservations land in a different table.\n */\n document?: unknown;\n}\n\n/** A held name plus the token identifying the reservation that holds it. */\ninterface HeldName {\n owner: WebMcpToolNameOwner;\n token: object;\n}\n\ninterface ToolNameTable {\n /** The model context these reservations were taken against. */\n modelContext: unknown;\n names: Map<string, HeldName>;\n}\n\nconst TABLE_KEY = Symbol.for('@happyvertical/smrt-web:webmcp-tool-names');\n\nconst INERT_RESERVATION: WebMcpToolNameReservation = { release: () => {} };\n\nfunction resolveDocument(documentLike: unknown): object | undefined {\n const doc =\n documentLike ??\n (globalThis as { document?: unknown }).document ??\n undefined;\n return doc && typeof doc === 'object' ? (doc as object) : undefined;\n}\n\nfunction tableFor(doc: object): ToolNameTable {\n const slot = doc as Record<symbol, ToolNameTable | undefined>;\n const modelContext = (doc as { modelContext?: unknown }).modelContext;\n const existing = slot[TABLE_KEY];\n // A replaced model context means the host registry that held the old\n // reservations no longer exists; start clean rather than stranding names.\n if (existing && existing.modelContext === modelContext) return existing;\n const table: ToolNameTable = { modelContext, names: new Map() };\n Object.defineProperty(doc, TABLE_KEY, {\n value: table,\n configurable: true,\n enumerable: false,\n writable: true,\n });\n return table;\n}\n\n/**\n * Reserve `names` for `owner` against the document's WebMCP registry.\n *\n * Reservation is ALL-OR-NOTHING: if any name is already held — by this path\n * or another — nothing is reserved and a {@link WebMcpToolNameCollisionError}\n * naming the colliding name and its current owner is thrown synchronously,\n * before any tool reaches `document.modelContext`. Duplicates within `names`\n * itself collide the same way.\n *\n * Off-WebMCP (no document) the call is a no-op returning an inert\n * reservation, matching every registrar's own no-op guarantee.\n *\n * A reservation releases only the names it took: `release()` never drops a\n * name a later reservation has since acquired, so a dispose that lands after\n * a same-name re-registration cannot revoke the new holder's claim.\n */\nexport function reserveWebMcpToolNames(\n names: readonly string[],\n owner: WebMcpToolNameOwner,\n options: ReserveWebMcpToolNamesOptions = {},\n): WebMcpToolNameReservation {\n const doc = resolveDocument(options.document);\n if (!doc) return INERT_RESERVATION;\n\n const table = tableFor(doc);\n const token = {};\n const taken: string[] = [];\n const drop = (): void => {\n for (const name of taken) {\n // Only ever drop a name this reservation still holds. Together with the\n // `released` latch below, a stale handle can neither release twice nor\n // revoke a claim a later reservation has since taken under the same name.\n if (table.names.get(name)?.token === token) table.names.delete(name);\n }\n };\n\n for (const name of names) {\n const held = table.names.get(name);\n if (held) {\n // All-or-nothing: give back everything taken so far, so a rejected\n // reservation leaves no name stranded and the caller can retry with a\n // corrected set.\n drop();\n throw new WebMcpToolNameCollisionError(name, held.owner, owner);\n }\n table.names.set(name, { owner, token });\n taken.push(name);\n }\n\n let released = false;\n return {\n release: () => {\n if (released) return;\n released = true;\n drop();\n },\n };\n}\n\n/**\n * The owner currently holding `name` on this document, or undefined. For\n * diagnostics and tests; registration paths use\n * {@link reserveWebMcpToolNames}, which is atomic.\n */\nexport function webMcpToolNameOwner(\n name: string,\n options: ReserveWebMcpToolNamesOptions = {},\n): WebMcpToolNameOwner | undefined {\n const doc = resolveDocument(options.document);\n if (!doc) return undefined;\n return tableFor(doc).names.get(name)?.owner;\n}\n"],"mappings":";AA2DO,IAAM,+BAAN,cAA2C,MAAM;CACtD,YACW,UAEA,OAEA,aACT;EACA,MACE,qBAAqB,SAAQ,iCAAkC,MAAK,aAAc,YAAW,uCAAwC,YAAW,yFAClJ;EARS,KAAA,WAAA;EAEA,KAAA,QAAA;EAEA,KAAA,cAAA;EAKT,KAAK,OAAO;CACd;CAVW;CAEA;CAEA;AAOb;AA6BA,IAAM,YAAY,uBAAO,IAAI,2CAA2C;AAExE,IAAM,oBAA+C,EAAE,eAAe,CAAC,EAAE;AAEzE,SAAS,gBAAgB,cAA2C;CAClE,MAAM,MACJ,gBACC,WAAsC,YACvC,KAAA;CACF,OAAO,OAAO,OAAO,QAAQ,WAAY,MAAiB,KAAA;AAC5D;AAEA,SAAS,SAAS,KAA4B;CAC5C,MAAM,OAAO;CACb,MAAM,eAAgB,IAAmC;CACzD,MAAM,WAAW,KAAK;CAGtB,IAAI,YAAY,SAAS,iBAAiB,cAAc,OAAO;CAC/D,MAAM,QAAuB;EAAE;EAAc,uBAAO,IAAI,IAAI;CAAE;CAC9D,OAAO,eAAe,KAAK,WAAW;EACpC,OAAO;EACP,cAAc;EACd,YAAY;EACZ,UAAU;CACZ,CAAC;CACD,OAAO;AACT;AAkBO,SAAS,uBACd,OACA,OACA,UAAyC,CAAC,GACf;CAC3B,MAAM,MAAM,gBAAgB,QAAQ,QAAQ;CAC5C,IAAI,CAAC,KAAK,OAAO;CAEjB,MAAM,QAAQ,SAAS,GAAG;CAC1B,MAAM,QAAQ,CAAC;CACf,MAAM,QAAkB,CAAC;CACzB,MAAM,aAAmB;EACvB,KAAA,MAAW,QAAQ,OAIjB,IAAI,MAAM,MAAM,IAAI,IAAI,CAAA,EAAG,UAAU,OAAO,MAAM,MAAM,OAAO,IAAI;CAEvE;CAEA,KAAA,MAAW,QAAQ,OAAO;EACxB,MAAM,OAAO,MAAM,MAAM,IAAI,IAAI;EACjC,IAAI,MAAM;GAIR,KAAK;GACL,MAAM,IAAI,6BAA6B,MAAM,KAAK,OAAO,KAAK;EAChE;EACA,MAAM,MAAM,IAAI,MAAM;GAAE;GAAO;EAAM,CAAC;EACtC,MAAM,KAAK,IAAI;CACjB;CAEA,IAAI,WAAW;CACf,OAAO,EACL,eAAe;EACb,IAAI,UAAU;EACd,WAAW;EACX,KAAK;CACP,EACF;AACF;AAOO,SAAS,oBACd,MACA,UAAyC,CAAC,GACT;CACjC,MAAM,MAAM,gBAAgB,QAAQ,QAAQ;CAC5C,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,OAAO,SAAS,GAAG,CAAA,CAAE,MAAM,IAAI,IAAI,CAAA,EAAG;AACxC"}
|
package/dist/webmcp.d.ts
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* `compileViewIntentToolSpec` from `intent.target`. No author-supplied code
|
|
14
14
|
* runs, and the only thing it can do is dispatch one browser registry
|
|
15
15
|
* command — the runtime half of the no-REST invariant.
|
|
16
|
+
*
|
|
17
|
+
* The intent's derived tool name is claimed against the document-global
|
|
18
|
+
* tool-name lock (#2613), so an id whose flattened name collides with a
|
|
19
|
+
* generated model tool or a fixed UI tool throws
|
|
20
|
+
* {@link WebMcpToolNameCollisionError} here rather than silently losing the
|
|
21
|
+
* tool at the host. `defineIntent`'s own intent-vs-intent check cannot see
|
|
22
|
+
* either, because both depend on a runtime `namespace` / `prefix` the
|
|
23
|
+
* declaration never sees.
|
|
24
|
+
*
|
|
25
|
+
* @throws {WebMcpToolNameCollisionError} when the derived name is already held.
|
|
16
26
|
*/
|
|
17
27
|
export declare function registerViewIntent(intent: ViewIntent, binding: ViewIntentBinding, options?: RegisterWebMcpBespokeToolOptions): WebMcpRegistrationDisposer;
|
|
18
28
|
|
|
@@ -26,9 +36,19 @@ export declare function registerViewIntent(intent: ViewIntent, binding: ViewInte
|
|
|
26
36
|
* `maxTools` are out of scope for a bespoke tool; see
|
|
27
37
|
* {@link RegisterWebMcpBespokeToolOptions}.
|
|
28
38
|
*
|
|
39
|
+
* The tool's name is also claimed against the
|
|
40
|
+
* document-global tool-name lock before the browser sees it. A name a
|
|
41
|
+
* generated model tool, a fixed UI tool, or another live bespoke tool /
|
|
42
|
+
* intent already holds throws {@link WebMcpToolNameCollisionError}
|
|
43
|
+
* SYNCHRONOUSLY, naming the colliding name and the owner that holds it.
|
|
44
|
+
* Previously the host accepted the call and silently dropped the losing
|
|
45
|
+
* registration. Disposing releases the name, so register/dispose/re-register
|
|
46
|
+
* under one name still succeeds.
|
|
47
|
+
*
|
|
29
48
|
* @returns a disposer that deregisters the tool this call registered (a
|
|
30
49
|
* no-op double-call). On a browser without WebMCP, or when policy excludes
|
|
31
50
|
* the tool's effect, the call is a no-op and the disposer is inert.
|
|
51
|
+
* @throws {WebMcpToolNameCollisionError} when the name is already held.
|
|
32
52
|
*/
|
|
33
53
|
export declare function registerWebMcpBespokeTool(spec: WebMcpBespokeToolSpec, options?: RegisterWebMcpBespokeToolOptions): WebMcpRegistrationDisposer;
|
|
34
54
|
|
|
@@ -41,13 +61,36 @@ export declare interface RegisterWebMcpBespokeToolOptions {
|
|
|
41
61
|
* an unrelated generated tool set fail to register.
|
|
42
62
|
*/
|
|
43
63
|
effects?: readonly WebMcpToolEffect[];
|
|
64
|
+
/**
|
|
65
|
+
* Which path this registration belongs to, used ONLY to label its
|
|
66
|
+
* tool-name reservation (#2613) so a later collision names the right
|
|
67
|
+
* source. Defaults to `bespoke`.
|
|
68
|
+
*
|
|
69
|
+
* A UI-framework binding that compiles a declared view intent itself and
|
|
70
|
+
* registers it here — rather than through {@link registerViewIntent} —
|
|
71
|
+
* must pass `'intent'`. `useViewIntent` in `@happyvertical/smrt-svelte`
|
|
72
|
+
* does exactly that, because reusing `useWebMcpTool`'s single WebMCP
|
|
73
|
+
* lifecycle is a documented invariant of that package; without this it
|
|
74
|
+
* would be the only shipped intent path and every intent collision would
|
|
75
|
+
* blame a `useWebMcpTool` call that does not exist.
|
|
76
|
+
*
|
|
77
|
+
* Purely a diagnostic label: it grants no capability, narrows no policy,
|
|
78
|
+
* and changes nothing about how the tool registers or executes.
|
|
79
|
+
*/
|
|
80
|
+
owner?: Extract<WebMcpToolNameOwner, 'intent' | 'bespoke'>;
|
|
44
81
|
}
|
|
45
82
|
|
|
46
83
|
/**
|
|
47
84
|
* Register every collection's generated tool descriptors with WebMCP.
|
|
48
85
|
*
|
|
86
|
+
* Every selected tool name is claimed against the document-global tool-name
|
|
87
|
+
* lock (#2613) before the first browser registration, so a collision with a
|
|
88
|
+
* UI-layer or bespoke tool throws {@link WebMcpToolNameCollisionError} with
|
|
89
|
+
* the offending name and its current owner instead of reaching the host.
|
|
90
|
+
*
|
|
49
91
|
* @returns a disposer that deregisters all tools this call registered. On a
|
|
50
92
|
* browser without WebMCP the call is a no-op and the disposer is inert.
|
|
93
|
+
* @throws {WebMcpToolNameCollisionError} when a selected name is already held.
|
|
51
94
|
*/
|
|
52
95
|
export declare function registerWebMcpTools(definitions: readonly WebMcpRegistrationDefinition[], options?: RegisterWebMcpToolsOptions): WebMcpRegistrationDisposer;
|
|
53
96
|
|
|
@@ -78,6 +121,34 @@ export declare interface RegisterWebMcpToolsOptions extends WebMcpExposurePolicy
|
|
|
78
121
|
filterTool?: (definition: WebMcpToolDefinition) => boolean;
|
|
79
122
|
}
|
|
80
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Reserve `names` for `owner` against the document's WebMCP registry.
|
|
126
|
+
*
|
|
127
|
+
* Reservation is ALL-OR-NOTHING: if any name is already held — by this path
|
|
128
|
+
* or another — nothing is reserved and a {@link WebMcpToolNameCollisionError}
|
|
129
|
+
* naming the colliding name and its current owner is thrown synchronously,
|
|
130
|
+
* before any tool reaches `document.modelContext`. Duplicates within `names`
|
|
131
|
+
* itself collide the same way.
|
|
132
|
+
*
|
|
133
|
+
* Off-WebMCP (no document) the call is a no-op returning an inert
|
|
134
|
+
* reservation, matching every registrar's own no-op guarantee.
|
|
135
|
+
*
|
|
136
|
+
* A reservation releases only the names it took: `release()` never drops a
|
|
137
|
+
* name a later reservation has since acquired, so a dispose that lands after
|
|
138
|
+
* a same-name re-registration cannot revoke the new holder's claim.
|
|
139
|
+
*/
|
|
140
|
+
export declare function reserveWebMcpToolNames(names: readonly string[], owner: WebMcpToolNameOwner, options?: ReserveWebMcpToolNamesOptions): WebMcpToolNameReservation;
|
|
141
|
+
|
|
142
|
+
declare interface ReserveWebMcpToolNamesOptions {
|
|
143
|
+
/**
|
|
144
|
+
* The document whose `modelContext` the names are registered against.
|
|
145
|
+
* Defaults to `globalThis.document`. Registrars that accept an injectable
|
|
146
|
+
* document (tests, non-window hosts) must pass the SAME object they read
|
|
147
|
+
* `modelContext` from, or their reservations land in a different table.
|
|
148
|
+
*/
|
|
149
|
+
document?: unknown;
|
|
150
|
+
}
|
|
151
|
+
|
|
81
152
|
/**
|
|
82
153
|
* The per-collection CRUD surface of the generated REST client
|
|
83
154
|
* (`createClient(basePath).<collection>` from `@happyvertical/smrt-virt-client`).
|
|
@@ -457,6 +528,89 @@ declare interface WebMcpToolDefinition extends WebToolDescriptor {
|
|
|
457
528
|
*/
|
|
458
529
|
export declare type WebMcpToolEffect = 'read' | 'write' | 'destructive';
|
|
459
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Thrown synchronously when a name is already held. Carries the colliding
|
|
533
|
+
* name and the owner that holds it so a host can report both without parsing
|
|
534
|
+
* the message.
|
|
535
|
+
*/
|
|
536
|
+
export declare class WebMcpToolNameCollisionError extends Error {
|
|
537
|
+
readonly toolName: string;
|
|
538
|
+
/** The path that already holds {@link toolName}. */
|
|
539
|
+
readonly owner: WebMcpToolNameOwner;
|
|
540
|
+
/** The path that tried to take it. */
|
|
541
|
+
readonly requestedBy: WebMcpToolNameOwner;
|
|
542
|
+
constructor(toolName: string,
|
|
543
|
+
/** The path that already holds {@link toolName}. */
|
|
544
|
+
owner: WebMcpToolNameOwner,
|
|
545
|
+
/** The path that tried to take it. */
|
|
546
|
+
requestedBy: WebMcpToolNameOwner);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Document-global WebMCP tool-name lock (#2613).
|
|
551
|
+
*
|
|
552
|
+
* A browser tool name is derived independently on three paths that all end at
|
|
553
|
+
* the same `document.modelContext`:
|
|
554
|
+
*
|
|
555
|
+
* 1. **generated** model tools — `<namespace>_<model>_<action>`, built by
|
|
556
|
+
* `registerWebMcpTools` from the exposure policy;
|
|
557
|
+
* 2. **ui** — the six fixed `smrt_ui_*` tools a UI layer registers under a
|
|
558
|
+
* configurable prefix (`registerWebMcpUiTools` in `@happyvertical/smrt-svelte`);
|
|
559
|
+
* 3. **intent** / **bespoke** — declared view intents (#2588) and hand-written
|
|
560
|
+
* `useWebMcpTool` tools, whose names come from the declaration `id` or the
|
|
561
|
+
* spec `name`.
|
|
562
|
+
*
|
|
563
|
+
* None of the three can see the others at declaration time: a namespace and a
|
|
564
|
+
* UI prefix are runtime values a declaration never sees. `registerWebMcpTools`
|
|
565
|
+
* rejects duplicates WITHIN its own prospective set, but nothing checked
|
|
566
|
+
* ACROSS the paths, so a collision reached the host — which rejects the later
|
|
567
|
+
* registration and leaves the tool silently absent.
|
|
568
|
+
*
|
|
569
|
+
* This module is the shared reservation table those paths coordinate through.
|
|
570
|
+
* It is an AVAILABILITY guard, not a trust boundary: every path stays
|
|
571
|
+
* fail-closed and browser-only regardless, and a collision loses a tool rather
|
|
572
|
+
* than granting one. The authenticated REST surface remains the auth, tenant,
|
|
573
|
+
* and field-write boundary.
|
|
574
|
+
*
|
|
575
|
+
* ## Why the table lives on the document, not in this module
|
|
576
|
+
*
|
|
577
|
+
* The lock must be shared by every registrar reachable from one page, which
|
|
578
|
+
* spans package boundaries (`smrt-web` and a UI layer) and therefore possibly
|
|
579
|
+
* more than one copy of this module — a second bundle chunk, a duplicated
|
|
580
|
+
* dependency, or an HMR reload that replaces module state while the host
|
|
581
|
+
* registry keeps every tool it already accepted. Module-level state loses the
|
|
582
|
+
* reservations in all of those cases, and a lost reservation is worse than no
|
|
583
|
+
* lock: it lets a duplicate through, or (on the release side) strands a name
|
|
584
|
+
* forever. The table is therefore stored on the `document` object itself under
|
|
585
|
+
* a cross-realm `Symbol.for` key, which every copy of this module resolves to
|
|
586
|
+
* the same slot.
|
|
587
|
+
*
|
|
588
|
+
* The table is additionally stamped with the `modelContext` it was built for.
|
|
589
|
+
* When a host installs a NEW model context on the same document — a fresh
|
|
590
|
+
* origin-trial registry, or a test harness swapping the double — every tool
|
|
591
|
+
* the old registry held is gone, so the table resets rather than stranding
|
|
592
|
+
* those names.
|
|
593
|
+
*
|
|
594
|
+
* This module is dependency-free by design (see the package's dependency-DAG
|
|
595
|
+
* guardrails) and ships as the `@happyvertical/smrt-web/webmcp-tool-names`
|
|
596
|
+
* entry so a UI layer can reserve its fixed tool names without pulling in the
|
|
597
|
+
* client-data engine.
|
|
598
|
+
*/
|
|
599
|
+
/** Which registration path holds a reserved WebMCP tool name. */
|
|
600
|
+
export declare type WebMcpToolNameOwner = 'generated' | 'ui' | 'intent' | 'bespoke';
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* The owner currently holding `name` on this document, or undefined. For
|
|
604
|
+
* diagnostics and tests; registration paths use
|
|
605
|
+
* {@link reserveWebMcpToolNames}, which is atomic.
|
|
606
|
+
*/
|
|
607
|
+
export declare function webMcpToolNameOwner(name: string, options?: ReserveWebMcpToolNamesOptions): WebMcpToolNameOwner | undefined;
|
|
608
|
+
|
|
609
|
+
/** Releases every name one reservation holds. Idempotent. */
|
|
610
|
+
export declare interface WebMcpToolNameReservation {
|
|
611
|
+
release(): void;
|
|
612
|
+
}
|
|
613
|
+
|
|
460
614
|
/**
|
|
461
615
|
* One generated collection definition: everything needed to construct a client
|
|
462
616
|
* collection over the generated REST surface. The `_row` property is a phantom
|
package/dist/webmcp.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { h as registerWebMcpTools, m as registerWebMcpBespokeTool, p as registerViewIntent } from "./chunks/src-
|
|
2
|
-
|
|
1
|
+
import { h as registerWebMcpTools, m as registerWebMcpBespokeTool, p as registerViewIntent } from "./chunks/src-DHH8ptH8.js";
|
|
2
|
+
import { WebMcpToolNameCollisionError, reserveWebMcpToolNames, webMcpToolNameOwner } from "./webmcp-tool-names.js";
|
|
3
|
+
export { WebMcpToolNameCollisionError, registerViewIntent, registerWebMcpBespokeTool, registerWebMcpTools, reserveWebMcpToolNames, webMcpToolNameOwner };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "SMRT browser client data runtime: typed collection factory wrapping the client-data engine over generated REST clients",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"./intents": {
|
|
25
25
|
"types": "./dist/intents.d.ts",
|
|
26
26
|
"import": "./dist/intents.js"
|
|
27
|
+
},
|
|
28
|
+
"./webmcp-tool-names": {
|
|
29
|
+
"types": "./dist/webmcp-tool-names.d.ts",
|
|
30
|
+
"import": "./dist/webmcp-tool-names.js"
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
"dependencies": {
|
|
@@ -32,13 +36,13 @@
|
|
|
32
36
|
"@tanstack/query-db-collection": "^1.0.46"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
39
|
+
"@happyvertical/smrt-core": "0.47.0",
|
|
40
|
+
"@happyvertical/smrt-scanner": "0.47.0",
|
|
35
41
|
"@types/node": "24.13.2",
|
|
36
42
|
"fake-indexeddb": "^6.2.5",
|
|
37
43
|
"typescript": "5.9.3",
|
|
38
44
|
"vite": "8.1.4",
|
|
39
|
-
"vitest": "4.1.10"
|
|
40
|
-
"@happyvertical/smrt-core": "0.45.3",
|
|
41
|
-
"@happyvertical/smrt-scanner": "0.45.3"
|
|
45
|
+
"vitest": "4.1.10"
|
|
42
46
|
},
|
|
43
47
|
"publishConfig": {
|
|
44
48
|
"registry": "https://registry.npmjs.org",
|