@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.
- package/CHANGELOG.md +100 -0
- package/dist/components/UIResourceRenderer.cjs +40 -10
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts +20 -0
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +42 -12
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/duplicate-mount-registry.cjs +27 -0
- package/dist/utils/duplicate-mount-registry.cjs.map +1 -0
- package/dist/utils/duplicate-mount-registry.d.ts +84 -0
- package/dist/utils/duplicate-mount-registry.d.ts.map +1 -0
- package/dist/utils/duplicate-mount-registry.js +27 -0
- package/dist/utils/duplicate-mount-registry.js.map +1 -0
- package/dist/utils/stable-key.cjs +41 -0
- package/dist/utils/stable-key.cjs.map +1 -0
- package/dist/utils/stable-key.d.ts +33 -0
- package/dist/utils/stable-key.d.ts.map +1 -0
- package/dist/utils/stable-key.js +41 -0
- package/dist/utils/stable-key.js.map +1 -0
- package/package.json +1 -1
- package/src/components/UIResourceRenderer.identity.test.tsx +161 -0
- package/src/components/UIResourceRenderer.tsx +63 -2
- package/src/index.ts +8 -0
- package/src/utils/duplicate-mount-registry.test.ts +82 -0
- package/src/utils/duplicate-mount-registry.ts +113 -0
- package/src/utils/stable-key.test.ts +96 -0
- package/src/utils/stable-key.ts +91 -0
- 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((
|
|
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((
|
|
1780
|
-
|
|
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
|
});
|