symbiote-ui 0.3.0-alpha.63 → 0.3.0-alpha.65
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/CHANGELOG.md +27 -0
- package/chat/ChatComposer/ChatComposer.css.js +31 -159
- package/chat/ChatComposer/ChatComposer.js +4 -2
- package/docs/platform-baseline.md +2 -2
- package/package.json +1 -1
- package/skills/symbiote-ui/SKILL.md +4 -1
- package/skills/symbiote-ui/references/webmcp-bridge.md +32 -10
- package/webmcp.js +444 -61
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to `symbiote-ui` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.3.0-alpha.65] - 2026-07-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Kept compact composer parameters on the same action row at narrow widths.
|
|
12
|
+
- Preserved full native select hit areas and explicit accessible names when footer controls collapse to icon-only presentation.
|
|
13
|
+
|
|
14
|
+
## [0.3.0-alpha.64] - 2026-07-18
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added `destructiveHint: true` under `annotations` on the descriptor for destructive action descriptors.
|
|
19
|
+
- Added a `bundle` option to `registerProductContextTools()` so products can register the exact executable descriptors already used to construct their adapter and consumer.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Removed the unused legacy `createNativeToolDescriptor()` export now that
|
|
24
|
+
registration uses canonical executable plain descriptors directly.
|
|
25
|
+
- Fixed `registerWebMcpTool` to register and return the same canonical plain executable descriptor passed by the product bundle, preserving exact descriptor identity before and after refresh.
|
|
26
|
+
- Fixed registration signal lifecycle using an internal `AbortController` that properly cleans up external listeners on all exits, supports idempotent unregistration, and handles undefined native disposers.
|
|
27
|
+
- Fixed native capability check to read active `modelContext` markers (`nativeActive` and `supportsNativeToolDescriptor`) directly, removing public options bag overrides.
|
|
28
|
+
- Fixed native options forwarding to pass exactly `signal` and `exposedTo` options to `registerTool`.
|
|
29
|
+
- Fixed product registration options resolver to correctly identify options bags containing `signal` or `exposedTo`.
|
|
30
|
+
- Fixed `createProductWebMcpBundle` to bind all allowed actions to their own executable plain descriptor closures, preserving action/descriptor pairing through filtering and refresh in-place, and forwarding out-of-band context arguments.
|
|
31
|
+
- Fixed tool validation to fail fast with tool/action identity before the first native registration, rolling back previous registrations on partial failure.
|
|
32
|
+
- Fixed runtime-context property publication to restore prior property state exactly and made producer refresh publication replacement atomic.
|
|
33
|
+
|
|
7
34
|
## [0.3.0-alpha.63] - 2026-07-17
|
|
8
35
|
|
|
9
36
|
- Keep cascade-theme control foregrounds above WCAG AA after browser RGB quantization.
|
|
@@ -417,6 +417,7 @@ sn-button.btn-send[variant="icon"].btn-stop::after {
|
|
|
417
417
|
|
|
418
418
|
.composer-footer-select-control {
|
|
419
419
|
padding-right: var(--sn-step-2);
|
|
420
|
+
position: relative;
|
|
420
421
|
}
|
|
421
422
|
|
|
422
423
|
.composer-footer-checkbox-control {
|
|
@@ -464,20 +465,27 @@ sn-button.btn-send[variant="icon"].btn-stop::after {
|
|
|
464
465
|
text-overflow: ellipsis;
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
.composer-param-collapsed
|
|
468
|
-
|
|
469
|
-
.composer-param-collapsed .composer-footer-value,
|
|
470
|
-
.composer-param-collapsed .composer-footer-suffix {
|
|
471
|
-
width: var(--sn-composer-collapsed-control-width);
|
|
472
|
-
max-width: var(--sn-composer-collapsed-control-width);
|
|
473
|
-
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
474
|
-
color: transparent !important;
|
|
468
|
+
.composer-param-collapsed {
|
|
469
|
+
--sn-is-collapsed: 1;
|
|
475
470
|
}
|
|
476
471
|
|
|
477
|
-
|
|
478
|
-
.composer-
|
|
479
|
-
|
|
480
|
-
|
|
472
|
+
@container style(--sn-is-collapsed: 1) {
|
|
473
|
+
.composer-footer-select {
|
|
474
|
+
position: absolute;
|
|
475
|
+
inset: 0;
|
|
476
|
+
width: 100%;
|
|
477
|
+
min-width: var(--sn-composer-collapsed-control-width);
|
|
478
|
+
height: 100%;
|
|
479
|
+
box-sizing: border-box;
|
|
480
|
+
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
481
|
+
opacity: 0;
|
|
482
|
+
z-index: 1;
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
:is(.composer-footer-label, .composer-footer-value, .composer-footer-suffix) {
|
|
487
|
+
display: none;
|
|
488
|
+
}
|
|
481
489
|
}
|
|
482
490
|
|
|
483
491
|
.composer-footer-details-popover {
|
|
@@ -786,108 +794,33 @@ sn-button.btn-send[variant="icon"].btn-stop::after {
|
|
|
786
794
|
}
|
|
787
795
|
|
|
788
796
|
@container composer-footer (width <= 560px) {
|
|
789
|
-
.composer-priority-1
|
|
790
|
-
|
|
791
|
-
.composer-priority-1 .composer-footer-value,
|
|
792
|
-
.composer-priority-1 .composer-footer-suffix {
|
|
793
|
-
width: var(--sn-composer-collapsed-control-width);
|
|
794
|
-
max-width: var(--sn-composer-collapsed-control-width);
|
|
795
|
-
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
796
|
-
color: transparent !important;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
.composer-priority-1 .composer-footer-label,
|
|
800
|
-
.composer-priority-1 .composer-footer-value,
|
|
801
|
-
.composer-priority-1 .composer-footer-suffix {
|
|
802
|
-
display: none;
|
|
797
|
+
.composer-priority-1 {
|
|
798
|
+
--sn-is-collapsed: 1;
|
|
803
799
|
}
|
|
804
800
|
}
|
|
805
801
|
|
|
806
802
|
@container composer-footer (width <= 500px) {
|
|
807
|
-
.composer-param-model.composer-long-value
|
|
808
|
-
.composer-
|
|
809
|
-
|
|
810
|
-
.composer-param-model.composer-long-value .composer-footer-suffix {
|
|
811
|
-
width: var(--sn-composer-collapsed-control-width);
|
|
812
|
-
max-width: var(--sn-composer-collapsed-control-width);
|
|
813
|
-
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
814
|
-
color: transparent !important;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
.composer-param-model.composer-long-value .composer-footer-label,
|
|
818
|
-
.composer-param-model.composer-long-value .composer-footer-value,
|
|
819
|
-
.composer-param-model.composer-long-value .composer-footer-suffix {
|
|
820
|
-
display: none;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
.composer-priority-2 .composer-footer-select,
|
|
824
|
-
.composer-priority-2 .composer-footer-label,
|
|
825
|
-
.composer-priority-2 .composer-footer-value,
|
|
826
|
-
.composer-priority-2 .composer-footer-suffix {
|
|
827
|
-
width: var(--sn-composer-collapsed-control-width);
|
|
828
|
-
max-width: var(--sn-composer-collapsed-control-width);
|
|
829
|
-
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
830
|
-
color: transparent !important;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
.composer-priority-2 .composer-footer-label,
|
|
834
|
-
.composer-priority-2 .composer-footer-value,
|
|
835
|
-
.composer-priority-2 .composer-footer-suffix {
|
|
836
|
-
display: none;
|
|
803
|
+
.composer-param-model.composer-long-value,
|
|
804
|
+
.composer-priority-2 {
|
|
805
|
+
--sn-is-collapsed: 1;
|
|
837
806
|
}
|
|
838
807
|
}
|
|
839
808
|
|
|
840
809
|
@container composer-footer (width <= 440px) {
|
|
841
|
-
.composer-priority-3
|
|
842
|
-
|
|
843
|
-
.composer-priority-3 .composer-footer-value,
|
|
844
|
-
.composer-priority-3 .composer-footer-suffix {
|
|
845
|
-
width: var(--sn-composer-collapsed-control-width);
|
|
846
|
-
max-width: var(--sn-composer-collapsed-control-width);
|
|
847
|
-
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
848
|
-
color: transparent !important;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
.composer-priority-3 .composer-footer-label,
|
|
852
|
-
.composer-priority-3 .composer-footer-value,
|
|
853
|
-
.composer-priority-3 .composer-footer-suffix {
|
|
854
|
-
display: none;
|
|
810
|
+
.composer-priority-3 {
|
|
811
|
+
--sn-is-collapsed: 1;
|
|
855
812
|
}
|
|
856
813
|
}
|
|
857
814
|
|
|
858
815
|
@container composer-footer (width <= 380px) {
|
|
859
|
-
.composer-priority-4
|
|
860
|
-
|
|
861
|
-
.composer-priority-4 .composer-footer-value,
|
|
862
|
-
.composer-priority-4 .composer-footer-suffix {
|
|
863
|
-
width: var(--sn-composer-collapsed-control-width);
|
|
864
|
-
max-width: var(--sn-composer-collapsed-control-width);
|
|
865
|
-
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
866
|
-
color: transparent !important;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
.composer-priority-4 .composer-footer-label,
|
|
870
|
-
.composer-priority-4 .composer-footer-value,
|
|
871
|
-
.composer-priority-4 .composer-footer-suffix {
|
|
872
|
-
display: none;
|
|
816
|
+
.composer-priority-4 {
|
|
817
|
+
--sn-is-collapsed: 1;
|
|
873
818
|
}
|
|
874
819
|
}
|
|
875
820
|
|
|
876
821
|
@container composer-footer (width <= 320px) {
|
|
877
|
-
.composer-priority-5
|
|
878
|
-
|
|
879
|
-
.composer-priority-5 .composer-footer-value,
|
|
880
|
-
.composer-priority-5 .composer-footer-suffix {
|
|
881
|
-
width: var(--sn-composer-collapsed-control-width);
|
|
882
|
-
max-width: var(--sn-composer-collapsed-control-width);
|
|
883
|
-
padding-right: var(--sn-composer-collapsed-control-padding);
|
|
884
|
-
color: transparent !important;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
.composer-priority-5 .composer-footer-label,
|
|
888
|
-
.composer-priority-5 .composer-footer-value,
|
|
889
|
-
.composer-priority-5 .composer-footer-suffix {
|
|
890
|
-
display: none;
|
|
822
|
+
.composer-priority-5 {
|
|
823
|
+
--sn-is-collapsed: 1;
|
|
891
824
|
}
|
|
892
825
|
}
|
|
893
826
|
|
|
@@ -1231,67 +1164,6 @@ chat-composer.drag-over .composer-body {
|
|
|
1231
1164
|
}
|
|
1232
1165
|
|
|
1233
1166
|
@container chat-composer (width <= 480px) {
|
|
1234
|
-
.composer-body {
|
|
1235
|
-
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
1236
|
-
grid-template-rows: minmax(var(--sn-composer-input-min-height), auto) auto auto;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
:host([leading-controls]) .composer-body,
|
|
1240
|
-
chat-composer[leading-controls] .composer-body {
|
|
1241
|
-
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
:host([leading-controls]) .composer-leading-controls,
|
|
1245
|
-
chat-composer[leading-controls] .composer-leading-controls {
|
|
1246
|
-
grid-column: 1;
|
|
1247
|
-
grid-row: 3;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
:host([leading-controls]) .composer-body textarea,
|
|
1251
|
-
chat-composer[leading-controls] .composer-body textarea {
|
|
1252
|
-
grid-column: 1 / -1;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
.composer-footer {
|
|
1256
|
-
grid-column: 1 / -1;
|
|
1257
|
-
grid-row: 2;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
.composer-actions {
|
|
1261
|
-
grid-column: 2;
|
|
1262
|
-
grid-row: 3;
|
|
1263
|
-
width: auto;
|
|
1264
|
-
max-width: min(54cqi, var(--sn-composer-actions-max-inline-size, 460px));
|
|
1265
|
-
flex-wrap: nowrap;
|
|
1266
|
-
overflow: hidden;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
.btn-mic {
|
|
1270
|
-
grid-column: 3;
|
|
1271
|
-
grid-row: 3;
|
|
1272
|
-
align-self: end;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
.composer-body > sn-button.btn-send {
|
|
1276
|
-
grid-column: 4;
|
|
1277
|
-
grid-row: 3;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
:host([leading-controls]) .composer-actions,
|
|
1281
|
-
chat-composer[leading-controls] .composer-actions {
|
|
1282
|
-
grid-column: 2;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
:host([leading-controls]) .btn-mic,
|
|
1286
|
-
chat-composer[leading-controls] .btn-mic {
|
|
1287
|
-
grid-column: 3;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
:host([leading-controls]) .composer-body > sn-button.btn-send,
|
|
1291
|
-
chat-composer[leading-controls] .composer-body > sn-button.btn-send {
|
|
1292
|
-
grid-column: 4;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
1167
|
.btn-voice-language {
|
|
1296
1168
|
min-width: calc(var(--sn-composer-send-size) * 2.2);
|
|
1297
1169
|
max-width: calc(var(--sn-composer-send-size) * 2.6);
|
|
@@ -168,6 +168,7 @@ function decorateFooterControl(item) {
|
|
|
168
168
|
let meterValue = normalizeProgress(meterSource.value ?? meterSource.progress ?? details?.progress ?? item.progress ?? 0);
|
|
169
169
|
let meterLabel = String(meterSource.label || '');
|
|
170
170
|
let meterTitle = String(meterSource.title || details?.title || item.title || item.label || item.id || '');
|
|
171
|
+
let accessibleName = String(item.title || item.label || item.id || '');
|
|
171
172
|
let hasMeter = Boolean(item.meter || details);
|
|
172
173
|
let baseClass = [
|
|
173
174
|
'composer-footer-btn',
|
|
@@ -220,6 +221,7 @@ function decorateFooterControl(item) {
|
|
|
220
221
|
hasMeter,
|
|
221
222
|
hasMeterLabel: Boolean(meterLabel),
|
|
222
223
|
meterStyle: progressStyle(meterValue),
|
|
224
|
+
accessibleName,
|
|
223
225
|
options,
|
|
224
226
|
};
|
|
225
227
|
}
|
|
@@ -1732,7 +1734,7 @@ ChatComposer.template = html`
|
|
|
1732
1734
|
<label ${{ '@class': 'selectClass', '@data-footer-control-id': 'id', '@data-footer-control-kind': 'kind', '@title': 'title', '@hidden': '!isSelect' }}>
|
|
1733
1735
|
<span class="material-symbols-outlined" ${{ '@hidden': '!hasIcon' }}>{{icon}}</span>
|
|
1734
1736
|
<span class="composer-footer-label" ${{ '@hidden': '!hasLabel' }}>{{label}}</span>
|
|
1735
|
-
<select class="composer-footer-select" ${{ '@data-footer-control-id': 'id', '@data-footer-control-kind': 'kind', '@disabled': 'disabled' }} itemize="options">
|
|
1737
|
+
<select class="composer-footer-select" ${{ '@data-footer-control-id': 'id', '@data-footer-control-kind': 'kind', '@disabled': 'disabled', '@aria-label': 'accessibleName' }} itemize="options">
|
|
1736
1738
|
<template>
|
|
1737
1739
|
<option ${{ '@value': 'value', '@selected': 'selected' }}>{{label}}</option>
|
|
1738
1740
|
</template>
|
|
@@ -1741,7 +1743,7 @@ ChatComposer.template = html`
|
|
|
1741
1743
|
</label>
|
|
1742
1744
|
<label ${{ '@class': 'checkboxClass', '@data-footer-control-id': 'id', '@data-footer-control-kind': 'kind', '@title': 'title', '@hidden': '!isCheckbox' }}>
|
|
1743
1745
|
<span class="material-symbols-outlined" ${{ '@hidden': '!hasIcon' }}>{{icon}}</span>
|
|
1744
|
-
<input class="composer-footer-checkbox" type="checkbox" ${{ '@data-footer-control-id': 'id', '@data-footer-control-kind': 'kind', checked: 'checked', '@disabled': 'disabled' }}>
|
|
1746
|
+
<input class="composer-footer-checkbox" type="checkbox" ${{ '@data-footer-control-id': 'id', '@data-footer-control-kind': 'kind', checked: 'checked', '@disabled': 'disabled', '@aria-label': 'accessibleName' }}>
|
|
1745
1747
|
<span class="composer-footer-label" ${{ '@hidden': '!hasLabel' }}>{{label}}</span>
|
|
1746
1748
|
</label>
|
|
1747
1749
|
<button type="button" ${{ '@class': 'buttonClass', '@data-footer-control-id': 'id', '@data-footer-control-kind': 'kind', '@title': 'title', '@disabled': 'disabled', '@hidden': '!isButton' }}>
|
|
@@ -19,7 +19,7 @@ and whenever `CHROMIUM_BASELINE_TARGET` changes.
|
|
|
19
19
|
| Declarative Shadow DOM | baseline | - | SSR process HTML, stream rendering, root shadow hydration | `HTMLTemplateElement.prototype.shadowRootMode` |
|
|
20
20
|
| adoptedStyleSheets / rootStyles delivery | baseline | - | SSR rootStyles, cascade themes, constructable stylesheet delivery | `Document.prototype.adoptedStyleSheets`, `ShadowRoot.prototype.adoptedStyleSheets`, `CSSStyleSheet.prototype.replaceSync` |
|
|
21
21
|
| view-transition-name custom idents | baseline | - | Workspace address serialization, panel transition targets, route animations | `CSS.supports("view-transition-name", "workspace-panel-main")` |
|
|
22
|
-
|
|
|
22
|
+
| document.modelContext | shimmed | symbiote-webmcp-shim | WebMCP tool registration, tour context, agent-visible component actions | `document.modelContext`; otherwise install `symbiote-webmcp-shim` |
|
|
23
23
|
|
|
24
24
|
## Source Notes
|
|
25
25
|
|
|
@@ -28,5 +28,5 @@ and whenever `CHROMIUM_BASELINE_TARGET` changes.
|
|
|
28
28
|
- Declarative Shadow DOM and constructable stylesheets are required for SSR and
|
|
29
29
|
rootStyles delivery paths, but hosts still own fallback rendering where a target
|
|
30
30
|
browser does not expose the feature.
|
|
31
|
-
- `
|
|
31
|
+
- `document.modelContext` is intentionally shimmed until the WebMCP surface is
|
|
32
32
|
stable in Chromium; this row is the R-UI28 exception that must be re-graded first.
|
package/package.json
CHANGED
|
@@ -232,7 +232,10 @@ import { registerWebMcpTool, triggerWebMcpCommand } from 'symbiote-ui/webmcp';
|
|
|
232
232
|
let { descriptor, unregister } = await registerWebMcpTool({
|
|
233
233
|
name: 'update-panel',
|
|
234
234
|
description: 'Update panel state',
|
|
235
|
-
inputSchema: { type: 'object', properties: { label: { type: 'string' } } }
|
|
235
|
+
inputSchema: { type: 'object', properties: { label: { type: 'string' } } },
|
|
236
|
+
execute(input) {
|
|
237
|
+
return { label: input.label };
|
|
238
|
+
}
|
|
236
239
|
});
|
|
237
240
|
|
|
238
241
|
// Trigger a command (bubbles to controller via webmcp-command event)
|
|
@@ -8,7 +8,7 @@ Protocol (WebMCP) for model-driven tool registration and command events.
|
|
|
8
8
|
WebMCP allows Web Components to declare themselves as "tools" that AI models
|
|
9
9
|
can discover and invoke. Symbiote UI provides a bridge layer that:
|
|
10
10
|
|
|
11
|
-
1. Registers component capabilities with `
|
|
11
|
+
1. Registers component capabilities with `document.modelContext` (if available).
|
|
12
12
|
2. Falls back gracefully when WebMCP is not supported.
|
|
13
13
|
3. Emits `webmcp-command` custom events that the runtime controller forwards via WebSocket.
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ Returns the model context provider, or `null`.
|
|
|
22
22
|
import { getModelContext } from 'symbiote-ui/webmcp';
|
|
23
23
|
|
|
24
24
|
let ctx = getModelContext(document);
|
|
25
|
-
// ctx = document.modelContext ||
|
|
25
|
+
// ctx = document.modelContext || null
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## createToolDescriptor(options)
|
|
@@ -73,13 +73,14 @@ let descriptor = createComponentToolDescriptor(myPanelElement, {
|
|
|
73
73
|
| `componentClass` | `component.className` |
|
|
74
74
|
| `semanticRole` | `component.agent?.semanticRole` |
|
|
75
75
|
|
|
76
|
-
## registerWebMcpTool(options, target)
|
|
76
|
+
## registerWebMcpTool(options, target, registrationOptions)
|
|
77
77
|
|
|
78
78
|
Registers a tool with the model context provider. Falls back gracefully.
|
|
79
79
|
|
|
80
80
|
```javascript
|
|
81
81
|
import { registerWebMcpTool } from 'symbiote-ui/webmcp';
|
|
82
82
|
|
|
83
|
+
let abortController = new AbortController();
|
|
83
84
|
let { nativeActive, descriptor, unregister } = await registerWebMcpTool({
|
|
84
85
|
name: 'send-chat-message',
|
|
85
86
|
description: 'Send a message in the chat panel',
|
|
@@ -87,7 +88,13 @@ let { nativeActive, descriptor, unregister } = await registerWebMcpTool({
|
|
|
87
88
|
type: 'object',
|
|
88
89
|
properties: { message: { type: 'string' } },
|
|
89
90
|
required: ['message']
|
|
91
|
+
},
|
|
92
|
+
execute(input) {
|
|
93
|
+
return { sent: input.message };
|
|
90
94
|
}
|
|
95
|
+
}, document, {
|
|
96
|
+
signal: abortController.signal,
|
|
97
|
+
exposedTo: ['https://agent.example']
|
|
91
98
|
});
|
|
92
99
|
|
|
93
100
|
if (nativeActive) {
|
|
@@ -103,22 +110,37 @@ unregister();
|
|
|
103
110
|
**Parameters:**
|
|
104
111
|
| Param | Type | Default | Description |
|
|
105
112
|
|-------|------|---------|-------------|
|
|
106
|
-
| `options` | `object` | — | Tool descriptor options (name, description, inputSchema) |
|
|
113
|
+
| `options` | `object` | — | Tool descriptor options (name, description, inputSchema, execute) |
|
|
107
114
|
| `target` | `Document` | `globalThis.document` | Document for model context resolution |
|
|
115
|
+
| `registrationOptions` | `object` | `{}` | Registration configuration options (e.g. signal, exposedTo) |
|
|
108
116
|
|
|
109
117
|
**Returns:**
|
|
110
118
|
```javascript
|
|
111
119
|
{
|
|
112
120
|
nativeActive: boolean, // true if registered with native WebMCP API
|
|
113
|
-
descriptor: object, // The tool descriptor object
|
|
114
|
-
unregister: function //
|
|
121
|
+
descriptor: object, // The tool descriptor object (always the same canonical plain descriptor passed to registration)
|
|
122
|
+
unregister: function // Idempotent cleanup function
|
|
115
123
|
}
|
|
116
124
|
```
|
|
117
125
|
|
|
118
|
-
**Fallback behavior:**
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
- `
|
|
126
|
+
**Validation and Fallback behavior:**
|
|
127
|
+
- **Fail-Fast Verification**: Both `registerWebMcpTool()` and `registerProductContextTools()` throw an error if `descriptor.execute` is not a function.
|
|
128
|
+
- **Registration Options**: an external `signal` is mapped to the internal registration lifecycle signal passed to `document.modelContext.registerTool`; `exposedTo` is forwarded unchanged.
|
|
129
|
+
- If `modelContext` is not available → returns `nativeActive: false` with the same canonical plain descriptor.
|
|
130
|
+
- `unregister()` is always safe to call (noop if no native registration) and is fully idempotent.
|
|
131
|
+
|
|
132
|
+
`annotations.destructiveHint` is product/MCP policy metadata for consumers such
|
|
133
|
+
as a confirmation gateway. It does not imply that the browser will mediate or
|
|
134
|
+
confirm a destructive execution.
|
|
135
|
+
|
|
136
|
+
### Execution Context & Out-of-Band Calls
|
|
137
|
+
Standard WebMCP `execute` callbacks in the browser receive exactly one argument: the tool input. In-bound executions from the host platform invoke this callback using only the `input` argument. Internal framework callers can execute the closure with a second out-of-band execution parameter containing advanced metadata like `signal`, `source`, `descriptor`, and `onSettled`.
|
|
138
|
+
|
|
139
|
+
When a product constructs its adapter and consumer from a prebuilt
|
|
140
|
+
`createProductWebMcpBundle()` result, pass that result as the `bundle` option to
|
|
141
|
+
`registerProductContextTools()`. Registration then uses the exact canonical
|
|
142
|
+
descriptor objects from `bundle.descriptors`; it does not create a second
|
|
143
|
+
descriptor set.
|
|
122
144
|
|
|
123
145
|
## triggerWebMcpCommand(element, command, args)
|
|
124
146
|
|
package/webmcp.js
CHANGED
|
@@ -59,30 +59,87 @@ function registrationUnregister(registration) {
|
|
|
59
59
|
return () => registration?.dispose?.() || registration?.unregister?.();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
function createRegistrationAbortError() {
|
|
63
|
+
if (typeof globalThis.DOMException === 'function') {
|
|
64
|
+
return new DOMException('Registration aborted', 'AbortError');
|
|
65
|
+
}
|
|
66
|
+
let error = new Error('Registration aborted');
|
|
67
|
+
error.name = 'AbortError';
|
|
68
|
+
return error;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export async function registerWebMcpTool(options, target = globalThis.document, registrationOptions = {}) {
|
|
72
|
+
let identity = options?.name || 'unknown';
|
|
73
|
+
if (!options || typeof options.execute !== 'function') {
|
|
74
|
+
throw new Error(`ToolDescriptor ${identity} requires an execute function`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let externalSignal = registrationOptions?.signal;
|
|
78
|
+
if (externalSignal?.aborted) {
|
|
79
|
+
throw externalSignal.reason === undefined
|
|
80
|
+
? createRegistrationAbortError()
|
|
81
|
+
: externalSignal.reason;
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
let context = getModelContext(target);
|
|
69
85
|
if (!context || typeof context.registerTool !== 'function') {
|
|
70
|
-
return { nativeActive: false, descriptor:
|
|
86
|
+
return { nativeActive: false, descriptor: options, unregister: () => {} };
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
let nativeActive = true;
|
|
74
|
-
let descriptor;
|
|
75
90
|
if (typeof globalThis.HTMLElement !== 'function') {
|
|
76
91
|
nativeActive = false;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
descriptor = await createNativeToolDescriptor(options);
|
|
80
|
-
} catch {
|
|
92
|
+
}
|
|
93
|
+
if (context.nativeActive === false || context.supportsNativeToolDescriptor === false) {
|
|
81
94
|
nativeActive = false;
|
|
82
|
-
descriptor = createToolDescriptor(options);
|
|
83
95
|
}
|
|
84
|
-
|
|
85
|
-
let
|
|
96
|
+
|
|
97
|
+
let descriptor = options;
|
|
98
|
+
|
|
99
|
+
let internalController = new AbortController();
|
|
100
|
+
let internalSignal = internalController.signal;
|
|
101
|
+
|
|
102
|
+
let isUnregistered = false;
|
|
103
|
+
let unregister = (reason) => {
|
|
104
|
+
if (isUnregistered) return;
|
|
105
|
+
isUnregistered = true;
|
|
106
|
+
|
|
107
|
+
if (externalSignal) {
|
|
108
|
+
externalSignal.removeEventListener('abort', onExternalAbort);
|
|
109
|
+
}
|
|
110
|
+
if (!internalSignal.aborted) {
|
|
111
|
+
if (reason !== undefined) {
|
|
112
|
+
internalController.abort(reason);
|
|
113
|
+
} else {
|
|
114
|
+
internalController.abort();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let onExternalAbort = () => {
|
|
120
|
+
unregister(externalSignal?.reason);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (externalSignal) {
|
|
124
|
+
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let nativeOptions = { signal: internalSignal };
|
|
128
|
+
if (registrationOptions && registrationOptions.exposedTo !== undefined) {
|
|
129
|
+
nativeOptions.exposedTo = registrationOptions.exposedTo;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await context.registerTool(descriptor, nativeOptions);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (externalSignal) {
|
|
136
|
+
externalSignal.removeEventListener('abort', onExternalAbort);
|
|
137
|
+
}
|
|
138
|
+
if (!internalSignal.aborted) {
|
|
139
|
+
internalController.abort();
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
86
143
|
|
|
87
144
|
return { nativeActive, descriptor, unregister };
|
|
88
145
|
}
|
|
@@ -172,12 +229,34 @@ function looksLikeRuntimeContext(value) {
|
|
|
172
229
|
}
|
|
173
230
|
|
|
174
231
|
function resolveProductContextToolOptions(targetOrOptions, runtimeInput) {
|
|
232
|
+
let res;
|
|
233
|
+
let isOptionsBag = false;
|
|
175
234
|
if (isObject(targetOrOptions)
|
|
176
235
|
&& !targetOrOptions.modelContext
|
|
177
236
|
&& typeof targetOrOptions.registerTool !== 'function'
|
|
178
|
-
&& typeof targetOrOptions.createElement !== 'function'
|
|
179
|
-
|
|
180
|
-
|
|
237
|
+
&& typeof targetOrOptions.createElement !== 'function') {
|
|
238
|
+
let optionsKeys = [
|
|
239
|
+
'target',
|
|
240
|
+
'bundle',
|
|
241
|
+
'runtime',
|
|
242
|
+
'publishContext',
|
|
243
|
+
'executeAction',
|
|
244
|
+
'registrationOptions',
|
|
245
|
+
'safeActions',
|
|
246
|
+
'enrichContext',
|
|
247
|
+
'enrichRuntime',
|
|
248
|
+
'enrichActionDescriptor',
|
|
249
|
+
'signal',
|
|
250
|
+
'exposedTo'
|
|
251
|
+
];
|
|
252
|
+
if (optionsKeys.some(key => key in targetOrOptions)) {
|
|
253
|
+
isOptionsBag = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isOptionsBag) {
|
|
258
|
+
res = {
|
|
259
|
+
...targetOrOptions,
|
|
181
260
|
target: targetOrOptions.target || globalThis.document,
|
|
182
261
|
runtime: targetOrOptions.runtime,
|
|
183
262
|
safeActions: targetOrOptions.safeActions,
|
|
@@ -185,20 +264,27 @@ function resolveProductContextToolOptions(targetOrOptions, runtimeInput) {
|
|
|
185
264
|
enrichRuntime: targetOrOptions.enrichRuntime,
|
|
186
265
|
enrichActionDescriptor: targetOrOptions.enrichActionDescriptor,
|
|
187
266
|
publishContext: targetOrOptions.publishContext !== false,
|
|
267
|
+
registrationOptions: targetOrOptions.registrationOptions,
|
|
188
268
|
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return {
|
|
269
|
+
} else if (looksLikeRuntimeContext(targetOrOptions) && runtimeInput === undefined) {
|
|
270
|
+
res = {
|
|
192
271
|
target: globalThis.document,
|
|
193
272
|
runtime: targetOrOptions,
|
|
194
273
|
publishContext: true,
|
|
195
274
|
};
|
|
275
|
+
} else {
|
|
276
|
+
res = {
|
|
277
|
+
target: targetOrOptions || globalThis.document,
|
|
278
|
+
runtime: runtimeInput,
|
|
279
|
+
publishContext: true,
|
|
280
|
+
};
|
|
281
|
+
if (isObject(targetOrOptions)) {
|
|
282
|
+
res.executeAction = targetOrOptions.executeAction;
|
|
283
|
+
res.signal = targetOrOptions.signal;
|
|
284
|
+
res.exposedTo = targetOrOptions.exposedTo;
|
|
285
|
+
}
|
|
196
286
|
}
|
|
197
|
-
return
|
|
198
|
-
target: targetOrOptions || globalThis.document,
|
|
199
|
-
runtime: runtimeInput,
|
|
200
|
-
publishContext: true,
|
|
201
|
-
};
|
|
287
|
+
return res;
|
|
202
288
|
}
|
|
203
289
|
|
|
204
290
|
function createProductRuntimeContextPayload(contextView) {
|
|
@@ -227,7 +313,156 @@ function callContextPublisher(modelContext, methodName, payload) {
|
|
|
227
313
|
};
|
|
228
314
|
}
|
|
229
315
|
|
|
230
|
-
|
|
316
|
+
const PRODUCT_RUNTIME_PUBLICATION_STATE = Symbol('productRuntimePublicationState');
|
|
317
|
+
const PRODUCT_RUNTIME_CONTEXT_PROPERTIES = [PRODUCT_RUNTIME_CONTEXT_NAME, 'productRuntimeContext'];
|
|
318
|
+
|
|
319
|
+
function snapshotOwnPropertyState(target, property) {
|
|
320
|
+
let descriptor = Object.getOwnPropertyDescriptor(target, property);
|
|
321
|
+
try {
|
|
322
|
+
return { descriptor, valueReadable: true, value: target[property] };
|
|
323
|
+
} catch {
|
|
324
|
+
return { descriptor, valueReadable: false, value: undefined };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function propertyDescriptorsEqual(left, right) {
|
|
329
|
+
if (!left || !right) return left === right;
|
|
330
|
+
if (left.configurable !== right.configurable || left.enumerable !== right.enumerable) return false;
|
|
331
|
+
let leftIsData = 'value' in left || 'writable' in left;
|
|
332
|
+
let rightIsData = 'value' in right || 'writable' in right;
|
|
333
|
+
if (leftIsData !== rightIsData) return false;
|
|
334
|
+
if (leftIsData) {
|
|
335
|
+
return left.writable === right.writable && Object.is(left.value, right.value);
|
|
336
|
+
}
|
|
337
|
+
return left.get === right.get && left.set === right.set;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function propertyStatesEqual(left, right) {
|
|
341
|
+
if (!propertyDescriptorsEqual(left.descriptor, right.descriptor)) return false;
|
|
342
|
+
if (left.valueReadable !== right.valueReadable) return false;
|
|
343
|
+
return !left.valueReadable || Object.is(left.value, right.value);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function restoreOwnPropertyState(target, property, state) {
|
|
347
|
+
if (state.descriptor) {
|
|
348
|
+
Object.defineProperty(target, property, state.descriptor);
|
|
349
|
+
if (!('value' in state.descriptor) && state.valueReadable) {
|
|
350
|
+
let current = snapshotOwnPropertyState(target, property);
|
|
351
|
+
let valueDiffers = !current.valueReadable || !Object.is(current.value, state.value);
|
|
352
|
+
if (typeof state.descriptor.set === 'function' && valueDiffers) {
|
|
353
|
+
try {
|
|
354
|
+
Reflect.set(target, property, state.value, target);
|
|
355
|
+
} catch {}
|
|
356
|
+
Object.defineProperty(target, property, state.descriptor);
|
|
357
|
+
} else if (typeof state.descriptor.set !== 'function' && valueDiffers) {
|
|
358
|
+
throw new Error(`Unable to restore ${property}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!propertyStatesEqual(snapshotOwnPropertyState(target, property), state)) {
|
|
362
|
+
throw new Error(`Unable to restore ${property}`);
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!Reflect.deleteProperty(target, property)) {
|
|
368
|
+
throw new Error(`Unable to restore ${property}`);
|
|
369
|
+
}
|
|
370
|
+
let current = snapshotOwnPropertyState(target, property);
|
|
371
|
+
if (state.valueReadable && (!current.valueReadable || !Object.is(current.value, state.value))) {
|
|
372
|
+
if (!Reflect.set(target, property, state.value, target)) {
|
|
373
|
+
throw new Error(`Unable to restore ${property}`);
|
|
374
|
+
}
|
|
375
|
+
if (Object.hasOwn(target, property) && !Reflect.deleteProperty(target, property)) {
|
|
376
|
+
throw new Error(`Unable to restore ${property}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!propertyStatesEqual(snapshotOwnPropertyState(target, property), state)) {
|
|
380
|
+
throw new Error(`Unable to restore ${property}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function restorePropertyPublication(target, published, baseline) {
|
|
385
|
+
let errors = [];
|
|
386
|
+
for (let property of [...PRODUCT_RUNTIME_CONTEXT_PROPERTIES].reverse()) {
|
|
387
|
+
try {
|
|
388
|
+
let current = snapshotOwnPropertyState(target, property);
|
|
389
|
+
if (propertyStatesEqual(current, published.get(property))) {
|
|
390
|
+
restoreOwnPropertyState(target, property, baseline.get(property));
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
errors.push(error);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (errors.length) {
|
|
397
|
+
throw new AggregateError(errors, 'Failed to restore product runtime context publication');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function createPropertyRuntimeContextPublication(modelContext, payload, replaces) {
|
|
402
|
+
let immediateBaseline = new Map(PRODUCT_RUNTIME_CONTEXT_PROPERTIES.map((property) => (
|
|
403
|
+
[property, snapshotOwnPropertyState(modelContext, property)]
|
|
404
|
+
)));
|
|
405
|
+
let replacedState = replaces?.[PRODUCT_RUNTIME_PUBLICATION_STATE];
|
|
406
|
+
let finalBaseline = new Map(PRODUCT_RUNTIME_CONTEXT_PROPERTIES.map((property) => {
|
|
407
|
+
let current = immediateBaseline.get(property);
|
|
408
|
+
let canInherit = replacedState?.type === 'property'
|
|
409
|
+
&& replacedState.active
|
|
410
|
+
&& replacedState.modelContext === modelContext
|
|
411
|
+
&& propertyStatesEqual(current, replacedState.published.get(property));
|
|
412
|
+
return [property, canInherit ? replacedState.finalBaseline.get(property) : current];
|
|
413
|
+
}));
|
|
414
|
+
let published = new Map();
|
|
415
|
+
let attempted = [];
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
for (let property of PRODUCT_RUNTIME_CONTEXT_PROPERTIES) {
|
|
419
|
+
attempted.push(property);
|
|
420
|
+
modelContext[property] = payload.value;
|
|
421
|
+
published.set(property, snapshotOwnPropertyState(modelContext, property));
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
let rollbackErrors = [];
|
|
425
|
+
for (let property of attempted.reverse()) {
|
|
426
|
+
try {
|
|
427
|
+
restoreOwnPropertyState(modelContext, property, immediateBaseline.get(property));
|
|
428
|
+
} catch (rollbackError) {
|
|
429
|
+
rollbackErrors.push(rollbackError);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (rollbackErrors.length) {
|
|
433
|
+
throw new AggregateError([error, ...rollbackErrors], error.message, { cause: error });
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let state = {
|
|
439
|
+
type: 'property',
|
|
440
|
+
modelContext,
|
|
441
|
+
published,
|
|
442
|
+
finalBaseline,
|
|
443
|
+
active: true,
|
|
444
|
+
rollback() {
|
|
445
|
+
if (!state.active) return;
|
|
446
|
+
state.active = false;
|
|
447
|
+
restorePropertyPublication(modelContext, published, immediateBaseline);
|
|
448
|
+
},
|
|
449
|
+
unregister() {
|
|
450
|
+
if (!state.active) return;
|
|
451
|
+
state.active = false;
|
|
452
|
+
restorePropertyPublication(modelContext, published, finalBaseline);
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
return {
|
|
456
|
+
published: true,
|
|
457
|
+
nativeActive: false,
|
|
458
|
+
method: 'property',
|
|
459
|
+
payload,
|
|
460
|
+
unregister: state.unregister,
|
|
461
|
+
[PRODUCT_RUNTIME_PUBLICATION_STATE]: state,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function publishProductRuntimeContextInternal(contextView, target, replaces = null) {
|
|
231
466
|
let modelContext = getModelContext(target);
|
|
232
467
|
let payload = createProductRuntimeContextPayload(contextView);
|
|
233
468
|
if (!modelContext) {
|
|
@@ -243,22 +478,20 @@ export function publishProductRuntimeContext(contextView, target = globalThis.do
|
|
|
243
478
|
}
|
|
244
479
|
}
|
|
245
480
|
|
|
246
|
-
modelContext
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
},
|
|
261
|
-
};
|
|
481
|
+
return createPropertyRuntimeContextPublication(modelContext, payload, replaces);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function rollbackRuntimeContextPublication(publication) {
|
|
485
|
+
let state = publication?.[PRODUCT_RUNTIME_PUBLICATION_STATE];
|
|
486
|
+
if (state?.type === 'property') {
|
|
487
|
+
state.rollback();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
publication?.unregister?.();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function publishProductRuntimeContext(contextView, target = globalThis.document) {
|
|
494
|
+
return publishProductRuntimeContextInternal(contextView, target);
|
|
262
495
|
}
|
|
263
496
|
|
|
264
497
|
function resolveProductAction(context, action = {}) {
|
|
@@ -282,6 +515,7 @@ export function createProductActionToolDescriptor(productContext, action) {
|
|
|
282
515
|
let context = normalizeProductContext(productContext);
|
|
283
516
|
let actionRecord = resolveProductAction(context, action);
|
|
284
517
|
let productDescription = context.webmcp.productDescription;
|
|
518
|
+
let isDestructive = Boolean(actionRecord.destructive);
|
|
285
519
|
return createToolDescriptor({
|
|
286
520
|
name: actionRecord.name,
|
|
287
521
|
description: [
|
|
@@ -302,7 +536,8 @@ export function createProductActionToolDescriptor(productContext, action) {
|
|
|
302
536
|
entityRefs: actionRecord.entityRefs || [],
|
|
303
537
|
viewRefs: actionRecord.viewRefs || [],
|
|
304
538
|
permission: actionRecord.permission || '',
|
|
305
|
-
destructive:
|
|
539
|
+
destructive: isDestructive,
|
|
540
|
+
destructiveHint: isDestructive,
|
|
306
541
|
allowed: actionRecord.allowed !== false,
|
|
307
542
|
actionPolicy: context.webmcp.actionPolicy,
|
|
308
543
|
intent: actionRecord.intent || {},
|
|
@@ -1535,11 +1770,34 @@ export function createProductWebMcpBundle(productContext, options = {}) {
|
|
|
1535
1770
|
}
|
|
1536
1771
|
|
|
1537
1772
|
let agentView = createProductContextAgentView(context);
|
|
1538
|
-
let
|
|
1773
|
+
let allowedActions = context.actions.filter((action) => action.allowed !== false);
|
|
1774
|
+
let descriptors = allowedActions.map((action) => createProductActionToolDescriptor(context, action));
|
|
1775
|
+
|
|
1539
1776
|
if (typeof options.enrichActionDescriptor === 'function') {
|
|
1540
|
-
descriptors = descriptors.map((descriptor, index) =>
|
|
1541
|
-
|
|
1542
|
-
|
|
1777
|
+
descriptors = descriptors.map((descriptor, index) => {
|
|
1778
|
+
let action = allowedActions[index];
|
|
1779
|
+
return options.enrichActionDescriptor({ ...descriptor }, action, context) || descriptor;
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (typeof options.executeAction === 'function') {
|
|
1784
|
+
descriptors.forEach((descriptor, index) => {
|
|
1785
|
+
let action = allowedActions[index];
|
|
1786
|
+
descriptor.execute = function(input, outOfBand) {
|
|
1787
|
+
let oob = (outOfBand && typeof outOfBand === 'object') ? outOfBand : {};
|
|
1788
|
+
let currentDescriptor = oob.descriptor || descriptor;
|
|
1789
|
+
let command = { tool: currentDescriptor.name || descriptor.name, input };
|
|
1790
|
+
let executionContext = {
|
|
1791
|
+
action,
|
|
1792
|
+
descriptor: currentDescriptor,
|
|
1793
|
+
productContext: context,
|
|
1794
|
+
signal: oob.signal,
|
|
1795
|
+
source: oob.source,
|
|
1796
|
+
onSettled: oob.onSettled,
|
|
1797
|
+
};
|
|
1798
|
+
return options.executeAction(command, executionContext);
|
|
1799
|
+
};
|
|
1800
|
+
});
|
|
1543
1801
|
}
|
|
1544
1802
|
|
|
1545
1803
|
return {
|
|
@@ -1553,6 +1811,25 @@ export function createProductWebMcpBundle(productContext, options = {}) {
|
|
|
1553
1811
|
};
|
|
1554
1812
|
}
|
|
1555
1813
|
|
|
1814
|
+
function validateProductWebMcpBundle(bundle) {
|
|
1815
|
+
if (!isObject(bundle)) {
|
|
1816
|
+
throw new Error('Product WebMCP bundle must be an object');
|
|
1817
|
+
}
|
|
1818
|
+
if (!isObject(bundle.context)) {
|
|
1819
|
+
throw new Error('Product WebMCP bundle requires context');
|
|
1820
|
+
}
|
|
1821
|
+
if (!isObject(bundle.contextView)) {
|
|
1822
|
+
throw new Error('Product WebMCP bundle requires contextView');
|
|
1823
|
+
}
|
|
1824
|
+
if (!isObject(bundle.agentView)) {
|
|
1825
|
+
throw new Error('Product WebMCP bundle requires agentView');
|
|
1826
|
+
}
|
|
1827
|
+
if (!Array.isArray(bundle.descriptors)) {
|
|
1828
|
+
throw new Error('Product WebMCP bundle requires a descriptors array');
|
|
1829
|
+
}
|
|
1830
|
+
return bundle;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1556
1833
|
function entryCandidateValue(entry, key) {
|
|
1557
1834
|
if (!entry || !key) return undefined;
|
|
1558
1835
|
if (entry[key] !== undefined) return entry[key];
|
|
@@ -1762,20 +2039,57 @@ export function createWebMcpHooks({ observer } = {}) {
|
|
|
1762
2039
|
|
|
1763
2040
|
export async function registerProductContextTools(productContext, targetOrOptions = globalThis.document, runtimeInput) {
|
|
1764
2041
|
let options = resolveProductContextToolOptions(targetOrOptions, runtimeInput);
|
|
2042
|
+
let bundle = options.bundle === undefined
|
|
2043
|
+
? createProductWebMcpBundle(productContext, options)
|
|
2044
|
+
: validateProductWebMcpBundle(options.bundle);
|
|
1765
2045
|
let {
|
|
1766
2046
|
context,
|
|
1767
2047
|
agentView,
|
|
1768
2048
|
contextView,
|
|
1769
|
-
runtime,
|
|
1770
2049
|
descriptors,
|
|
1771
|
-
} =
|
|
2050
|
+
} = bundle;
|
|
2051
|
+
|
|
2052
|
+
for (let [index, descriptor] of descriptors.entries()) {
|
|
2053
|
+
if (!isObject(descriptor)) {
|
|
2054
|
+
throw new Error(`Product WebMCP bundle descriptor ${index} must be an object`);
|
|
2055
|
+
}
|
|
2056
|
+
if (typeof descriptor.execute !== 'function') {
|
|
2057
|
+
let identity = descriptor.name || descriptor.annotations?.actionId || 'unknown';
|
|
2058
|
+
throw new Error(`ToolDescriptor ${identity} requires an execute function`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1772
2062
|
let registrations = [];
|
|
1773
|
-
|
|
1774
|
-
|
|
2063
|
+
let publication = { published: false, nativeActive: false, method: '', payload: null, unregister: () => {} };
|
|
2064
|
+
try {
|
|
2065
|
+
for (let descriptor of descriptors) {
|
|
2066
|
+
let regOpts = {};
|
|
2067
|
+
if (typeof options.registrationOptions === 'function') {
|
|
2068
|
+
regOpts = options.registrationOptions(descriptor) || {};
|
|
2069
|
+
} else if (isObject(options.registrationOptions)) {
|
|
2070
|
+
regOpts = options.registrationOptions;
|
|
2071
|
+
} else {
|
|
2072
|
+
regOpts = {
|
|
2073
|
+
signal: options.signal,
|
|
2074
|
+
exposedTo: options.exposedTo,
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
registrations.push(await registerWebMcpTool(descriptor, options.target, regOpts));
|
|
2078
|
+
}
|
|
2079
|
+
if (options.publishContext) {
|
|
2080
|
+
publication = publishProductRuntimeContext(contextView, options.target);
|
|
2081
|
+
}
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
try {
|
|
2084
|
+
publication.unregister?.();
|
|
2085
|
+
} catch {}
|
|
2086
|
+
for (let reg of [...registrations].reverse()) {
|
|
2087
|
+
try {
|
|
2088
|
+
reg.unregister?.();
|
|
2089
|
+
} catch {}
|
|
2090
|
+
}
|
|
2091
|
+
throw err;
|
|
1775
2092
|
}
|
|
1776
|
-
let publication = options.publishContext
|
|
1777
|
-
? publishProductRuntimeContext(contextView, options.target)
|
|
1778
|
-
: { published: false, nativeActive: false, method: '', payload: null, unregister: () => {} };
|
|
1779
2093
|
let producer = {
|
|
1780
2094
|
nativeActive: registrations.some((registration) => registration.nativeActive),
|
|
1781
2095
|
context,
|
|
@@ -1786,19 +2100,88 @@ export async function registerProductContextTools(productContext, targetOrOption
|
|
|
1786
2100
|
descriptors,
|
|
1787
2101
|
registrations,
|
|
1788
2102
|
refresh(nextRuntime = options.runtime, refreshOptions = {}) {
|
|
1789
|
-
let
|
|
2103
|
+
let refreshedOptions = {
|
|
1790
2104
|
...options,
|
|
1791
2105
|
...refreshOptions,
|
|
1792
2106
|
runtime: nextRuntime,
|
|
1793
2107
|
publishContext: false,
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
producer.
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
2108
|
+
};
|
|
2109
|
+
delete refreshedOptions.bundle;
|
|
2110
|
+
let refreshed = createProductWebMcpBundle(context, refreshedOptions);
|
|
2111
|
+
let descriptorUpdates = producer.descriptors.map((descriptor) => ({
|
|
2112
|
+
descriptor,
|
|
2113
|
+
snapshot: Object.getOwnPropertyDescriptors(descriptor),
|
|
2114
|
+
refreshed: refreshed.descriptors.find(
|
|
2115
|
+
(candidate) => candidate.name === descriptor.name
|
|
2116
|
+
|| (candidate.annotations?.actionId && candidate.annotations.actionId === descriptor.annotations?.actionId)
|
|
2117
|
+
),
|
|
2118
|
+
}));
|
|
2119
|
+
let shouldPublish = options.publishContext !== false && refreshOptions.publishContext !== false;
|
|
2120
|
+
let previousPublication = publication;
|
|
2121
|
+
let stagedPublication = shouldPublish
|
|
2122
|
+
? publishProductRuntimeContextInternal(
|
|
2123
|
+
refreshed.contextView,
|
|
2124
|
+
refreshOptions.target || options.target,
|
|
2125
|
+
previousPublication,
|
|
2126
|
+
)
|
|
2127
|
+
: previousPublication;
|
|
2128
|
+
let producerSnapshot = Object.getOwnPropertyDescriptors(producer);
|
|
2129
|
+
|
|
2130
|
+
try {
|
|
2131
|
+
for (let update of descriptorUpdates) {
|
|
2132
|
+
if (!update.refreshed) continue;
|
|
2133
|
+
let previousExecute = update.descriptor.execute;
|
|
2134
|
+
for (let key of Object.keys(update.descriptor)) {
|
|
2135
|
+
if (key === 'execute' && typeof update.refreshed.execute !== 'function') continue;
|
|
2136
|
+
if (!(key in update.refreshed)) delete update.descriptor[key];
|
|
2137
|
+
}
|
|
2138
|
+
Object.assign(update.descriptor, update.refreshed);
|
|
2139
|
+
if (typeof update.refreshed.execute !== 'function') {
|
|
2140
|
+
update.descriptor.execute = previousExecute;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
producer.contextView = refreshed.contextView;
|
|
2145
|
+
producer.runtime = refreshed.contextView.runtime;
|
|
2146
|
+
if (shouldPublish) producer.publication = stagedPublication;
|
|
2147
|
+
contextView = refreshed.contextView;
|
|
2148
|
+
if (shouldPublish) publication = stagedPublication;
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
let rollbackErrors = [];
|
|
2151
|
+
for (let update of [...descriptorUpdates].reverse()) {
|
|
2152
|
+
try {
|
|
2153
|
+
for (let key of Reflect.ownKeys(update.descriptor)) {
|
|
2154
|
+
if (!Object.hasOwn(update.snapshot, key) && !Reflect.deleteProperty(update.descriptor, key)) {
|
|
2155
|
+
throw new Error(`Unable to restore descriptor ${update.descriptor.name || 'unknown'}`);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
Object.defineProperties(update.descriptor, update.snapshot);
|
|
2159
|
+
} catch (rollbackError) {
|
|
2160
|
+
rollbackErrors.push(rollbackError);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
try {
|
|
2164
|
+
Object.defineProperties(producer, producerSnapshot);
|
|
2165
|
+
} catch (rollbackError) {
|
|
2166
|
+
rollbackErrors.push(rollbackError);
|
|
2167
|
+
}
|
|
2168
|
+
if (shouldPublish) {
|
|
2169
|
+
try {
|
|
2170
|
+
rollbackRuntimeContextPublication(stagedPublication);
|
|
2171
|
+
} catch (rollbackError) {
|
|
2172
|
+
rollbackErrors.push(rollbackError);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (rollbackErrors.length) {
|
|
2176
|
+
throw new AggregateError([error, ...rollbackErrors], error.message, { cause: error });
|
|
2177
|
+
}
|
|
2178
|
+
throw error;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
if (shouldPublish) {
|
|
2182
|
+
try {
|
|
2183
|
+
previousPublication.unregister?.();
|
|
2184
|
+
} catch {}
|
|
1802
2185
|
}
|
|
1803
2186
|
return producer;
|
|
1804
2187
|
},
|