@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.
- package/CHANGELOG.md +223 -0
- package/README.md +120 -3
- package/dist/components/ChartJSRenderer.cjs +244 -68
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts +9 -0
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +246 -70
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ExpandableWrapper.cjs +9 -1
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +9 -1
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/IframeFallbackLink.cjs +37 -0
- package/dist/components/IframeFallbackLink.cjs.map +1 -0
- package/dist/components/IframeFallbackLink.d.ts +23 -0
- package/dist/components/IframeFallbackLink.d.ts.map +1 -0
- package/dist/components/IframeFallbackLink.js +37 -0
- package/dist/components/IframeFallbackLink.js.map +1 -0
- package/dist/components/LightboxOverlay.cjs +139 -16
- package/dist/components/LightboxOverlay.cjs.map +1 -1
- package/dist/components/LightboxOverlay.d.ts +6 -0
- package/dist/components/LightboxOverlay.d.ts.map +1 -1
- package/dist/components/LightboxOverlay.js +141 -18
- package/dist/components/LightboxOverlay.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +150 -127
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +151 -128
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/VideoRenderer.cjs +45 -19
- package/dist/components/VideoRenderer.cjs.map +1 -1
- package/dist/components/VideoRenderer.d.ts.map +1 -1
- package/dist/components/VideoRenderer.js +45 -19
- package/dist/components/VideoRenderer.js.map +1 -1
- package/dist/context/MCPUIConfigContext.cjs +30 -0
- package/dist/context/MCPUIConfigContext.cjs.map +1 -0
- package/dist/context/MCPUIConfigContext.d.ts +173 -0
- package/dist/context/MCPUIConfigContext.d.ts.map +1 -0
- package/dist/context/MCPUIConfigContext.js +30 -0
- package/dist/context/MCPUIConfigContext.js.map +1 -0
- package/dist/context/MCPUIStringsContext.cjs +8 -1
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +29 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +8 -1
- package/dist/context/MCPUIStringsContext.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/validation.cjs +11 -10
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts +20 -0
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +11 -10
- package/dist/services/validation.js.map +1 -1
- package/dist/utils/iframe-coep.cjs +30 -0
- package/dist/utils/iframe-coep.cjs.map +1 -0
- package/dist/utils/iframe-coep.d.ts +53 -0
- package/dist/utils/iframe-coep.d.ts.map +1 -0
- package/dist/utils/iframe-coep.js +30 -0
- package/dist/utils/iframe-coep.js.map +1 -0
- package/dist/utils/pan-zoom.cjs +312 -0
- package/dist/utils/pan-zoom.cjs.map +1 -0
- package/dist/utils/pan-zoom.d.ts +235 -0
- package/dist/utils/pan-zoom.d.ts.map +1 -0
- package/dist/utils/pan-zoom.js +312 -0
- package/dist/utils/pan-zoom.js.map +1 -0
- package/dist/validation.cjs +2 -0
- package/dist/validation.cjs.map +1 -1
- package/dist/validation.d.cts +1 -1
- package/dist/validation.d.ts +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +3 -1
- package/package.json +5 -1
- package/src/components/ChartJSRenderer.tsx +396 -1
- package/src/components/ExpandableWrapper.tsx +26 -0
- package/src/components/IframeFallbackLink.tsx +57 -0
- package/src/components/LightboxOverlay.tsx +280 -4
- package/src/components/UIResourceRenderer.tsx +31 -4
- package/src/components/VideoRenderer.tsx +20 -0
- package/src/context/MCPUIConfigContext.tsx +202 -0
- package/src/context/MCPUIStringsContext.tsx +40 -0
- package/src/hooks/index.ts +13 -0
- package/src/index.ts +31 -0
- package/src/services/index.ts +1 -0
- package/src/services/validation.ts +38 -16
- package/src/solid-jsx-attrs.d.ts +24 -0
- package/src/utils/iframe-coep.ts +81 -0
- package/src/utils/pan-zoom.ts +779 -0
- 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
|
[](https://www.npmjs.com/package/@seed-ship/mcp-ui-solid)
|
|
6
6
|
[](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,
|
|
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,
|
|
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,
|