symbiote-ui 0.3.0-alpha.63 → 0.3.0-alpha.64

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 CHANGED
@@ -4,6 +4,26 @@ All notable changes to `symbiote-ui` will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.3.0-alpha.64] - 2026-07-18
8
+
9
+ ### Added
10
+
11
+ - Added `destructiveHint: true` under `annotations` on the descriptor for destructive action descriptors.
12
+ - Added a `bundle` option to `registerProductContextTools()` so products can register the exact executable descriptors already used to construct their adapter and consumer.
13
+
14
+ ### Fixed
15
+
16
+ - Removed the unused legacy `createNativeToolDescriptor()` export now that
17
+ registration uses canonical executable plain descriptors directly.
18
+ - 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.
19
+ - 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.
20
+ - Fixed native capability check to read active `modelContext` markers (`nativeActive` and `supportsNativeToolDescriptor`) directly, removing public options bag overrides.
21
+ - Fixed native options forwarding to pass exactly `signal` and `exposedTo` options to `registerTool`.
22
+ - Fixed product registration options resolver to correctly identify options bags containing `signal` or `exposedTo`.
23
+ - 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.
24
+ - Fixed tool validation to fail fast with tool/action identity before the first native registration, rolling back previous registrations on partial failure.
25
+ - Fixed runtime-context property publication to restore prior property state exactly and made producer refresh publication replacement atomic.
26
+
7
27
  ## [0.3.0-alpha.63] - 2026-07-17
8
28
 
9
29
  - Keep cascade-theme control foregrounds above WCAG AA after browser RGB quantization.
@@ -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
- | navigator.modelContext | shimmed | symbiote-webmcp-shim | WebMCP tool registration, tour context, agent-visible component actions | `navigator.modelContext` or `document.modelContext`; otherwise install `symbiote-webmcp-shim` |
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
- - `navigator.modelContext` is intentionally shimmed until the WebMCP surface is
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiote-ui",
3
- "version": "0.3.0-alpha.63",
3
+ "version": "0.3.0-alpha.64",
4
4
  "type": "module",
5
5
  "description": "Agent-ready Web Components, layouts, themes, manifests, and UI contracts for Symbiote systems.",
6
6
  "main": "index.js",
@@ -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 `navigator.modelContext` (if available).
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 || navigator.modelContext || null
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 // Cleanup 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
- - If `modelContext` is not available returns `nativeActive: false` with a plain descriptor.
120
- - If native `ToolDescriptor` import fails falls back to plain object.
121
- - `unregister()` is always safe to call (noop if no native registration).
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
- export async function createNativeToolDescriptor(options) {
63
- let { ToolDescriptor } = await import('@symbiotejs/symbiote/webmcp');
64
- return new ToolDescriptor(options);
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: createToolDescriptor(options), unregister: () => {} };
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
- descriptor = createToolDescriptor(options);
78
- } else try {
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
- let registration = await context.registerTool(descriptor, registrationOptions);
85
- let unregister = registrationUnregister(registration);
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
- && ('target' in targetOrOptions || 'runtime' in targetOrOptions || 'publishContext' in targetOrOptions)) {
180
- return {
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
- if (looksLikeRuntimeContext(targetOrOptions) && runtimeInput === undefined) {
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
- export function publishProductRuntimeContext(contextView, target = globalThis.document) {
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[PRODUCT_RUNTIME_CONTEXT_NAME] = payload.value;
247
- modelContext.productRuntimeContext = payload.value;
248
- return {
249
- published: true,
250
- nativeActive: false,
251
- method: 'property',
252
- payload,
253
- unregister() {
254
- if (modelContext[PRODUCT_RUNTIME_CONTEXT_NAME] === payload.value) {
255
- delete modelContext[PRODUCT_RUNTIME_CONTEXT_NAME];
256
- }
257
- if (modelContext.productRuntimeContext === payload.value) {
258
- delete modelContext.productRuntimeContext;
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: Boolean(actionRecord.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 descriptors = createProductContextToolDescriptors(context);
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
- options.enrichActionDescriptor({ ...descriptor }, context.actions[index], context) || descriptor
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
- } = createProductWebMcpBundle(productContext, options);
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
- for (let descriptor of descriptors) {
1774
- registrations.push(await registerWebMcpTool(descriptor, options.target));
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 refreshed = createProductWebMcpBundle(context, {
2103
+ let refreshedOptions = {
1790
2104
  ...options,
1791
2105
  ...refreshOptions,
1792
2106
  runtime: nextRuntime,
1793
2107
  publishContext: false,
1794
- });
1795
- contextView = refreshed.contextView;
1796
- producer.contextView = contextView;
1797
- producer.runtime = contextView.runtime;
1798
- if (options.publishContext !== false && refreshOptions.publishContext !== false) {
1799
- publication.unregister?.();
1800
- publication = publishProductRuntimeContext(contextView, refreshOptions.target || options.target);
1801
- producer.publication = publication;
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
  },