@seed-ship/mcp-ui-solid 6.20.0 → 6.21.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 (69) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +120 -3
  3. package/dist/components/IframeFallbackLink.cjs +37 -0
  4. package/dist/components/IframeFallbackLink.cjs.map +1 -0
  5. package/dist/components/IframeFallbackLink.d.ts +23 -0
  6. package/dist/components/IframeFallbackLink.d.ts.map +1 -0
  7. package/dist/components/IframeFallbackLink.js +37 -0
  8. package/dist/components/IframeFallbackLink.js.map +1 -0
  9. package/dist/components/UIResourceRenderer.cjs +150 -127
  10. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  11. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  12. package/dist/components/UIResourceRenderer.js +151 -128
  13. package/dist/components/UIResourceRenderer.js.map +1 -1
  14. package/dist/components/VideoRenderer.cjs +45 -19
  15. package/dist/components/VideoRenderer.cjs.map +1 -1
  16. package/dist/components/VideoRenderer.d.ts.map +1 -1
  17. package/dist/components/VideoRenderer.js +45 -19
  18. package/dist/components/VideoRenderer.js.map +1 -1
  19. package/dist/context/MCPUIConfigContext.cjs +29 -0
  20. package/dist/context/MCPUIConfigContext.cjs.map +1 -0
  21. package/dist/context/MCPUIConfigContext.d.ts +124 -0
  22. package/dist/context/MCPUIConfigContext.d.ts.map +1 -0
  23. package/dist/context/MCPUIConfigContext.js +29 -0
  24. package/dist/context/MCPUIConfigContext.js.map +1 -0
  25. package/dist/context/MCPUIStringsContext.cjs +3 -1
  26. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  27. package/dist/context/MCPUIStringsContext.d.ts +7 -0
  28. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  29. package/dist/context/MCPUIStringsContext.js +3 -1
  30. package/dist/context/MCPUIStringsContext.js.map +1 -1
  31. package/dist/index.cjs +6 -0
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +3 -1
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +7 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/services/index.d.ts +1 -1
  39. package/dist/services/index.d.ts.map +1 -1
  40. package/dist/services/validation.cjs +11 -10
  41. package/dist/services/validation.cjs.map +1 -1
  42. package/dist/services/validation.d.ts +20 -0
  43. package/dist/services/validation.d.ts.map +1 -1
  44. package/dist/services/validation.js +11 -10
  45. package/dist/services/validation.js.map +1 -1
  46. package/dist/utils/iframe-coep.cjs +30 -0
  47. package/dist/utils/iframe-coep.cjs.map +1 -0
  48. package/dist/utils/iframe-coep.d.ts +46 -0
  49. package/dist/utils/iframe-coep.d.ts.map +1 -0
  50. package/dist/utils/iframe-coep.js +30 -0
  51. package/dist/utils/iframe-coep.js.map +1 -0
  52. package/dist/validation.cjs +2 -0
  53. package/dist/validation.cjs.map +1 -1
  54. package/dist/validation.d.cts +1 -1
  55. package/dist/validation.d.ts +1 -1
  56. package/dist/validation.d.ts.map +1 -1
  57. package/dist/validation.js +3 -1
  58. package/package.json +1 -1
  59. package/src/components/IframeFallbackLink.tsx +57 -0
  60. package/src/components/UIResourceRenderer.tsx +31 -4
  61. package/src/components/VideoRenderer.tsx +20 -0
  62. package/src/context/MCPUIConfigContext.tsx +151 -0
  63. package/src/context/MCPUIStringsContext.tsx +10 -0
  64. package/src/index.ts +15 -0
  65. package/src/services/index.ts +1 -0
  66. package/src/services/validation.ts +38 -16
  67. package/src/solid-jsx-attrs.d.ts +24 -0
  68. package/src/utils/iframe-coep.ts +74 -0
  69. package/src/validation.ts +4 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,150 @@ 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.21.0] - 2026-09-17
9
+
10
+ The library's iframes now work on a host that serves
11
+ `Cross-Origin-Embedder-Policy: credentialless`.
12
+
13
+ ### The problem
14
+
15
+ Under that header a browser refuses **any** cross-origin iframe whose own
16
+ document does not itself send a COEP header. That is YouTube, Vimeo, Google
17
+ Docs, Notion — nearly every entry of `DEFAULT_IFRAME_DOMAINS`. The frame stays
18
+ blank and nothing in the page can detect it: a refused cross-origin document
19
+ fires no `error` event the parent can observe. The `credentialless` *header*
20
+ only relaxes the rule for sub-resources (images, fonts, map tiles — which is
21
+ why the quickchart `<img>` fallback and the Leaflet tiles were never
22
+ affected); nested documents need the boolean HTML attribute
23
+ `<iframe credentialless>` on the tag itself.
24
+
25
+ ### What the library now does
26
+
27
+ Both iframes it renders — `IframeRenderer` (the `iframe` component) and
28
+ `VideoRenderer`'s YouTube / Vimeo embed — can carry `credentialless` when their
29
+ host is **not** in `TRUSTED_IFRAME_DOMAINS`.
30
+
31
+ - **The `iframe` component on an untrusted host gets it by default.** Those
32
+ frames already ran without cookies, since their sandbox has no
33
+ `allow-same-origin`. The attribute costs them nothing and is exactly what
34
+ unblocks them under COEP.
35
+ - **The video embed does NOT get it by default.** It carries no sandbox, so a
36
+ YouTube or Vimeo frame really does hold provider cookies (consent, playback
37
+ state, sign-in). Taking them away on every host — including the majority
38
+ that send no COEP header — would be a silent regression, so `'auto'` leaves
39
+ it alone and a COEP host opts in with `iframeCredentialless: 'always'`.
40
+ - **Trusted hosts deliberately do NOT get it.** `TRUSTED_IFRAME_DOMAINS`
41
+ (Google Docs/Drive, Notion, Airtable, Figma, Linear, Stripe, Polar, HubSpot,
42
+ Calendly, …) exists precisely because those embeds need their own cookies to
43
+ authenticate. Loaded `credentialless` they would render a login screen — a
44
+ visibly broken embed. Under COEP they stay blocked, which is the honest
45
+ outcome; `iframeCredentialless: 'always'` overrides it if you disagree.
46
+
47
+ The attribute is a **boolean** attribute: it is absent from the DOM when it
48
+ does not apply, never `credentialless="false"`.
49
+
50
+ ### Added
51
+
52
+ - **`<MCPUIConfigProvider>` / `useMCPUIConfig()` / `DEFAULT_MCPUI_CONFIG` /
53
+ `MCPUIConfig`** (new `src/context/MCPUIConfigContext.tsx`, exported from the
54
+ root barrel) — host-level rendering policy, the behavioural counterpart of
55
+ `MCPUIStrings`. Three options:
56
+
57
+ | option | default | meaning |
58
+ | --- | --- | --- |
59
+ | `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. |
60
+ | `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. |
61
+ | `iframePolicy` | `'strict'` | how an `iframe` component's host is validated; `'extend'` also accepts `customIframeDomains`. |
62
+ | `customIframeDomains` | `[]` | extra hosts the allow-list accepts under `iframePolicy: 'extend'`. Without this, an unlisted host never reaches a renderer. |
63
+ | `iframeFallbackLink` | `'auto'` | when the "open in a new tab" link shows under an embed. `'auto'`: only on a cross-origin-isolated page. |
64
+
65
+ ```tsx
66
+ import { MCPUIConfigProvider } from '@seed-ship/mcp-ui-solid'
67
+
68
+ // A host that serves COEP: credentialless and embeds its own tool.
69
+ <MCPUIConfigProvider
70
+ config={{
71
+ iframeCredentialless: 'always',
72
+ iframePolicy: 'extend',
73
+ customIframeDomains: ['embed.acme.com'],
74
+ customTrustedIframeDomains: ['embed.acme.com'],
75
+ iframeFallbackLink: 'auto',
76
+ }}
77
+ >
78
+ <App />
79
+ </MCPUIConfigProvider>
80
+
81
+ // A host with no COEP header that does not want its embeds to lose cookies.
82
+ <MCPUIConfigProvider config={{ iframeCredentialless: 'never' }}>
83
+ <App />
84
+ </MCPUIConfigProvider>
85
+ ```
86
+
87
+ With no provider mounted, `DEFAULT_MCPUI_CONFIG` applies — every renderer
88
+ still works standalone.
89
+
90
+ - **`isTrustedIframeDomain(url, { customTrustedDomains? })`** — the trusted-host
91
+ test `getIframeSandbox` already performed, extracted and exported from the
92
+ root barrel and from the `@seed-ship/mcp-ui-solid/validation` subpath
93
+ (`getIframeSandbox` is now exported from that subpath too). Invalid URL →
94
+ `false`, so an unparsable URL is never trusted and does receive the
95
+ attribute. Matching is unchanged: exact host, or a subdomain of a listed
96
+ host.
97
+
98
+ - **`MCPUIStrings.iframeOpenInNewTab`** — optional, default
99
+ `'Open in a new tab'`. Label of the link rendered under an embed, which is
100
+ the only way out of a silently blocked frame. Visibility follows
101
+ `iframeFallbackLink`; `window.crossOriginIsolated` is read **after mount**,
102
+ never during render, so SSR markup and hydration agree. That flag requires
103
+ COOP `same-origin` on top of COEP — a host that sends COEP alone should set
104
+ `iframeFallbackLink: 'always'`, since its embeds are blocked while the flag
105
+ reads false. The link is skipped
106
+ when `safeUrl()` rejects the URL. `MCPUIStrings` now covers **271 keys**.
107
+
108
+ ### Fixed
109
+
110
+ - `IframeRenderer` called `getIframeSandbox(params.url)` with no options, so a
111
+ host's custom trusted domains never reached the sandbox. It now passes
112
+ `{ customTrustedDomains: config.customTrustedIframeDomains }`, which is what
113
+ makes `customTrustedIframeDomains` grant `allow-same-origin` as documented.
114
+
115
+ ### Browser support
116
+
117
+ | | header `COEP: credentialless` | attribute `<iframe credentialless>` |
118
+ | --- | --- | --- |
119
+ | Chrome / Edge | 96+ | **110+** |
120
+ | Firefox | 119+ | **not supported** |
121
+ | Safari | not supported (value ignored) | not supported |
122
+
123
+ - **Chrome / Edge** — the attribute fixes the problem. This is the majority
124
+ case.
125
+ - **Firefox** — honours the header but ignores the attribute, so third-party
126
+ iframes stay blocked for as long as the host page sends COEP. Nothing in
127
+ this library can change that; it is a host-side trade-off (drop COEP, or
128
+ accept the degradation on Firefox). The fallback link is what the user gets
129
+ there.
130
+ - **Safari** — does not know the `credentialless` value and treats the header
131
+ as absent: no cross-origin isolation, but embeds work.
132
+
133
+ ### Note for hosts
134
+
135
+ The library's allow-list is not a substitute for your CSP. The browser also
136
+ enforces `frame-src`, which the host sets — a domain missing from it stays
137
+ blocked whatever this library emits (see the comment at
138
+ `services/validation.ts` on `DEFAULT_IFRAME_DOMAINS`: "Must match CSP
139
+ frame-src directive").
140
+
141
+ ### Tests
142
+
143
+ `src/components/IframeCredentialless.test.tsx` (25 tests) — the
144
+ `isTrustedIframeDomain` matching table, attribute presence/absence through
145
+ `<UIResourceRenderer>` and `<VideoRenderer>` for each `iframeCredentialless`
146
+ mode, the `customTrustedIframeDomains` → sandbox plumbing, the fallback link
147
+ under each `iframeFallbackLink` mode with `window.crossOriginIsolated`
148
+ stubbed, and the SSR-shape assertions on the pure decision helpers. Plus a new
149
+ `iframeOpenInNewTab` case in `MCPUIStringsContext.sweep.test.tsx` and the
150
+ default pinned in `mcpui-strings-defaults.test.ts`.
151
+
8
152
  ## [6.20.0] - 2026-09-17
9
153
 
10
154
  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,
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const web = require("solid-js/web");
4
+ const solidJs = require("solid-js");
5
+ const safeUrl = require("../utils/safe-url.cjs");
6
+ const MCPUIStringsContext = require("../context/MCPUIStringsContext.cjs");
7
+ const MCPUIConfigContext = require("../context/MCPUIConfigContext.cjs");
8
+ const iframeCoep = require("../utils/iframe-coep.cjs");
9
+ var _tmpl$ = /* @__PURE__ */ web.template(`<div><a target=_blank rel="noopener noreferrer"data-mcp-ui-action=open-external class="text-xs text-blue-600 dark:text-blue-400 hover:underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded">`);
10
+ function IframeFallbackLink(props) {
11
+ const strings = MCPUIStringsContext.useMCPUIStrings();
12
+ const config = MCPUIConfigContext.useMCPUIConfig();
13
+ const crossOriginIsolated = iframeCoep.createCrossOriginIsolated();
14
+ const href = solidJs.createMemo(() => safeUrl.safeUrl(props.url));
15
+ const visible = solidJs.createMemo(() => !!href() && iframeCoep.shouldShowIframeFallbackLink(config, crossOriginIsolated()));
16
+ return web.createComponent(solidJs.Show, {
17
+ get when() {
18
+ return visible();
19
+ },
20
+ get children() {
21
+ var _el$ = web.getNextElement(_tmpl$), _el$2 = _el$.firstChild;
22
+ web.insert(_el$2, () => strings.iframeOpenInNewTab);
23
+ web.effect((_p$) => {
24
+ var _v$ = `px-4 py-2 text-right ${props.class || ""}`, _v$2 = href();
25
+ _v$ !== _p$.e && web.className(_el$, _p$.e = _v$);
26
+ _v$2 !== _p$.t && web.setAttribute(_el$2, "href", _p$.t = _v$2);
27
+ return _p$;
28
+ }, {
29
+ e: void 0,
30
+ t: void 0
31
+ });
32
+ return _el$;
33
+ }
34
+ });
35
+ }
36
+ exports.IframeFallbackLink = IframeFallbackLink;
37
+ //# sourceMappingURL=IframeFallbackLink.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IframeFallbackLink.cjs","sources":["../../src/components/IframeFallbackLink.tsx"],"sourcesContent":["/**\n * IframeFallbackLink — the way out of a silently blocked embed.\n *\n * @since v6.21.0\n *\n * An iframe refused by `Cross-Origin-Embedder-Policy` fires no event the\n * parent page can observe (`onerror` does not fire for a rejected\n * cross-origin document), so the user is left with a blank frame and no\n * explanation. This link, rendered under the frame, always works.\n *\n * Visibility follows `MCPUIConfig.iframeFallbackLink`; `'auto'` (the default)\n * shows it only on a cross-origin-isolated page. The URL goes through\n * `safeUrl` — an unsafe one renders nothing at all.\n */\n\nimport { createMemo, Show, type JSX } from 'solid-js'\nimport { safeUrl } from '../utils/safe-url'\nimport { useMCPUIStrings } from '../context/MCPUIStringsContext'\nimport { useMCPUIConfig } from '../context/MCPUIConfigContext'\nimport {\n createCrossOriginIsolated,\n shouldShowIframeFallbackLink,\n} from '../utils/iframe-coep'\n\nexport interface IframeFallbackLinkProps {\n /** The embedded URL. Bound to `href` only when `safeUrl` accepts it. */\n url: string\n /** Extra classes on the wrapping `<div>`. */\n class?: string\n}\n\nexport function IframeFallbackLink(props: IframeFallbackLinkProps): JSX.Element {\n const strings = useMCPUIStrings()\n const config = useMCPUIConfig()\n const crossOriginIsolated = createCrossOriginIsolated()\n\n const href = createMemo(() => safeUrl(props.url))\n const visible = createMemo(\n () => !!href() && shouldShowIframeFallbackLink(config, crossOriginIsolated())\n )\n\n return (\n <Show when={visible()}>\n <div class={`px-4 py-2 text-right ${props.class || ''}`}>\n <a\n href={href()}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-mcp-ui-action=\"open-external\"\n class=\"text-xs text-blue-600 dark:text-blue-400 hover:underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded\"\n >\n {strings.iframeOpenInNewTab}\n </a>\n </div>\n </Show>\n )\n}\n"],"names":["IframeFallbackLink","props","strings","useMCPUIStrings","config","useMCPUIConfig","crossOriginIsolated","createCrossOriginIsolated","href","createMemo","safeUrl","url","visible","shouldShowIframeFallbackLink","_$createComponent","Show","when","children","_el$","_$getNextElement","_tmpl$","_el$2","firstChild","_$insert","iframeOpenInNewTab","_$effect","_p$","_v$","class","_v$2","e","_$className","t","_$setAttribute","undefined"],"mappings":";;;;;;;;;AA+BO,SAASA,mBAAmBC,OAA6C;AAC9E,QAAMC,UAAUC,oBAAAA,gBAAAA;AAChB,QAAMC,SAASC,mBAAAA,eAAAA;AACf,QAAMC,sBAAsBC,WAAAA,0BAAAA;AAE5B,QAAMC,OAAOC,QAAAA,WAAW,MAAMC,QAAAA,QAAQT,MAAMU,GAAG,CAAC;AAChD,QAAMC,UAAUH,mBACd,MAAM,CAAC,CAACD,KAAAA,KAAUK,wCAA6BT,QAAQE,oBAAAA,CAAqB,CAC9E;AAEA,SAAAQ,IAAAA,gBACGC,QAAAA,MAAI;AAAA,IAAA,IAACC,OAAI;AAAA,aAAEJ,QAAAA;AAAAA,IAAS;AAAA,IAAA,IAAAK,WAAA;AAAA,UAAAC,OAAAC,IAAAA,eAAAC,MAAA,GAAAC,QAAAH,KAAAI;AAAAC,UAAAA,OAAAF,OAAA,MASdnB,QAAQsB,kBAAkB;AAAAC,UAAAA,OAAAC,CAAAA,QAAA;AAAA,YAAAC,MARnB,wBAAwB1B,MAAM2B,SAAS,EAAE,IAAEC,OAE7CrB,KAAAA;AAAMmB,gBAAAD,IAAAI,KAAAC,IAAAA,UAAAb,MAAAQ,IAAAI,IAAAH,GAAA;AAAAE,iBAAAH,IAAAM,KAAAC,IAAAA,aAAAZ,OAAA,QAAAK,IAAAM,IAAAH,IAAA;AAAA,eAAAH;AAAAA,MAAA,GAAA;AAAA,QAAAI,GAAAI;AAAAA,QAAAF,GAAAE;AAAAA,MAAAA,CAAA;AAAA,aAAAhB;AAAAA,IAAA;AAAA,EAAA,CAAA;AAWtB;;"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * IframeFallbackLink — the way out of a silently blocked embed.
3
+ *
4
+ * @since v6.21.0
5
+ *
6
+ * An iframe refused by `Cross-Origin-Embedder-Policy` fires no event the
7
+ * parent page can observe (`onerror` does not fire for a rejected
8
+ * cross-origin document), so the user is left with a blank frame and no
9
+ * explanation. This link, rendered under the frame, always works.
10
+ *
11
+ * Visibility follows `MCPUIConfig.iframeFallbackLink`; `'auto'` (the default)
12
+ * shows it only on a cross-origin-isolated page. The URL goes through
13
+ * `safeUrl` — an unsafe one renders nothing at all.
14
+ */
15
+ import { type JSX } from 'solid-js';
16
+ export interface IframeFallbackLinkProps {
17
+ /** The embedded URL. Bound to `href` only when `safeUrl` accepts it. */
18
+ url: string;
19
+ /** Extra classes on the wrapping `<div>`. */
20
+ class?: string;
21
+ }
22
+ export declare function IframeFallbackLink(props: IframeFallbackLinkProps): JSX.Element;
23
+ //# sourceMappingURL=IframeFallbackLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IframeFallbackLink.d.ts","sourceRoot":"","sources":["../../src/components/IframeFallbackLink.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAoB,KAAK,GAAG,EAAE,MAAM,UAAU,CAAA;AASrD,MAAM,WAAW,uBAAuB;IACtC,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAA;IACX,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,GAAG,CAAC,OAAO,CAyB9E"}
@@ -0,0 +1,37 @@
1
+ import { createComponent, getNextElement, insert, effect, className, setAttribute, template } from "solid-js/web";
2
+ import { createMemo, Show } from "solid-js";
3
+ import { safeUrl } from "../utils/safe-url.js";
4
+ import { useMCPUIStrings } from "../context/MCPUIStringsContext.js";
5
+ import { useMCPUIConfig } from "../context/MCPUIConfigContext.js";
6
+ import { createCrossOriginIsolated, shouldShowIframeFallbackLink } from "../utils/iframe-coep.js";
7
+ var _tmpl$ = /* @__PURE__ */ template(`<div><a target=_blank rel="noopener noreferrer"data-mcp-ui-action=open-external class="text-xs text-blue-600 dark:text-blue-400 hover:underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded">`);
8
+ function IframeFallbackLink(props) {
9
+ const strings = useMCPUIStrings();
10
+ const config = useMCPUIConfig();
11
+ const crossOriginIsolated = createCrossOriginIsolated();
12
+ const href = createMemo(() => safeUrl(props.url));
13
+ const visible = createMemo(() => !!href() && shouldShowIframeFallbackLink(config, crossOriginIsolated()));
14
+ return createComponent(Show, {
15
+ get when() {
16
+ return visible();
17
+ },
18
+ get children() {
19
+ var _el$ = getNextElement(_tmpl$), _el$2 = _el$.firstChild;
20
+ insert(_el$2, () => strings.iframeOpenInNewTab);
21
+ effect((_p$) => {
22
+ var _v$ = `px-4 py-2 text-right ${props.class || ""}`, _v$2 = href();
23
+ _v$ !== _p$.e && className(_el$, _p$.e = _v$);
24
+ _v$2 !== _p$.t && setAttribute(_el$2, "href", _p$.t = _v$2);
25
+ return _p$;
26
+ }, {
27
+ e: void 0,
28
+ t: void 0
29
+ });
30
+ return _el$;
31
+ }
32
+ });
33
+ }
34
+ export {
35
+ IframeFallbackLink
36
+ };
37
+ //# sourceMappingURL=IframeFallbackLink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IframeFallbackLink.js","sources":["../../src/components/IframeFallbackLink.tsx"],"sourcesContent":["/**\n * IframeFallbackLink — the way out of a silently blocked embed.\n *\n * @since v6.21.0\n *\n * An iframe refused by `Cross-Origin-Embedder-Policy` fires no event the\n * parent page can observe (`onerror` does not fire for a rejected\n * cross-origin document), so the user is left with a blank frame and no\n * explanation. This link, rendered under the frame, always works.\n *\n * Visibility follows `MCPUIConfig.iframeFallbackLink`; `'auto'` (the default)\n * shows it only on a cross-origin-isolated page. The URL goes through\n * `safeUrl` — an unsafe one renders nothing at all.\n */\n\nimport { createMemo, Show, type JSX } from 'solid-js'\nimport { safeUrl } from '../utils/safe-url'\nimport { useMCPUIStrings } from '../context/MCPUIStringsContext'\nimport { useMCPUIConfig } from '../context/MCPUIConfigContext'\nimport {\n createCrossOriginIsolated,\n shouldShowIframeFallbackLink,\n} from '../utils/iframe-coep'\n\nexport interface IframeFallbackLinkProps {\n /** The embedded URL. Bound to `href` only when `safeUrl` accepts it. */\n url: string\n /** Extra classes on the wrapping `<div>`. */\n class?: string\n}\n\nexport function IframeFallbackLink(props: IframeFallbackLinkProps): JSX.Element {\n const strings = useMCPUIStrings()\n const config = useMCPUIConfig()\n const crossOriginIsolated = createCrossOriginIsolated()\n\n const href = createMemo(() => safeUrl(props.url))\n const visible = createMemo(\n () => !!href() && shouldShowIframeFallbackLink(config, crossOriginIsolated())\n )\n\n return (\n <Show when={visible()}>\n <div class={`px-4 py-2 text-right ${props.class || ''}`}>\n <a\n href={href()}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-mcp-ui-action=\"open-external\"\n class=\"text-xs text-blue-600 dark:text-blue-400 hover:underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded\"\n >\n {strings.iframeOpenInNewTab}\n </a>\n </div>\n </Show>\n )\n}\n"],"names":["IframeFallbackLink","props","strings","useMCPUIStrings","config","useMCPUIConfig","crossOriginIsolated","createCrossOriginIsolated","href","createMemo","safeUrl","url","visible","shouldShowIframeFallbackLink","_$createComponent","Show","when","children","_el$","_$getNextElement","_tmpl$","_el$2","firstChild","_$insert","iframeOpenInNewTab","_$effect","_p$","_v$","class","_v$2","e","_$className","t","_$setAttribute","undefined"],"mappings":";;;;;;;AA+BO,SAASA,mBAAmBC,OAA6C;AAC9E,QAAMC,UAAUC,gBAAAA;AAChB,QAAMC,SAASC,eAAAA;AACf,QAAMC,sBAAsBC,0BAAAA;AAE5B,QAAMC,OAAOC,WAAW,MAAMC,QAAQT,MAAMU,GAAG,CAAC;AAChD,QAAMC,UAAUH,WACd,MAAM,CAAC,CAACD,KAAAA,KAAUK,6BAA6BT,QAAQE,oBAAAA,CAAqB,CAC9E;AAEA,SAAAQ,gBACGC,MAAI;AAAA,IAAA,IAACC,OAAI;AAAA,aAAEJ,QAAAA;AAAAA,IAAS;AAAA,IAAA,IAAAK,WAAA;AAAA,UAAAC,OAAAC,eAAAC,MAAA,GAAAC,QAAAH,KAAAI;AAAAC,aAAAF,OAAA,MASdnB,QAAQsB,kBAAkB;AAAAC,aAAAC,CAAAA,QAAA;AAAA,YAAAC,MARnB,wBAAwB1B,MAAM2B,SAAS,EAAE,IAAEC,OAE7CrB,KAAAA;AAAMmB,gBAAAD,IAAAI,KAAAC,UAAAb,MAAAQ,IAAAI,IAAAH,GAAA;AAAAE,iBAAAH,IAAAM,KAAAC,aAAAZ,OAAA,QAAAK,IAAAM,IAAAH,IAAA;AAAA,eAAAH;AAAAA,MAAA,GAAA;AAAA,QAAAI,GAAAI;AAAAA,QAAAF,GAAAE;AAAAA,MAAAA,CAAA;AAAA,aAAAhB;AAAAA,IAAA;AAAA,EAAA,CAAA;AAWtB;"}