@seed-ship/mcp-ui-solid 6.20.0 → 6.22.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 (103) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/README.md +120 -3
  3. package/dist/components/ChartJSRenderer.cjs +244 -68
  4. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  5. package/dist/components/ChartJSRenderer.d.ts +9 -0
  6. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  7. package/dist/components/ChartJSRenderer.js +246 -70
  8. package/dist/components/ChartJSRenderer.js.map +1 -1
  9. package/dist/components/ExpandableWrapper.cjs +9 -1
  10. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  11. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  12. package/dist/components/ExpandableWrapper.js +9 -1
  13. package/dist/components/ExpandableWrapper.js.map +1 -1
  14. package/dist/components/IframeFallbackLink.cjs +37 -0
  15. package/dist/components/IframeFallbackLink.cjs.map +1 -0
  16. package/dist/components/IframeFallbackLink.d.ts +23 -0
  17. package/dist/components/IframeFallbackLink.d.ts.map +1 -0
  18. package/dist/components/IframeFallbackLink.js +37 -0
  19. package/dist/components/IframeFallbackLink.js.map +1 -0
  20. package/dist/components/LightboxOverlay.cjs +139 -16
  21. package/dist/components/LightboxOverlay.cjs.map +1 -1
  22. package/dist/components/LightboxOverlay.d.ts +6 -0
  23. package/dist/components/LightboxOverlay.d.ts.map +1 -1
  24. package/dist/components/LightboxOverlay.js +141 -18
  25. package/dist/components/LightboxOverlay.js.map +1 -1
  26. package/dist/components/UIResourceRenderer.cjs +150 -127
  27. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  28. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  29. package/dist/components/UIResourceRenderer.js +151 -128
  30. package/dist/components/UIResourceRenderer.js.map +1 -1
  31. package/dist/components/VideoRenderer.cjs +45 -19
  32. package/dist/components/VideoRenderer.cjs.map +1 -1
  33. package/dist/components/VideoRenderer.d.ts.map +1 -1
  34. package/dist/components/VideoRenderer.js +45 -19
  35. package/dist/components/VideoRenderer.js.map +1 -1
  36. package/dist/context/MCPUIConfigContext.cjs +30 -0
  37. package/dist/context/MCPUIConfigContext.cjs.map +1 -0
  38. package/dist/context/MCPUIConfigContext.d.ts +173 -0
  39. package/dist/context/MCPUIConfigContext.d.ts.map +1 -0
  40. package/dist/context/MCPUIConfigContext.js +30 -0
  41. package/dist/context/MCPUIConfigContext.js.map +1 -0
  42. package/dist/context/MCPUIStringsContext.cjs +8 -1
  43. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  44. package/dist/context/MCPUIStringsContext.d.ts +29 -0
  45. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  46. package/dist/context/MCPUIStringsContext.js +8 -1
  47. package/dist/context/MCPUIStringsContext.js.map +1 -1
  48. package/dist/hooks/index.d.ts +2 -0
  49. package/dist/hooks/index.d.ts.map +1 -1
  50. package/dist/hooks.cjs +2 -0
  51. package/dist/hooks.cjs.map +1 -1
  52. package/dist/hooks.js +2 -0
  53. package/dist/hooks.js.map +1 -1
  54. package/dist/index.cjs +12 -0
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.cts +5 -1
  57. package/dist/index.d.ts +5 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +14 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/services/index.d.ts +1 -1
  62. package/dist/services/index.d.ts.map +1 -1
  63. package/dist/services/validation.cjs +11 -10
  64. package/dist/services/validation.cjs.map +1 -1
  65. package/dist/services/validation.d.ts +20 -0
  66. package/dist/services/validation.d.ts.map +1 -1
  67. package/dist/services/validation.js +11 -10
  68. package/dist/services/validation.js.map +1 -1
  69. package/dist/utils/iframe-coep.cjs +30 -0
  70. package/dist/utils/iframe-coep.cjs.map +1 -0
  71. package/dist/utils/iframe-coep.d.ts +53 -0
  72. package/dist/utils/iframe-coep.d.ts.map +1 -0
  73. package/dist/utils/iframe-coep.js +30 -0
  74. package/dist/utils/iframe-coep.js.map +1 -0
  75. package/dist/utils/pan-zoom.cjs +312 -0
  76. package/dist/utils/pan-zoom.cjs.map +1 -0
  77. package/dist/utils/pan-zoom.d.ts +235 -0
  78. package/dist/utils/pan-zoom.d.ts.map +1 -0
  79. package/dist/utils/pan-zoom.js +312 -0
  80. package/dist/utils/pan-zoom.js.map +1 -0
  81. package/dist/validation.cjs +2 -0
  82. package/dist/validation.cjs.map +1 -1
  83. package/dist/validation.d.cts +1 -1
  84. package/dist/validation.d.ts +1 -1
  85. package/dist/validation.d.ts.map +1 -1
  86. package/dist/validation.js +3 -1
  87. package/package.json +5 -1
  88. package/src/components/ChartJSRenderer.tsx +396 -1
  89. package/src/components/ExpandableWrapper.tsx +26 -0
  90. package/src/components/IframeFallbackLink.tsx +57 -0
  91. package/src/components/LightboxOverlay.tsx +280 -4
  92. package/src/components/UIResourceRenderer.tsx +31 -4
  93. package/src/components/VideoRenderer.tsx +20 -0
  94. package/src/context/MCPUIConfigContext.tsx +202 -0
  95. package/src/context/MCPUIStringsContext.tsx +40 -0
  96. package/src/hooks/index.ts +13 -0
  97. package/src/index.ts +31 -0
  98. package/src/services/index.ts +1 -0
  99. package/src/services/validation.ts +38 -16
  100. package/src/solid-jsx-attrs.d.ts +24 -0
  101. package/src/utils/iframe-coep.ts +81 -0
  102. package/src/utils/pan-zoom.ts +779 -0
  103. package/src/validation.ts +4 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,229 @@ 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.22.0] - 2026-09-18
9
+
10
+ Wheel zoom and drag pan on the surfaces that could not be read when the content
11
+ was dense: the image lightbox, and Chart.js charts in their expanded view.
12
+
13
+ ### The rule this follows
14
+
15
+ **Wheel zoom is only ever enabled on a surface that owns its viewport** — a
16
+ fullscreen overlay or the expanded modal. A component sitting inline in a
17
+ scrolling page must not swallow the wheel, or a user scrolling the page finds
18
+ themselves zooming a chart instead. That is why chart zoom defaults to the
19
+ expanded view only, and why inline zoom is opt-in.
20
+
21
+ ### Added
22
+
23
+ - **`createPanZoom(options)`** (`src/utils/pan-zoom.ts`, exported from the root
24
+ and from the `./hooks` subpath) — a dependency-free cursor-anchored zoom and
25
+ drag-pan primitive: the point under the pointer stays under the pointer.
26
+ Handles two-finger pinch, clamps to `min`/`max`, and exposes
27
+ `cancelGesture()` for the case where the surface carrying the handlers is
28
+ torn down while a pointer is still down. The pure helpers behind it
29
+ (`clampScale`, `contentPointAt`, `viewportPointOf`, `zoomAtAnchor`) are
30
+ exported too.
31
+ - **`LightboxOverlay` zooms.** Wheel, drag to pan, double-click to toggle,
32
+ pinch on touch, and the keyboard shortcuts `+` / `-` / `0` alongside the
33
+ existing Escape and arrows. Zoom controls carry a level readout, and the zoom
34
+ resets when the displayed image changes.
35
+ - **Chart.js zoom and pan**, through `chartjs-plugin-zoom` as a new **optional**
36
+ peer dependency. Absent, charts render exactly as before — no warning, no
37
+ broken UI. A reset button appears in the toolbar only while the chart is
38
+ actually zoomed. A chart's own `options.plugins.zoom` is preserved: the
39
+ renderer's block is deep-merged into it, the caller's explicit values win
40
+ (`limits`, wheel `speed`, a pan `modifierKey`, `mode`), and a caller's
41
+ `onZoom` / `onPan` is *composed* with the renderer's rather than replacing
42
+ it, so the reset control stays in sync with the viewport either way. A key
43
+ the caller left `undefined` is read as an omission, not as an erasure. And
44
+ `options.plugins.zoom: false` — Chart.js's documented per-chart plugin
45
+ opt-out — beats `chartZoom`: the plugin is never downloaded and the toolbar
46
+ offers no controls.
47
+ - **`MCPUIConfig.chartZoom`**: `'expanded'` (default), `'always'` or `'never'`.
48
+ - Four string keys: `zoomIn`, `zoomOut`, `zoomReset`, `zoomLevel`
49
+ (`'Zoom {percent}%'`). Chrome key count 271 → 275.
50
+
51
+ ### Changed
52
+
53
+ - **Every `MCPUIConfig` field is now optional**, as `MCPUIStrings` has always
54
+ been. `MCPUIConfig` grows a key in a minor release each time the library
55
+ gains a policy switch — `iframeFallbackLink` in 6.21.0, `chartZoom` here —
56
+ and while the fields were required, each addition broke the build of anyone
57
+ holding a *complete* config: a typed constant, a helper returning one, or a
58
+ direct `<MCPUIConfigContext.Provider value={…}>`. Nothing about their
59
+ behaviour had changed; only `tsc` failed. Adding a key is additive now.
60
+
61
+ The completeness guarantee moved onto the values: `DEFAULT_MCPUI_CONFIG` is
62
+ typed `Required<MCPUIConfig>` (a new key without a default is a compile error
63
+ in the library), and `useMCPUIConfig()` returns `Required<MCPUIConfig>`, so
64
+ every renderer still reads a fully resolved policy and never null-checks one.
65
+ `MCPUIConfigContext` itself stays typed `MCPUIConfig`, which makes providing
66
+ it directly *more* permissive than before. Backward compatible in both
67
+ directions: a consumer already passing a field still type-checks.
68
+
69
+ ### Fixed
70
+
71
+ - **`ExpandableWrapper` no longer closes on a drag released over its backdrop.**
72
+ When a press and its release have different targets the browser dispatches
73
+ `click` on their nearest common ancestor — the backdrop — so panning a zoomed
74
+ chart, or selecting text in an expanded table, and releasing over the 16px
75
+ margin around the panel tore the modal down mid-gesture. The close now
76
+ requires the gesture to have *begun* on the backdrop. This was reachable
77
+ before this release for any drag inside an expanded view; chart panning just
78
+ made it easy to hit.
79
+
80
+ ### Not affected
81
+
82
+ Maps and graphs already zoomed: `MapRenderer` enables Leaflet's
83
+ `scrollWheelZoom` and `GraphRenderer` registers G6's `zoom-canvas` /
84
+ `drag-canvas`. Iframes (including a diagram served from `mermaid.ink`) are
85
+ cross-origin documents and cannot be transformed by the host at all.
86
+
87
+ ## [6.21.0] - 2026-09-17
88
+
89
+ The library's iframes now work on a host that serves
90
+ `Cross-Origin-Embedder-Policy: credentialless`.
91
+
92
+ ### The problem
93
+
94
+ Under that header a browser refuses **any** cross-origin iframe whose own
95
+ document does not itself send a COEP header. That is YouTube, Vimeo, Google
96
+ Docs, Notion — nearly every entry of `DEFAULT_IFRAME_DOMAINS`. The frame stays
97
+ blank and nothing in the page can detect it: a refused cross-origin document
98
+ fires no `error` event the parent can observe. The `credentialless` *header*
99
+ only relaxes the rule for sub-resources (images, fonts, map tiles — which is
100
+ why the quickchart `<img>` fallback and the Leaflet tiles were never
101
+ affected); nested documents need the boolean HTML attribute
102
+ `<iframe credentialless>` on the tag itself.
103
+
104
+ ### What the library now does
105
+
106
+ Both iframes it renders — `IframeRenderer` (the `iframe` component) and
107
+ `VideoRenderer`'s YouTube / Vimeo embed — can carry `credentialless` when their
108
+ host is **not** in `TRUSTED_IFRAME_DOMAINS`.
109
+
110
+ - **The `iframe` component on an untrusted host gets it by default.** Those
111
+ frames already ran without cookies, since their sandbox has no
112
+ `allow-same-origin`. The attribute costs them nothing and is exactly what
113
+ unblocks them under COEP.
114
+ - **The video embed does NOT get it by default.** It carries no sandbox, so a
115
+ YouTube or Vimeo frame really does hold provider cookies (consent, playback
116
+ state, sign-in). Taking them away on every host — including the majority
117
+ that send no COEP header — would be a silent regression, so `'auto'` leaves
118
+ it alone and a COEP host opts in with `iframeCredentialless: 'always'`.
119
+ - **Trusted hosts deliberately do NOT get it.** `TRUSTED_IFRAME_DOMAINS`
120
+ (Google Docs/Drive, Notion, Airtable, Figma, Linear, Stripe, Polar, HubSpot,
121
+ Calendly, …) exists precisely because those embeds need their own cookies to
122
+ authenticate. Loaded `credentialless` they would render a login screen — a
123
+ visibly broken embed. Under COEP they stay blocked, which is the honest
124
+ outcome; `iframeCredentialless: 'always'` overrides it if you disagree.
125
+
126
+ The attribute is a **boolean** attribute: it is absent from the DOM when it
127
+ does not apply, never `credentialless="false"`.
128
+
129
+ ### Added
130
+
131
+ - **`<MCPUIConfigProvider>` / `useMCPUIConfig()` / `DEFAULT_MCPUI_CONFIG` /
132
+ `MCPUIConfig`** (new `src/context/MCPUIConfigContext.tsx`, exported from the
133
+ root barrel) — host-level rendering policy, the behavioural counterpart of
134
+ `MCPUIStrings`. Three options:
135
+
136
+ | option | default | meaning |
137
+ | --- | --- | --- |
138
+ | `iframeCredentialless` | `'auto'` | `'auto'`: attribute on the `iframe` component when its host is not trusted (those frames already run without cookies); the video embed, which has no sandbox, is left alone. `'always'`: every iframe, video and trusted hosts included. `'never'`: none. |
139
+ | `customTrustedIframeDomains` | `[]` | extra hosts treated as trusted — no `credentialless`, and `allow-same-origin` in the sandbox. Subdomains match. Reclassification only: the allow-list must already accept the host. |
140
+ | `iframePolicy` | `'strict'` | how an `iframe` component's host is validated; `'extend'` also accepts `customIframeDomains`. |
141
+ | `customIframeDomains` | `[]` | extra hosts the allow-list accepts under `iframePolicy: 'extend'`. Without this, an unlisted host never reaches a renderer. |
142
+ | `iframeFallbackLink` | `'auto'` | when the "open in a new tab" link shows under an embed. `'auto'`: only on a cross-origin-isolated page. |
143
+
144
+ ```tsx
145
+ import { MCPUIConfigProvider } from '@seed-ship/mcp-ui-solid'
146
+
147
+ // A host that serves COEP: credentialless and embeds its own tool.
148
+ <MCPUIConfigProvider
149
+ config={{
150
+ iframeCredentialless: 'always',
151
+ iframePolicy: 'extend',
152
+ customIframeDomains: ['embed.acme.com'],
153
+ customTrustedIframeDomains: ['embed.acme.com'],
154
+ iframeFallbackLink: 'auto',
155
+ }}
156
+ >
157
+ <App />
158
+ </MCPUIConfigProvider>
159
+
160
+ // A host with no COEP header that does not want its embeds to lose cookies.
161
+ <MCPUIConfigProvider config={{ iframeCredentialless: 'never' }}>
162
+ <App />
163
+ </MCPUIConfigProvider>
164
+ ```
165
+
166
+ With no provider mounted, `DEFAULT_MCPUI_CONFIG` applies — every renderer
167
+ still works standalone.
168
+
169
+ - **`isTrustedIframeDomain(url, { customTrustedDomains? })`** — the trusted-host
170
+ test `getIframeSandbox` already performed, extracted and exported from the
171
+ root barrel and from the `@seed-ship/mcp-ui-solid/validation` subpath
172
+ (`getIframeSandbox` is now exported from that subpath too). Invalid URL →
173
+ `false`, so an unparsable URL is never trusted and does receive the
174
+ attribute. Matching is unchanged: exact host, or a subdomain of a listed
175
+ host.
176
+
177
+ - **`MCPUIStrings.iframeOpenInNewTab`** — optional, default
178
+ `'Open in a new tab'`. Label of the link rendered under an embed, which is
179
+ the only way out of a silently blocked frame. Visibility follows
180
+ `iframeFallbackLink`; `window.crossOriginIsolated` is read **after mount**,
181
+ never during render, so SSR markup and hydration agree. That flag requires
182
+ COOP `same-origin` on top of COEP — a host that sends COEP alone should set
183
+ `iframeFallbackLink: 'always'`, since its embeds are blocked while the flag
184
+ reads false. The link is skipped
185
+ when `safeUrl()` rejects the URL. `MCPUIStrings` now covers **271 keys**.
186
+
187
+ ### Fixed
188
+
189
+ - `IframeRenderer` called `getIframeSandbox(params.url)` with no options, so a
190
+ host's custom trusted domains never reached the sandbox. It now passes
191
+ `{ customTrustedDomains: config.customTrustedIframeDomains }`, which is what
192
+ makes `customTrustedIframeDomains` grant `allow-same-origin` as documented.
193
+
194
+ ### Browser support
195
+
196
+ | | header `COEP: credentialless` | attribute `<iframe credentialless>` |
197
+ | --- | --- | --- |
198
+ | Chrome / Edge | 96+ | **110+** |
199
+ | Firefox | 119+ | **not supported** |
200
+ | Safari | not supported (value ignored) | not supported |
201
+
202
+ - **Chrome / Edge** — the attribute fixes the problem. This is the majority
203
+ case.
204
+ - **Firefox** — honours the header but ignores the attribute, so third-party
205
+ iframes stay blocked for as long as the host page sends COEP. Nothing in
206
+ this library can change that; it is a host-side trade-off (drop COEP, or
207
+ accept the degradation on Firefox). The fallback link is what the user gets
208
+ there.
209
+ - **Safari** — does not know the `credentialless` value and treats the header
210
+ as absent: no cross-origin isolation, but embeds work.
211
+
212
+ ### Note for hosts
213
+
214
+ The library's allow-list is not a substitute for your CSP. The browser also
215
+ enforces `frame-src`, which the host sets — a domain missing from it stays
216
+ blocked whatever this library emits (see the comment at
217
+ `services/validation.ts` on `DEFAULT_IFRAME_DOMAINS`: "Must match CSP
218
+ frame-src directive").
219
+
220
+ ### Tests
221
+
222
+ `src/components/IframeCredentialless.test.tsx` (25 tests) — the
223
+ `isTrustedIframeDomain` matching table, attribute presence/absence through
224
+ `<UIResourceRenderer>` and `<VideoRenderer>` for each `iframeCredentialless`
225
+ mode, the `customTrustedIframeDomains` → sandbox plumbing, the fallback link
226
+ under each `iframeFallbackLink` mode with `window.crossOriginIsolated`
227
+ stubbed, and the SSR-shape assertions on the pure decision helpers. Plus a new
228
+ `iframeOpenInNewTab` case in `MCPUIStringsContext.sweep.test.tsx` and the
229
+ default pinned in `mcpui-strings-defaults.test.ts`.
230
+
8
231
  ## [6.20.0] - 2026-09-17
9
232
 
10
233
  Every user-visible chrome string the library itself renders — button titles,
package/README.md CHANGED
@@ -5,6 +5,25 @@ SolidJS components + chat toolkit for MCP-generated UI. Part of the [MCP UI ecos
5
5
  [![npm version](https://img.shields.io/npm/v/@seed-ship/mcp-ui-solid.svg)](https://www.npmjs.com/package/@seed-ship/mcp-ui-solid)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
+ ## What's New in v6.21.0
9
+
10
+ - **Iframes work under `Cross-Origin-Embedder-Policy: credentialless`.** Both
11
+ iframes the library renders — the `iframe` component and `VideoRenderer`'s
12
+ YouTube / Vimeo embed — now carry the boolean `credentialless` attribute
13
+ when their host is **not** in `TRUSTED_IFRAME_DOMAINS`. Trusted hosts
14
+ deliberately do not get it (a cookie-less authenticated embed shows a login
15
+ screen). Chrome / Edge 110+ honour the attribute; Firefox ignores it; Safari
16
+ ignores the COEP value entirely — see
17
+ [*Iframes under COEP*](#iframes-under-coep--mcpuiconfigprovider-v6210).
18
+ - **New `<MCPUIConfigProvider>`** (`MCPUIConfig`, `useMCPUIConfig`,
19
+ `DEFAULT_MCPUI_CONFIG`) — host-level rendering policy:
20
+ `iframeCredentialless`, `customTrustedIframeDomains`, `iframeFallbackLink`.
21
+ - **New `isTrustedIframeDomain()`** export (root barrel and `/validation`),
22
+ and a new `iframeOpenInNewTab` chrome string for the "open in a new tab"
23
+ link rendered under an embed that COEP may have blocked silently.
24
+ - **Fixed**: `IframeRenderer` called `getIframeSandbox()` without options, so
25
+ a host's custom trusted domains never reached the sandbox.
26
+
8
27
  ## What's New in v6.20.0
9
28
 
10
29
  - **Complete i18n of the library chrome.** Every user-visible chrome string —
@@ -951,7 +970,7 @@ const { state, pinned, close } = useScratchpadState()
951
970
  | `modal` | Portal overlay, sizes sm-full, Escape/backdrop close |
952
971
  | `image-gallery` | Grid layout, lightbox, keyboard nav |
953
972
  | `video` | YouTube/Vimeo/direct URL |
954
- | `iframe` | Tiered sandbox, 80+ whitelisted domains |
973
+ | `iframe` | Tiered sandbox, 80+ whitelisted domains, COEP `credentialless` (v6.21.0) |
955
974
  | `image` | Responsive with lazy loading |
956
975
  | `link` | Styled link cards |
957
976
  | `action` | Tool call buttons |
@@ -961,6 +980,99 @@ const { state, pinned, close } = useScratchpadState()
961
980
  | `artifact` | File download/preview |
962
981
  | `footer` | Metadata display |
963
982
 
983
+ ## Iframes under COEP — `MCPUIConfigProvider` (v6.21.0)
984
+
985
+ The library renders exactly two iframes: the `iframe` component
986
+ (`IframeRenderer`) and `VideoRenderer`'s YouTube / Vimeo embed. Both are
987
+ constrained by `DEFAULT_IFRAME_DOMAINS` (the allow-list) and
988
+ `getIframeSandbox()` (the tiered sandbox — `allow-same-origin allow-forms`
989
+ for `TRUSTED_IFRAME_DOMAINS`, `allow-scripts allow-popups` for everything
990
+ else).
991
+
992
+ A host that serves `Cross-Origin-Embedder-Policy: credentialless` adds a
993
+ third constraint: the browser refuses any cross-origin iframe whose own
994
+ document sends no COEP header — which is YouTube, Vimeo, Google Docs, Notion
995
+ and most of the allow-list. The frame stays blank, and the page cannot even
996
+ detect it (a refused cross-origin document fires no `error` event). The fix
997
+ is the boolean HTML attribute `<iframe credentialless>`.
998
+
999
+ By default (`'auto'`) the library sets it only where nothing is lost: the
1000
+ `iframe` component pointing at a **non-trusted** host, whose sandbox already
1001
+ has no `allow-same-origin` and therefore no cookies or storage. Two cases are
1002
+ left alone on purpose:
1003
+
1004
+ - **Trusted hosts** are in that list because they need their own cookies to
1005
+ authenticate, and a cookie-less authenticated embed only renders a login
1006
+ screen — a clear block beats a broken frame.
1007
+ - **The video embed** carries no sandbox, so a YouTube or Vimeo frame really
1008
+ does hold provider cookies (consent, playback state). A host under COEP
1009
+ unblocks it with `iframeCredentialless: 'always'`, accepting that trade;
1010
+ a host without COEP keeps 6.20.0 behaviour and changes nothing.
1011
+
1012
+ `<MCPUIConfigProvider>` is where a host overrides all of this. It is the
1013
+ behavioural counterpart of `<MCPUIStringsProvider>`: context-based, entirely
1014
+ optional, and `DEFAULT_MCPUI_CONFIG` applies when it is absent.
1015
+
1016
+ ```tsx
1017
+ import { MCPUIConfigProvider } from '@seed-ship/mcp-ui-solid'
1018
+
1019
+ <MCPUIConfigProvider
1020
+ config={{
1021
+ iframeCredentialless: 'always', // also unblocks the video embed
1022
+ iframePolicy: 'extend', // accept hosts outside the allow-list
1023
+ customIframeDomains: ['embed.acme.com'], // …these ones
1024
+ customTrustedIframeDomains: ['embed.acme.com'], // …and treat them as trusted
1025
+ iframeFallbackLink: 'auto', // default
1026
+ }}
1027
+ >
1028
+ <App />
1029
+ </MCPUIConfigProvider>
1030
+ ```
1031
+
1032
+ | Option | Values | Default | What it does |
1033
+ |--------|--------|---------|--------------|
1034
+ | `iframeCredentialless` | `'auto'` \| `'always'` \| `'never'` | `'auto'` | `'auto'`: the attribute on the `iframe` component when its host is **not** in `TRUSTED_IFRAME_DOMAINS` (+ `customTrustedIframeDomains`) — those frames already run without cookies. The video embed is excluded, since it has none of that sandboxing. `'always'`: every iframe, video and trusted hosts included — what a COEP host wants. `'never'`: none. |
1035
+ | `customTrustedIframeDomains` | `string[]` | `[]` | Hosts treated as trusted on top of `TRUSTED_IFRAME_DOMAINS`: no `credentialless`, and `allow-same-origin` in the sandbox. Subdomains of a listed host match. This only reclassifies a host the allow-list already accepts — pair it with `customIframeDomains` for anything else. |
1036
+ | `iframePolicy` | `'strict'` \| `'extend'` \| `'allow-all'` | `'strict'` | How `UIResourceRenderer` validates an `iframe` component's host. `'strict'` accepts `DEFAULT_IFRAME_DOMAINS` only; a host outside it is replaced by the validation card before any renderer runs. |
1037
+ | `customIframeDomains` | `string[]` | `[]` | Extra hosts the allow-list accepts when `iframePolicy` is `'extend'`. |
1038
+ | `iframeFallbackLink` | `'auto'` \| `'always'` \| `'never'` | `'auto'` | Whether an "open in a new tab" link (`strings.iframeOpenInNewTab`) shows under the embed. `'auto'` shows it only when the page is cross-origin isolated. `window.crossOriginIsolated` is read **after mount**, so SSR markup and hydration match. Note that flag needs COOP `same-origin` **and** COEP: a host that sends COEP alone blocks embeds while the flag is false, so it should set `'always'`. |
1039
+
1040
+ The attribute is a boolean attribute: absent from the DOM when it does not
1041
+ apply, never `credentialless="false"`.
1042
+
1043
+ Like `<MCPUIStringsProvider>`, a nested `<MCPUIConfigProvider>` merges over the
1044
+ defaults, not over an outer provider: repeat the options you want to keep.
1045
+
1046
+ `isTrustedIframeDomain(url, { customTrustedDomains? })` is exported (root
1047
+ barrel and the `/validation` subpath) if you need the same decision outside
1048
+ the renderers.
1049
+
1050
+ ### Browser support
1051
+
1052
+ | | header `COEP: credentialless` | attribute `<iframe credentialless>` |
1053
+ | --- | --- | --- |
1054
+ | Chrome / Edge | 96+ | **110+** |
1055
+ | Firefox | 119+ | **not supported** |
1056
+ | Safari | not supported (value ignored) | not supported |
1057
+
1058
+ Firefox applies the header but ignores the attribute, so third-party iframes
1059
+ stay blocked there while the host sends COEP — nothing in this library can
1060
+ change that, and the fallback link is what the user gets. Safari does not
1061
+ know the value, treats the header as absent, and embeds simply work.
1062
+
1063
+ ### The CSP is still yours
1064
+
1065
+ The allow-list is not a substitute for your Content-Security-Policy. The
1066
+ browser enforces `frame-src`, which the **host** sets, and a domain missing
1067
+ from it stays blocked whatever this library emits:
1068
+
1069
+ ```
1070
+ Content-Security-Policy: frame-src https://www.youtube-nocookie.com https://player.vimeo.com …
1071
+ ```
1072
+
1073
+ The comment on `DEFAULT_IFRAME_DOMAINS` in `services/validation.ts` says the
1074
+ same thing: *"Must match CSP frame-src directive."*
1075
+
964
1076
  ## SSR Compatibility
965
1077
 
966
1078
  The published `dist/` is a DOM (client) Solid build — importing it directly
@@ -1153,6 +1265,7 @@ formatMCPUIString('Export CSV ({count} rows)', { count: 42 }) // 'Export CSV (42
1153
1265
  | `imageAlt` | `Image` | `UIResourceRenderer` (image) — fallback `alt` text |
1154
1266
  | `imageViewFullSize` | `View full size: {alt}` | `UIResourceRenderer` (image) — zoom-link `aria-label`. Template: `{alt}` |
1155
1267
  | `iframeTitle` | `Embedded content` | `UIResourceRenderer` (iframe) — fallback `title` |
1268
+ | `iframeOpenInNewTab` | `Open in a new tab` | `IframeRenderer` / `VideoRenderer` — label of the COEP fallback link under an embed (v6.21.0) |
1156
1269
  | `linkLabel` | `Link` | `UIResourceRenderer` (link) — fallback visible label |
1157
1270
  | `linkOpensInNewTab` | `{label}: {description} (opens in new tab)` | `UIResourceRenderer` (link) — `aria-label`. Template: `{label}`, `{description}` |
1158
1271
  | `validationWarning` | `Component validation warning` | `UIResourceRenderer` — inline component-validation warning chip `aria-label` |
@@ -1484,6 +1597,7 @@ const fr: MCPUIStrings = {
1484
1597
  imageAlt: 'Image',
1485
1598
  imageViewFullSize: 'Voir en taille réelle : {alt}',
1486
1599
  iframeTitle: 'Contenu intégré',
1600
+ iframeOpenInNewTab: 'Ouvrir dans un nouvel onglet',
1487
1601
  linkLabel: 'Lien',
1488
1602
  linkOpensInNewTab: '{label} : {description} (ouvre un nouvel onglet)',
1489
1603
  validationWarning: 'Avertissement de validation du composant',
@@ -1781,6 +1895,7 @@ import {
1781
1895
  GraphRenderer, isG6Available, graphToMermaid, graphToJSON,
1782
1896
  renderCellValue, // v5.7.0 — citation-chip-aware table cell renderer
1783
1897
  MCPUIStringsProvider, useMCPUIStrings, DEFAULT_MCPUI_STRINGS, // v6.6.0 i18n chrome
1898
+ MCPUIConfigProvider, useMCPUIConfig, DEFAULT_MCPUI_CONFIG, // v6.21.0 host policy
1784
1899
  } from '@seed-ship/mcp-ui-solid'
1785
1900
 
1786
1901
  // Hooks
@@ -1808,7 +1923,8 @@ import {
1808
1923
  // Validation + Security
1809
1924
  import {
1810
1925
  validateComponent, validateLayout, validateIframeDomain,
1811
- getIframeSandbox, DEFAULT_RESOURCE_LIMITS,
1926
+ getIframeSandbox, isTrustedIframeDomain, // v6.21.0 — trusted-host test
1927
+ DEFAULT_RESOURCE_LIMITS,
1812
1928
  DEFAULT_IFRAME_DOMAINS, TRUSTED_IFRAME_DOMAINS,
1813
1929
  ComponentRegistry, mergeScratchpadSections,
1814
1930
  validateAgainstSource, // v4.0.0 — anti-hallucination
@@ -1849,7 +1965,8 @@ import type {
1849
1965
  MapPopupConfig, MapLayer, MapPMTilesConfig,
1850
1966
  IframePolicy, ValidationOptions,
1851
1967
  CitationCtx, CitationEntry, DuplicateMountInfo, DuplicateMountReporter,
1852
- MCPUIStrings, TelemetryEvent, TelemetrySink, TelemetryOptions, TelemetryDispatcher,
1968
+ MCPUIStrings, MCPUIConfig, // v6.21.0
1969
+ TelemetryEvent, TelemetrySink, TelemetryOptions, TelemetryDispatcher,
1853
1970
  DataValidation, HallucinatedNumber, DataValidationOptions,
1854
1971
  VerifiedTextContent, DataPreviewContent, MapSectionContent,
1855
1972
  ChatBus, ChatEvents, ChatCommands, ScratchpadState, ScratchpadSection,