@webmcp-auto-ui/core 2.5.26 → 2.5.28

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/src/polyfill.ts CHANGED
@@ -4,8 +4,10 @@
4
4
  * Implements the 17 MUST HAVE features of the W3C WebMCP Draft CG Report
5
5
  * (2026-03-27) plus ecosystem extras.
6
6
  *
7
- * Architecture: module-level state (no class) for tree-shakability.
8
- * Zero external dependencies.
7
+ * Architecture: shared module-level state (toolMap, polyfillOptions,
8
+ * installState) mutated by installPolyfill/uninstallPolyfill. Installs a
9
+ * single `navigator.modelContext` descriptor at init time; cleanup restores
10
+ * the previous descriptor. Zero external dependencies.
9
11
  *
10
12
  * @license AGPL-3.0-or-later
11
13
  */
@@ -39,6 +41,14 @@ import { validateJsonSchema } from './validate.js';
39
41
  /** Install state for save/restore of previous descriptors (Fix 7) */
40
42
  interface InstallState {
41
43
  installed: boolean;
44
+ /**
45
+ * True only when we actually overrode navigator.modelContext. When native
46
+ * WebMCP is present and forcePolyfill is false we short-circuit installation
47
+ * and MUST NOT touch navigator on cleanup — otherwise we would delete the
48
+ * native implementation.
49
+ */
50
+ didOverride: boolean;
51
+ didOverrideTesting: boolean;
42
52
  previousModelContextDescriptor?: PropertyDescriptor;
43
53
  previousModelContextTestingDescriptor?: PropertyDescriptor;
44
54
  }
@@ -60,7 +70,7 @@ let toolsChangedCallback: ToolsChangedCallback | null = null;
60
70
  let polyfillOptions: WebMCPPolyfillOptions = {};
61
71
 
62
72
  /** Install state — tracks whether we installed and the previous descriptors */
63
- const installState: InstallState = { installed: false };
73
+ const installState: InstallState = { installed: false, didOverride: false, didOverrideTesting: false };
64
74
 
65
75
  // ---------------------------------------------------------------------------
66
76
  // Internal helpers
@@ -753,6 +763,8 @@ export function initializeWebMCPPolyfill(
753
763
  // forcePolyfill is checked BEFORE the native skip so it takes precedence
754
764
  if (hasNativeWebMCP() && !polyfillOptions.forcePolyfill) {
755
765
  installState.installed = true;
766
+ installState.didOverride = false;
767
+ installState.didOverrideTesting = false;
756
768
  return;
757
769
  }
758
770
 
@@ -781,6 +793,7 @@ export function initializeWebMCPPolyfill(
781
793
  writable: false,
782
794
  enumerable: true,
783
795
  });
796
+ installState.didOverride = true;
784
797
 
785
798
  // Fix 14 — installTestingShim option
786
799
  const shimOption = polyfillOptions.installTestingShim ?? 'if-missing';
@@ -794,6 +807,7 @@ export function initializeWebMCPPolyfill(
794
807
  writable: false,
795
808
  enumerable: true,
796
809
  });
810
+ installState.didOverrideTesting = true;
797
811
  }
798
812
  }
799
813
 
@@ -818,32 +832,39 @@ export function cleanupWebMCPPolyfill(): void {
818
832
  // Clear callback slot
819
833
  toolsChangedCallback = null;
820
834
 
821
- // Fix 7 — Restore previous descriptors instead of deleting
835
+ // Fix 7 — Restore previous descriptors instead of deleting.
836
+ // Guard with didOverride/didOverrideTesting: when native WebMCP was present
837
+ // and we short-circuited install, we never defined properties on navigator
838
+ // and must NOT delete or overwrite them here (that would remove the native).
822
839
  if (typeof navigator !== 'undefined') {
823
840
  try {
824
- if (installState.previousModelContextDescriptor) {
825
- Object.defineProperty(
826
- navigator,
827
- 'modelContext',
828
- installState.previousModelContextDescriptor,
829
- );
830
- } else {
831
- delete (navigator as Navigator & Record<string, unknown>).modelContext;
841
+ if (installState.didOverride) {
842
+ if (installState.previousModelContextDescriptor) {
843
+ Object.defineProperty(
844
+ navigator,
845
+ 'modelContext',
846
+ installState.previousModelContextDescriptor,
847
+ );
848
+ } else {
849
+ delete (navigator as Navigator & Record<string, unknown>).modelContext;
850
+ }
832
851
  }
833
852
 
834
- if (installState.previousModelContextTestingDescriptor) {
835
- Object.defineProperty(
836
- navigator,
837
- 'modelContextTesting',
838
- installState.previousModelContextTestingDescriptor,
839
- );
840
- } else {
841
- const testingDesc = Object.getOwnPropertyDescriptor(
842
- navigator,
843
- 'modelContextTesting',
844
- );
845
- if (testingDesc?.configurable) {
846
- delete (navigator as Navigator & Record<string, unknown>).modelContextTesting;
853
+ if (installState.didOverrideTesting) {
854
+ if (installState.previousModelContextTestingDescriptor) {
855
+ Object.defineProperty(
856
+ navigator,
857
+ 'modelContextTesting',
858
+ installState.previousModelContextTestingDescriptor,
859
+ );
860
+ } else {
861
+ const testingDesc = Object.getOwnPropertyDescriptor(
862
+ navigator,
863
+ 'modelContextTesting',
864
+ );
865
+ if (testingDesc?.configurable) {
866
+ delete (navigator as Navigator & Record<string, unknown>).modelContextTesting;
867
+ }
847
868
  }
848
869
  }
849
870
  } catch (e) {
@@ -854,6 +875,8 @@ export function cleanupWebMCPPolyfill(): void {
854
875
  // Reset options and install state
855
876
  polyfillOptions = {};
856
877
  installState.installed = false;
878
+ installState.didOverride = false;
879
+ installState.didOverrideTesting = false;
857
880
  installState.previousModelContextDescriptor = undefined;
858
881
  installState.previousModelContextTestingDescriptor = undefined;
859
882
  }
@@ -586,12 +586,20 @@ export function createWebMcpServer(
586
586
  const allTools = [...customTools];
587
587
 
588
588
  if (builtinTools) {
589
- // Rebuild widget_display description with current widget names
589
+ // Rebuild widget_display description with current widget names.
590
+ // Clone the tool descriptor rather than mutating builtinTools in place,
591
+ // so multiple servers / repeated layer() calls don't stomp on each
592
+ // other's description (the function refs are preserved).
590
593
  const names = [...widgets.keys()];
591
- const displayTool = builtinTools.find(t => t.name === 'widget_display')!;
592
- displayTool.description = `Render a widget on the user's canvas with the specified type and parameters. Use this tool whenever you need to display data visually — charts, tables, statistics, timelines, maps, galleries, etc. The widget type must match one of the available widget names (use search_recipes or get_recipe first to learn the exact schema). Returns the widget's unique ID for later updates via the canvas tool. Do NOT use this tool for plain text responses. Available widgets: ${names.join(', ')}.`;
594
+ const dynamicDescription = `Render a widget on the user's canvas with the specified type and parameters. Use this tool whenever you need to display data visually — charts, tables, statistics, timelines, maps, galleries, etc. The widget type must match one of the available widget names (use search_recipes or get_recipe first to learn the exact schema). Returns the widget's unique ID for later updates via the canvas tool. Do NOT use this tool for plain text responses. Available widgets: ${names.join(', ')}.`;
593
595
 
594
- allTools.push(...builtinTools);
596
+ for (const tool of builtinTools) {
597
+ if (tool.name === 'widget_display') {
598
+ allTools.push({ ...tool, description: dynamicDescription });
599
+ } else {
600
+ allTools.push({ ...tool });
601
+ }
602
+ }
595
603
  }
596
604
 
597
605
  // Merge flow recipes into the layer's recipe list so the UI can browse them