@seed-ship/mcp-ui-solid 6.4.0 → 6.5.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/components/UIResourceRenderer.cjs +40 -10
  3. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  4. package/dist/components/UIResourceRenderer.d.ts +20 -0
  5. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  6. package/dist/components/UIResourceRenderer.js +42 -12
  7. package/dist/components/UIResourceRenderer.js.map +1 -1
  8. package/dist/index.cjs +4 -0
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +3 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/utils/duplicate-mount-registry.cjs +27 -0
  16. package/dist/utils/duplicate-mount-registry.cjs.map +1 -0
  17. package/dist/utils/duplicate-mount-registry.d.ts +84 -0
  18. package/dist/utils/duplicate-mount-registry.d.ts.map +1 -0
  19. package/dist/utils/duplicate-mount-registry.js +27 -0
  20. package/dist/utils/duplicate-mount-registry.js.map +1 -0
  21. package/dist/utils/stable-key.cjs +41 -0
  22. package/dist/utils/stable-key.cjs.map +1 -0
  23. package/dist/utils/stable-key.d.ts +33 -0
  24. package/dist/utils/stable-key.d.ts.map +1 -0
  25. package/dist/utils/stable-key.js +41 -0
  26. package/dist/utils/stable-key.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/components/UIResourceRenderer.identity.test.tsx +161 -0
  29. package/src/components/UIResourceRenderer.tsx +63 -2
  30. package/src/index.ts +8 -0
  31. package/src/utils/duplicate-mount-registry.test.ts +82 -0
  32. package/src/utils/duplicate-mount-registry.ts +113 -0
  33. package/src/utils/stable-key.test.ts +96 -0
  34. package/src/utils/stable-key.ts +91 -0
  35. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,106 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [6.5.0] - 2026-05-05
9
+
10
+ Closes Demande 1 + Demande 2 of `deposium_solid`'s
11
+ `BRIEF-MCPUI-2026-05-10.md` (rescue-duplicate root-cause investigation).
12
+
13
+ ### Added — `getUiResourceStableKey(content)` helper
14
+
15
+ New public function exported from the package root :
16
+
17
+ ```ts
18
+ import { getUiResourceStableKey } from '@seed-ship/mcp-ui-solid'
19
+
20
+ getUiResourceStableKey({ id: 'dashboard-q3', components: [...] })
21
+ // → 'dashboard-q3' (passthrough)
22
+
23
+ getUiResourceStableKey({ type: 'chart', params: { ... } })
24
+ // → 'a4f3b91' (FNV-1a 32-bit, 7 chars base36)
25
+ ```
26
+
27
+ If `content.id` is a non-empty string, the helper returns it verbatim.
28
+ Otherwise it derives a deterministic hash from the content with `id` and
29
+ `metadata.generatedAt` stripped — stable across renders for the same
30
+ logical payload, sync, no peer dependency.
31
+
32
+ This is the canonical implementation of the spec's "bare payload"
33
+ fallback policy (cf. `mcp-ui-spec` README → §Runtime Payload Identity).
34
+ Host apps that pre-process payloads before passing them to a renderer
35
+ (e.g. wrapping a bare chart config into a layout) should reuse this
36
+ helper instead of generating `Date.now()` or counter-based ids — those
37
+ break `<For>` reconciliation and double-mount detection.
38
+
39
+ ### Added — opt-in duplicate-mount observability
40
+
41
+ `<UIResourceRenderer>` now exposes two ways for consumers to detect
42
+ when the same content key is mounted concurrently more than once :
43
+
44
+ 1. **Per-instance callback** :
45
+
46
+ ```tsx
47
+ <UIResourceRenderer
48
+ content={layout}
49
+ onMountDuplicate={({ key, count, firstMountedAt }) => {
50
+ console.warn('[app] duplicate mount', { key, count })
51
+ }}
52
+ />
53
+ ```
54
+
55
+ 2. **Module-level reporter** (app-wide telemetry) :
56
+
57
+ ```ts
58
+ import { setDuplicateMountReporter } from '@seed-ship/mcp-ui-solid'
59
+
60
+ setDuplicateMountReporter(({ key, count }) => {
61
+ telemetry.warn('mcp-ui.duplicate-mount', { key, count })
62
+ })
63
+ ```
64
+
65
+ The new `debugDuplicateMounts` prop forces a `console.warn` from a
66
+ single instance even when the global `setDebugMode()` flag is off —
67
+ useful when you want to diagnose one suspect surface without flipping
68
+ the global switch.
69
+
70
+ **The renderer never deduplicates visually on its own.** Hiding a 2nd
71
+ mount would mask parent-framework bugs and could remove legitimate
72
+ co-mounts (drawer + main panel showing the same card). Consumers who
73
+ want dedup implement it on top of the reported events.
74
+
75
+ ### Added — `data-mcp-ui-{layout|component}-id` DOM attributes
76
+
77
+ Every `<UIResourceRenderer>` wrapper now carries a stable identity
78
+ attribute :
79
+
80
+ - The outer wrapper carries `data-mcp-ui-layout-id` when `content` is
81
+ a `UILayout` (composite), or `data-mcp-ui-component-id` when `content`
82
+ is a single `UIComponent`.
83
+ - Each per-component wrapper inside a layout carries
84
+ `data-mcp-ui-component-id`.
85
+
86
+ This enables CSS targeting, debug overlay tooling, and DOM-based
87
+ double-mount detection without a wrapper :
88
+
89
+ ```js
90
+ document.querySelectorAll('[data-mcp-ui-layout-id="dashboard-q3"]').length
91
+ // → 2 (whoops — somewhere in the parent framework, this is mounted twice)
92
+ ```
93
+
94
+ ### Internal — no `Date.now()` in identity-bearing code paths
95
+
96
+ Audit confirmed : every `Date.now()` call inside `mcp-ui-solid` is for
97
+ telemetry timestamps (`ts: Date.now()`), cache TTLs, or download-filename
98
+ fallbacks — none feed into a rendered DOM `id` or a key passed to
99
+ `<For>`. The new `getUiResourceStableKey` helper preserves this
100
+ invariant by hashing content rather than reading the clock.
101
+
102
+ ### Spec companion — `mcp-ui-spec@5.0.6`
103
+
104
+ Documentation-only patch bump : the spec README now formalizes the
105
+ runtime-payload identity contract (§Runtime Payload Identity) — `id`
106
+ obligation, fallback policy, and pointer to `getUiResourceStableKey`.
107
+
8
108
  ## [6.4.0] - 2026-05-03
9
109
 
10
110
  Closes axe 3 of `deposium_solid`'s
@@ -6,6 +6,9 @@ const solidJs = require("solid-js");
6
6
  const validation = require("../services/validation.cjs");
7
7
  const GenerativeUIErrorBoundary = require("./GenerativeUIErrorBoundary.cjs");
8
8
  const perf = require("../utils/perf.cjs");
9
+ const logger = require("../utils/logger.cjs");
10
+ const stableKey = require("../utils/stable-key.cjs");
11
+ const duplicateMountRegistry = require("../utils/duplicate-mount-registry.cjs");
9
12
  const MCPUITelemetryContext = require("../context/MCPUITelemetryContext.cjs");
10
13
  const GridRenderer = require("./GridRenderer.cjs");
11
14
  const FooterRenderer = require("./FooterRenderer.cjs");
@@ -1728,6 +1731,23 @@ const UIResourceRenderer = (props) => {
1728
1731
  };
1729
1732
  });
1730
1733
  const layoutData = layout();
1734
+ const isLayoutContent = !("type" in props.content) || props.content.type === "composite";
1735
+ const outerKey = solidJs.createMemo(() => stableKey.getUiResourceStableKey(props.content));
1736
+ solidJs.onMount(() => {
1737
+ var _a, _b;
1738
+ const key = outerKey();
1739
+ const info = duplicateMountRegistry._registerMount(key);
1740
+ if (info.count > 1) {
1741
+ (_a = props.onMountDuplicate) == null ? void 0 : _a.call(props, info);
1742
+ (_b = duplicateMountRegistry.getDuplicateMountReporter()) == null ? void 0 : _b(info);
1743
+ if (logger.isDebugEnabled() || props.debugDuplicateMounts) {
1744
+ console.warn("[mcp-ui] duplicate UIResourceRenderer mount", info);
1745
+ }
1746
+ }
1747
+ });
1748
+ solidJs.onCleanup(() => {
1749
+ duplicateMountRegistry._unregisterMount(outerKey());
1750
+ });
1731
1751
  const renderComponent = (component, onError) => web.createComponent(ComponentRenderer, {
1732
1752
  component,
1733
1753
  onError,
@@ -1742,6 +1762,15 @@ const UIResourceRenderer = (props) => {
1742
1762
  renderComponent,
1743
1763
  get children() {
1744
1764
  var _el$232 = web.getNextElement(_tmpl$53), _el$233 = _el$232.firstChild, _el$234 = _el$233.nextSibling, [_el$235, _co$50] = web.getNextMarker(_el$234.nextSibling);
1765
+ web.spread(_el$232, web.mergeProps({
1766
+ get ["class"]() {
1767
+ return `w-full ${props.class || ""}`;
1768
+ }
1769
+ }, () => isLayoutContent ? {
1770
+ "data-mcp-ui-layout-id": outerKey()
1771
+ } : {
1772
+ "data-mcp-ui-component-id": outerKey()
1773
+ }), false, true);
1745
1774
  web.insert(_el$233, web.createComponent(solidJs.For, {
1746
1775
  get each() {
1747
1776
  return layoutData.components;
@@ -1760,7 +1789,15 @@ const UIResourceRenderer = (props) => {
1760
1789
  return props.toolbarVariant;
1761
1790
  }
1762
1791
  }));
1763
- web.effect((_$p) => web.style(_el$236, getGridStyleString(component), _$p));
1792
+ web.effect((_p$) => {
1793
+ var _v$43 = getGridStyleString(component), _v$44 = stableKey.getUiResourceStableKey(component);
1794
+ _p$.e = web.style(_el$236, _v$43, _p$.e);
1795
+ _v$44 !== _p$.t && web.setAttribute(_el$236, "data-mcp-ui-component-id", _p$.t = _v$44);
1796
+ return _p$;
1797
+ }, {
1798
+ e: void 0,
1799
+ t: void 0
1800
+ });
1764
1801
  return _el$236;
1765
1802
  })()
1766
1803
  }));
@@ -1776,15 +1813,8 @@ const UIResourceRenderer = (props) => {
1776
1813
  });
1777
1814
  }
1778
1815
  }), _el$235, _co$50);
1779
- web.effect((_p$) => {
1780
- var _v$43 = `w-full ${props.class || ""}`, _v$44 = gridContainerStyle();
1781
- _v$43 !== _p$.e && web.className(_el$232, _p$.e = _v$43);
1782
- _p$.t = web.style(_el$233, _v$44, _p$.t);
1783
- return _p$;
1784
- }, {
1785
- e: void 0,
1786
- t: void 0
1787
- });
1816
+ web.effect((_$p) => web.style(_el$233, gridContainerStyle(), _$p));
1817
+ web.runHydrationEvents();
1788
1818
  return _el$232;
1789
1819
  }
1790
1820
  });