@seed-ship/mcp-ui-solid 6.19.1 → 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 (282) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/README.md +911 -4
  3. package/dist/adapters/connector.cjs +15 -4
  4. package/dist/adapters/connector.cjs.map +1 -1
  5. package/dist/adapters/connector.d.ts +38 -0
  6. package/dist/adapters/connector.d.ts.map +1 -1
  7. package/dist/adapters/connector.js +15 -4
  8. package/dist/adapters/connector.js.map +1 -1
  9. package/dist/adapters/index.d.ts +4 -3
  10. package/dist/adapters/index.d.ts.map +1 -1
  11. package/dist/adapters/macro-run.cjs +25 -13
  12. package/dist/adapters/macro-run.cjs.map +1 -1
  13. package/dist/adapters/macro-run.d.ts +37 -2
  14. package/dist/adapters/macro-run.d.ts.map +1 -1
  15. package/dist/adapters/macro-run.js +25 -13
  16. package/dist/adapters/macro-run.js.map +1 -1
  17. package/dist/adapters.cjs +2 -0
  18. package/dist/adapters.cjs.map +1 -1
  19. package/dist/adapters.js +4 -2
  20. package/dist/components/ActionGroupRenderer.cjs +3 -1
  21. package/dist/components/ActionGroupRenderer.cjs.map +1 -1
  22. package/dist/components/ActionGroupRenderer.d.ts.map +1 -1
  23. package/dist/components/ActionGroupRenderer.js +3 -1
  24. package/dist/components/ActionGroupRenderer.js.map +1 -1
  25. package/dist/components/AgentCard.cjs +11 -8
  26. package/dist/components/AgentCard.cjs.map +1 -1
  27. package/dist/components/AgentCard.d.ts.map +1 -1
  28. package/dist/components/AgentCard.js +11 -8
  29. package/dist/components/AgentCard.js.map +1 -1
  30. package/dist/components/AgentHandoff.cjs +6 -1
  31. package/dist/components/AgentHandoff.cjs.map +1 -1
  32. package/dist/components/AgentHandoff.d.ts.map +1 -1
  33. package/dist/components/AgentHandoff.js +6 -1
  34. package/dist/components/AgentHandoff.js.map +1 -1
  35. package/dist/components/ArtifactRenderer.cjs +25 -6
  36. package/dist/components/ArtifactRenderer.cjs.map +1 -1
  37. package/dist/components/ArtifactRenderer.d.ts.map +1 -1
  38. package/dist/components/ArtifactRenderer.js +25 -6
  39. package/dist/components/ArtifactRenderer.js.map +1 -1
  40. package/dist/components/AutocompleteDropdown.cjs +30 -24
  41. package/dist/components/AutocompleteDropdown.cjs.map +1 -1
  42. package/dist/components/AutocompleteDropdown.d.ts.map +1 -1
  43. package/dist/components/AutocompleteDropdown.js +30 -24
  44. package/dist/components/AutocompleteDropdown.js.map +1 -1
  45. package/dist/components/AutocompleteFormField.cjs +3 -1
  46. package/dist/components/AutocompleteFormField.cjs.map +1 -1
  47. package/dist/components/AutocompleteFormField.d.ts.map +1 -1
  48. package/dist/components/AutocompleteFormField.js +4 -2
  49. package/dist/components/AutocompleteFormField.js.map +1 -1
  50. package/dist/components/BriefingDiff.cjs +45 -39
  51. package/dist/components/BriefingDiff.cjs.map +1 -1
  52. package/dist/components/BriefingDiff.d.ts.map +1 -1
  53. package/dist/components/BriefingDiff.js +45 -39
  54. package/dist/components/BriefingDiff.js.map +1 -1
  55. package/dist/components/CarouselRenderer.cjs +8 -2
  56. package/dist/components/CarouselRenderer.cjs.map +1 -1
  57. package/dist/components/CarouselRenderer.d.ts.map +1 -1
  58. package/dist/components/CarouselRenderer.js +8 -2
  59. package/dist/components/CarouselRenderer.js.map +1 -1
  60. package/dist/components/ChartJSRenderer.cjs +60 -36
  61. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  62. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  63. package/dist/components/ChartJSRenderer.js +61 -37
  64. package/dist/components/ChartJSRenderer.js.map +1 -1
  65. package/dist/components/ChatPrompt.cjs +15 -9
  66. package/dist/components/ChatPrompt.cjs.map +1 -1
  67. package/dist/components/ChatPrompt.d.ts.map +1 -1
  68. package/dist/components/ChatPrompt.js +15 -9
  69. package/dist/components/ChatPrompt.js.map +1 -1
  70. package/dist/components/CodeBlockRenderer.cjs +34 -16
  71. package/dist/components/CodeBlockRenderer.cjs.map +1 -1
  72. package/dist/components/CodeBlockRenderer.d.ts.map +1 -1
  73. package/dist/components/CodeBlockRenderer.js +34 -16
  74. package/dist/components/CodeBlockRenderer.js.map +1 -1
  75. package/dist/components/DataPreviewSection.cjs +91 -44
  76. package/dist/components/DataPreviewSection.cjs.map +1 -1
  77. package/dist/components/DataPreviewSection.d.ts.map +1 -1
  78. package/dist/components/DataPreviewSection.js +92 -45
  79. package/dist/components/DataPreviewSection.js.map +1 -1
  80. package/dist/components/DegradedFallback.cjs +20 -16
  81. package/dist/components/DegradedFallback.cjs.map +1 -1
  82. package/dist/components/DegradedFallback.d.ts.map +1 -1
  83. package/dist/components/DegradedFallback.js +20 -16
  84. package/dist/components/DegradedFallback.js.map +1 -1
  85. package/dist/components/FooterRenderer.cjs +21 -17
  86. package/dist/components/FooterRenderer.cjs.map +1 -1
  87. package/dist/components/FooterRenderer.d.ts.map +1 -1
  88. package/dist/components/FooterRenderer.js +21 -17
  89. package/dist/components/FooterRenderer.js.map +1 -1
  90. package/dist/components/FormFieldRenderer.cjs +116 -93
  91. package/dist/components/FormFieldRenderer.cjs.map +1 -1
  92. package/dist/components/FormFieldRenderer.d.ts.map +1 -1
  93. package/dist/components/FormFieldRenderer.js +116 -93
  94. package/dist/components/FormFieldRenderer.js.map +1 -1
  95. package/dist/components/FormRenderer.cjs +55 -31
  96. package/dist/components/FormRenderer.cjs.map +1 -1
  97. package/dist/components/FormRenderer.d.ts.map +1 -1
  98. package/dist/components/FormRenderer.js +55 -31
  99. package/dist/components/FormRenderer.js.map +1 -1
  100. package/dist/components/GenerativeUIErrorBoundary.cjs +20 -15
  101. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
  102. package/dist/components/GenerativeUIErrorBoundary.d.ts.map +1 -1
  103. package/dist/components/GenerativeUIErrorBoundary.js +20 -15
  104. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
  105. package/dist/components/GraphRenderer.cjs +56 -33
  106. package/dist/components/GraphRenderer.cjs.map +1 -1
  107. package/dist/components/GraphRenderer.d.ts.map +1 -1
  108. package/dist/components/GraphRenderer.js +56 -33
  109. package/dist/components/GraphRenderer.js.map +1 -1
  110. package/dist/components/IframeFallbackLink.cjs +37 -0
  111. package/dist/components/IframeFallbackLink.cjs.map +1 -0
  112. package/dist/components/IframeFallbackLink.d.ts +23 -0
  113. package/dist/components/IframeFallbackLink.d.ts.map +1 -0
  114. package/dist/components/IframeFallbackLink.js +37 -0
  115. package/dist/components/IframeFallbackLink.js.map +1 -0
  116. package/dist/components/ImageGalleryRenderer.cjs +13 -4
  117. package/dist/components/ImageGalleryRenderer.cjs.map +1 -1
  118. package/dist/components/ImageGalleryRenderer.d.ts.map +1 -1
  119. package/dist/components/ImageGalleryRenderer.js +13 -4
  120. package/dist/components/ImageGalleryRenderer.js.map +1 -1
  121. package/dist/components/LightboxOverlay.cjs +16 -8
  122. package/dist/components/LightboxOverlay.cjs.map +1 -1
  123. package/dist/components/LightboxOverlay.d.ts.map +1 -1
  124. package/dist/components/LightboxOverlay.js +17 -9
  125. package/dist/components/LightboxOverlay.js.map +1 -1
  126. package/dist/components/MapRenderer.cjs +59 -29
  127. package/dist/components/MapRenderer.cjs.map +1 -1
  128. package/dist/components/MapRenderer.d.ts +10 -2
  129. package/dist/components/MapRenderer.d.ts.map +1 -1
  130. package/dist/components/MapRenderer.js +60 -30
  131. package/dist/components/MapRenderer.js.map +1 -1
  132. package/dist/components/ModalRenderer.cjs +4 -1
  133. package/dist/components/ModalRenderer.cjs.map +1 -1
  134. package/dist/components/ModalRenderer.d.ts.map +1 -1
  135. package/dist/components/ModalRenderer.js +5 -2
  136. package/dist/components/ModalRenderer.js.map +1 -1
  137. package/dist/components/ScratchpadPanel.cjs +578 -535
  138. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  139. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  140. package/dist/components/ScratchpadPanel.js +578 -535
  141. package/dist/components/ScratchpadPanel.js.map +1 -1
  142. package/dist/components/StreamingUIRenderer.cjs +92 -28
  143. package/dist/components/StreamingUIRenderer.cjs.map +1 -1
  144. package/dist/components/StreamingUIRenderer.d.ts.map +1 -1
  145. package/dist/components/StreamingUIRenderer.js +92 -28
  146. package/dist/components/StreamingUIRenderer.js.map +1 -1
  147. package/dist/components/UIResourceRenderer.cjs +481 -365
  148. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  149. package/dist/components/UIResourceRenderer.d.ts +21 -0
  150. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  151. package/dist/components/UIResourceRenderer.js +484 -368
  152. package/dist/components/UIResourceRenderer.js.map +1 -1
  153. package/dist/components/VerifiedText.cjs +39 -20
  154. package/dist/components/VerifiedText.cjs.map +1 -1
  155. package/dist/components/VerifiedText.d.ts +2 -1
  156. package/dist/components/VerifiedText.d.ts.map +1 -1
  157. package/dist/components/VerifiedText.js +39 -20
  158. package/dist/components/VerifiedText.js.map +1 -1
  159. package/dist/components/VideoRenderer.cjs +51 -20
  160. package/dist/components/VideoRenderer.cjs.map +1 -1
  161. package/dist/components/VideoRenderer.d.ts.map +1 -1
  162. package/dist/components/VideoRenderer.js +51 -20
  163. package/dist/components/VideoRenderer.js.map +1 -1
  164. package/dist/components/chart-data-table.cjs +38 -17
  165. package/dist/components/chart-data-table.cjs.map +1 -1
  166. package/dist/components/chart-data-table.d.ts +30 -1
  167. package/dist/components/chart-data-table.d.ts.map +1 -1
  168. package/dist/components/chart-data-table.js +38 -17
  169. package/dist/components/chart-data-table.js.map +1 -1
  170. package/dist/context/MCPActionContext.cjs +3 -1
  171. package/dist/context/MCPActionContext.cjs.map +1 -1
  172. package/dist/context/MCPActionContext.d.ts.map +1 -1
  173. package/dist/context/MCPActionContext.js +3 -1
  174. package/dist/context/MCPActionContext.js.map +1 -1
  175. package/dist/context/MCPUIConfigContext.cjs +29 -0
  176. package/dist/context/MCPUIConfigContext.cjs.map +1 -0
  177. package/dist/context/MCPUIConfigContext.d.ts +124 -0
  178. package/dist/context/MCPUIConfigContext.d.ts.map +1 -0
  179. package/dist/context/MCPUIConfigContext.js +29 -0
  180. package/dist/context/MCPUIConfigContext.js.map +1 -0
  181. package/dist/context/MCPUIStringsContext.cjs +287 -2
  182. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  183. package/dist/context/MCPUIStringsContext.d.ts +607 -3
  184. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  185. package/dist/context/MCPUIStringsContext.js +287 -2
  186. package/dist/context/MCPUIStringsContext.js.map +1 -1
  187. package/dist/hooks/index.d.ts +2 -2
  188. package/dist/hooks/index.d.ts.map +1 -1
  189. package/dist/hooks/useStreamingUI.cjs +28 -10
  190. package/dist/hooks/useStreamingUI.cjs.map +1 -1
  191. package/dist/hooks/useStreamingUI.d.ts +37 -0
  192. package/dist/hooks/useStreamingUI.d.ts.map +1 -1
  193. package/dist/hooks/useStreamingUI.js +28 -10
  194. package/dist/hooks/useStreamingUI.js.map +1 -1
  195. package/dist/hooks.cjs +1 -0
  196. package/dist/hooks.cjs.map +1 -1
  197. package/dist/hooks.js +2 -1
  198. package/dist/index.cjs +29 -8
  199. package/dist/index.cjs.map +1 -1
  200. package/dist/index.d.cts +12 -4
  201. package/dist/index.d.ts +12 -4
  202. package/dist/index.d.ts.map +1 -1
  203. package/dist/index.js +24 -3
  204. package/dist/index.js.map +1 -1
  205. package/dist/services/index.d.ts +1 -1
  206. package/dist/services/index.d.ts.map +1 -1
  207. package/dist/services/validation.cjs +52 -26
  208. package/dist/services/validation.cjs.map +1 -1
  209. package/dist/services/validation.d.ts +67 -2
  210. package/dist/services/validation.d.ts.map +1 -1
  211. package/dist/services/validation.js +52 -26
  212. package/dist/services/validation.js.map +1 -1
  213. package/dist/utils/degraded-projections.cjs +37 -12
  214. package/dist/utils/degraded-projections.cjs.map +1 -1
  215. package/dist/utils/degraded-projections.d.ts +39 -3
  216. package/dist/utils/degraded-projections.d.ts.map +1 -1
  217. package/dist/utils/degraded-projections.js +37 -12
  218. package/dist/utils/degraded-projections.js.map +1 -1
  219. package/dist/utils/format-string.cjs +10 -0
  220. package/dist/utils/format-string.cjs.map +1 -0
  221. package/dist/utils/format-string.d.ts +26 -0
  222. package/dist/utils/format-string.d.ts.map +1 -0
  223. package/dist/utils/format-string.js +10 -0
  224. package/dist/utils/format-string.js.map +1 -0
  225. package/dist/utils/iframe-coep.cjs +30 -0
  226. package/dist/utils/iframe-coep.cjs.map +1 -0
  227. package/dist/utils/iframe-coep.d.ts +46 -0
  228. package/dist/utils/iframe-coep.d.ts.map +1 -0
  229. package/dist/utils/iframe-coep.js +30 -0
  230. package/dist/utils/iframe-coep.js.map +1 -0
  231. package/dist/validation.cjs +5 -0
  232. package/dist/validation.cjs.map +1 -1
  233. package/dist/validation.d.cts +2 -1
  234. package/dist/validation.d.ts +2 -1
  235. package/dist/validation.d.ts.map +1 -1
  236. package/dist/validation.js +6 -1
  237. package/package.json +1 -1
  238. package/src/adapters/connector.ts +58 -4
  239. package/src/adapters/index.ts +12 -2
  240. package/src/adapters/macro-run.ts +71 -11
  241. package/src/components/ActionGroupRenderer.tsx +3 -1
  242. package/src/components/AgentCard.tsx +27 -9
  243. package/src/components/AgentHandoff.tsx +4 -1
  244. package/src/components/ArtifactRenderer.tsx +17 -5
  245. package/src/components/AutocompleteDropdown.tsx +8 -6
  246. package/src/components/AutocompleteFormField.tsx +5 -1
  247. package/src/components/BriefingDiff.tsx +11 -3
  248. package/src/components/CarouselRenderer.tsx +4 -2
  249. package/src/components/ChartJSRenderer.tsx +18 -9
  250. package/src/components/ChatPrompt.tsx +13 -8
  251. package/src/components/CodeBlockRenderer.tsx +12 -10
  252. package/src/components/DataPreviewSection.tsx +92 -26
  253. package/src/components/DegradedFallback.tsx +4 -2
  254. package/src/components/FooterRenderer.tsx +3 -1
  255. package/src/components/FormFieldRenderer.tsx +34 -19
  256. package/src/components/FormRenderer.tsx +38 -10
  257. package/src/components/GenerativeUIErrorBoundary.tsx +8 -3
  258. package/src/components/GraphRenderer.tsx +31 -16
  259. package/src/components/IframeFallbackLink.tsx +57 -0
  260. package/src/components/ImageGalleryRenderer.tsx +8 -4
  261. package/src/components/LightboxOverlay.tsx +6 -4
  262. package/src/components/MapRenderer.tsx +57 -20
  263. package/src/components/ModalRenderer.tsx +3 -1
  264. package/src/components/ScratchpadPanel.tsx +95 -44
  265. package/src/components/StreamingUIRenderer.tsx +41 -11
  266. package/src/components/UIResourceRenderer.tsx +176 -67
  267. package/src/components/VerifiedText.tsx +19 -9
  268. package/src/components/VideoRenderer.tsx +26 -4
  269. package/src/components/chart-data-table.ts +68 -14
  270. package/src/context/MCPActionContext.tsx +4 -1
  271. package/src/context/MCPUIConfigContext.tsx +151 -0
  272. package/src/context/MCPUIStringsContext.tsx +1014 -1
  273. package/src/hooks/index.ts +2 -1
  274. package/src/hooks/useStreamingUI.ts +64 -10
  275. package/src/index.ts +38 -0
  276. package/src/services/index.ts +1 -0
  277. package/src/services/validation.ts +131 -32
  278. package/src/solid-jsx-attrs.d.ts +24 -0
  279. package/src/utils/degraded-projections.ts +75 -12
  280. package/src/utils/format-string.ts +33 -0
  281. package/src/utils/iframe-coep.ts +74 -0
  282. package/src/validation.ts +9 -0
@@ -10,7 +10,7 @@ import { escapeHtml } from '../utils/escape-html'
10
10
  import { safeUrl } from '../utils/safe-url'
11
11
  import { SafeHtml } from './SafeHtml'
12
12
  import type { UIComponent, UILayout, RendererError, TableVirtualizeOptions } from '../types'
13
- import { validateComponent, DEFAULT_RESOURCE_LIMITS, getIframeSandbox } from '../services/validation'
13
+ import { validateComponent, DEFAULT_RESOURCE_LIMITS, getIframeSandbox, isTrustedIframeDomain } from '../services/validation'
14
14
  import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
15
15
  import { markRenderStart, markRenderEnd, PERF_PREFIX } from '../utils/perf'
16
16
  import { isDebugEnabled } from '../utils/logger'
@@ -22,7 +22,14 @@ import {
22
22
  type DuplicateMountInfo,
23
23
  } from '../utils/duplicate-mount-registry'
24
24
  import { useTelemetry } from '../context/MCPUITelemetryContext'
25
- import { useMCPUIStrings } from '../context/MCPUIStringsContext'
25
+ import {
26
+ DEFAULT_MCPUI_STRINGS,
27
+ formatMCPUIString,
28
+ useMCPUIStrings,
29
+ } from '../context/MCPUIStringsContext'
30
+ import { useMCPUIConfig } from '../context/MCPUIConfigContext'
31
+ import { shouldSetCredentialless } from '../utils/iframe-coep'
32
+ import { IframeFallbackLink } from './IframeFallbackLink'
26
33
 
27
34
  /**
28
35
  * How `<UIResourceRenderer>` reacts when `validateComponent()` rejects a
@@ -65,6 +72,7 @@ import { marked } from 'marked'
65
72
  * Copy button component with visual feedback
66
73
  */
67
74
  function CopyButton(props: { getText: () => string; title?: string; position?: 'top-right' | 'bottom-right' }) {
75
+ const strings = useMCPUIStrings()
68
76
  const [copied, setCopied] = createSignal(false)
69
77
 
70
78
  const handleCopy = async () => {
@@ -87,8 +95,8 @@ function CopyButton(props: { getText: () => string; title?: string; position?: '
87
95
  <button
88
96
  onClick={handleCopy}
89
97
  class={`${positionClasses()} opacity-60 hover:opacity-100 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm z-10`}
90
- title={props.title || 'Copy'}
91
- aria-label={props.title || 'Copy'}
98
+ title={props.title || strings.copy}
99
+ aria-label={props.title || strings.copy}
92
100
  data-mcp-ui-action="copy"
93
101
  type="button"
94
102
  >
@@ -210,6 +218,7 @@ function ChartRenderer(props: {
210
218
  /** Host opt-in for the external quickchart.io fallback (audit P1.7). */
211
219
  allowQuickchartFallback?: boolean
212
220
  }) {
221
+ const strings = useMCPUIStrings()
213
222
  const [useNative, setUseNative] = createSignal(false)
214
223
  const [iframeUrl, setIframeUrl] = createSignal<string>()
215
224
  const [isLoading, setIsLoading] = createSignal(true)
@@ -223,6 +232,11 @@ function ChartRenderer(props: {
223
232
  const params = () => props.component.params as any
224
233
  const rendererPref = () => params()?.renderer || 'auto'
225
234
  const allowQuickchart = () => props.allowQuickchartFallback === true
235
+ /** `alt` / `aria-label` of the quickchart image — localized chrome. */
236
+ const chartImageAlt = () =>
237
+ params()?.title
238
+ ? formatMCPUIString(strings.chartWithTitleAlt, { title: String(params().title) })
239
+ : strings.chartVisualizationAlt
226
240
 
227
241
  // Emit a clear, observable signal whenever we decline the external fallback.
228
242
  const signalBlockedFallback = (reason: string) => {
@@ -242,7 +256,7 @@ function ChartRenderer(props: {
242
256
  if (!params()?.data?.datasets) {
243
257
  return (
244
258
  <div class="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
245
- <p class="text-red-500 dark:text-red-400 text-sm">Invalid chart data: missing data.datasets</p>
259
+ <p class="text-red-500 dark:text-red-400 text-sm">{strings.chartInvalidData}</p>
246
260
  </div>
247
261
  )
248
262
  }
@@ -270,7 +284,7 @@ function ChartRenderer(props: {
270
284
  setUseNative(true)
271
285
  setIsLoading(false)
272
286
  } else {
273
- setError('Chart.js is not available. Install chart.js peer dependency.')
287
+ setError(strings.chartJsUnavailable)
274
288
  setIsLoading(false)
275
289
  }
276
290
  } else {
@@ -327,9 +341,11 @@ function ChartRenderer(props: {
327
341
  <Show when={degraded()}>
328
342
  <div class="p-3">
329
343
  <DegradedFallback
330
- message="Interactive chart unavailable — install the chart.js peer dependency, or set allowQuickchartFallback to use the external quickchart.io renderer."
331
- caption="Showing the chart data as a table."
332
- {...chartToDegradedTable(params() ?? {})}
344
+ message={strings.chartIframeUnavailable}
345
+ caption={strings.chartQuickchartCaption}
346
+ {...chartToDegradedTable(params() ?? {}, {
347
+ series: strings.degradedSeries,
348
+ })}
333
349
  />
334
350
  </div>
335
351
  </Show>
@@ -343,7 +359,7 @@ function ChartRenderer(props: {
343
359
  <Show when={error()}>
344
360
  <div class="absolute inset-0 flex items-center justify-center p-4">
345
361
  <div class="text-center">
346
- <p class="text-red-600 dark:text-red-400 text-sm font-medium">Chart Error</p>
362
+ <p class="text-red-600 dark:text-red-400 text-sm font-medium">{strings.chartError}</p>
347
363
  <p class="text-gray-600 dark:text-gray-400 text-xs mt-1">{error()}</p>
348
364
  </div>
349
365
  </div>
@@ -356,13 +372,13 @@ function ChartRenderer(props: {
356
372
  {params()?.title}
357
373
  </h3>
358
374
  </Show>
359
- <div class="w-full h-full" role="img" aria-label={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}>
375
+ <div class="w-full h-full" role="img" aria-label={chartImageAlt()}>
360
376
  <img
361
377
  src={iframeUrl()}
362
- alt={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}
378
+ alt={chartImageAlt()}
363
379
  class="w-full h-auto max-h-[300px] object-contain"
364
380
  onError={() => {
365
- setError('Failed to load chart')
381
+ setError(strings.chartLoadFailed)
366
382
  props.onError?.({
367
383
  type: 'render',
368
384
  message: 'Chart rendering failed',
@@ -491,6 +507,27 @@ export interface CitationCtx {
491
507
  id: number,
492
508
  mapping: { page: number | string; file?: string; file_id?: number | string } | undefined
493
509
  ) => string
510
+ /**
511
+ * Template for the visible placeholder kept when `map` is EMPTY and a
512
+ * marker therefore resolves to nothing — `{id}` is the marker number.
513
+ *
514
+ * Optional: `transformCellCitations` falls back to
515
+ * `DEFAULT_MCPUI_STRINGS.citationUnresolved` (`'[ref. {id}]'`). The
516
+ * renderers populate it from `strings.citationUnresolved`, so a host with a
517
+ * `<MCPUIStringsProvider>` gets its own wording; a direct
518
+ * `renderCellValue(value, ctx)` call may pass it explicitly.
519
+ *
520
+ * @since v6.20.0 — the hardcoded placeholder used to be French.
521
+ */
522
+ unresolvedLabel?: string
523
+ /**
524
+ * Tooltip template of the default citation chip button — `{label}` is the
525
+ * file name and page. Falls back to `DEFAULT_MCPUI_STRINGS.citationViewSource`;
526
+ * the renderers populate it from `strings.citationViewSource`.
527
+ *
528
+ * @since v6.20.0
529
+ */
530
+ viewSourceLabel?: string
494
531
  }
495
532
 
496
533
  /**
@@ -502,7 +539,8 @@ export interface CitationCtx {
502
539
  function defaultCitationChip(
503
540
  pageNum: number | string,
504
541
  fileName: string,
505
- verified = true
542
+ verified = true,
543
+ viewSourceLabel: string = DEFAULT_MCPUI_STRINGS.citationViewSource
506
544
  ): string {
507
545
  const safeDocName = encodeURIComponent(fileName || '')
508
546
  const label = fileName ? `${fileName} - ${pageNum}` : `${pageNum}`
@@ -516,7 +554,7 @@ function defaultCitationChip(
516
554
  ` data-citation-page="${pageNum}"`,
517
555
  ` data-citation-doc="${safeDocName}"`,
518
556
  ' data-citation-verified="true"',
519
- ` title="View source - ${label}">`,
557
+ ` title="${escapeHtml(formatMCPUIString(viewSourceLabel, { label }))}">`,
520
558
  '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
521
559
  '</button>',
522
560
  '</span>',
@@ -543,12 +581,16 @@ function transformCellCitations(text: string, ctx: CitationCtx): string {
543
581
  const id = parseInt(idStr, 10)
544
582
  const mapping = ctx.map[id] ?? ctx.map[String(id)]
545
583
  if (ctx.render) return ctx.render(id, mapping)
546
- if (mapping) return defaultCitationChip(mapping.page, mapping.file ?? '', true)
584
+ if (mapping) return defaultCitationChip(mapping.page, mapping.file ?? '', true, ctx.viewSourceLabel)
547
585
  // Unresolved id: when the map is non-empty (consumer claims to know
548
586
  // the citations), drop silently — it's likely an LLM hallucination.
549
587
  // When the map is empty (consumer didn't supply one), preserve a
550
588
  // human-visible placeholder so the marker isn't lost.
551
- return Object.keys(ctx.map).length > 0 ? '' : `[réf. ${id}]`
589
+ return Object.keys(ctx.map).length > 0
590
+ ? ''
591
+ : formatMCPUIString(ctx.unresolvedLabel ?? DEFAULT_MCPUI_STRINGS.citationUnresolved, {
592
+ id,
593
+ })
552
594
  }
553
595
  )
554
596
  }
@@ -670,7 +712,17 @@ function TableRenderer(props: {
670
712
  // every `renderCellValue` call below. Absent → undefined → cells render
671
713
  // as before (regression-safe).
672
714
  const citationCtx: CitationCtx | undefined = tableParams.citationMap
673
- ? { map: tableParams.citationMap, render: tableParams.citationRender }
715
+ ? {
716
+ map: tableParams.citationMap,
717
+ render: tableParams.citationRender,
718
+ // Getters, not snapshots: cells read the provider's current wording.
719
+ get unresolvedLabel() {
720
+ return strings.citationUnresolved
721
+ },
722
+ get viewSourceLabel() {
723
+ return strings.citationViewSource
724
+ },
725
+ }
674
726
  : undefined
675
727
 
676
728
  // ─── Client-side sorting (v4.0.5) ────────────────────────
@@ -706,7 +758,7 @@ function TableRenderer(props: {
706
758
  if (isNum) {
707
759
  cmp = (Number(va) || 0) - (Number(vb) || 0)
708
760
  } else {
709
- cmp = String(va).localeCompare(String(vb), 'fr', { sensitivity: 'base' })
761
+ cmp = String(va).localeCompare(String(vb), strings.locale, { sensitivity: 'base' })
710
762
  }
711
763
  return dir === 'desc' ? -cmp : cmp
712
764
  })
@@ -729,7 +781,7 @@ function TableRenderer(props: {
729
781
  // context where users scan many tables across messages. Backward-compat
730
782
  // for anyone who explicitly disabled.
731
783
  const isSearchable = () => tableParams.searchable !== false
732
- const searchPlaceholder = () => tableParams.searchPlaceholder || 'Rechercher dans le tableau...'
784
+ const searchPlaceholder = () => tableParams.searchPlaceholder || strings.tableSearchPlaceholder
733
785
 
734
786
  const handleSearch = (value: string) => {
735
787
  setSearchQuery(value)
@@ -800,7 +852,7 @@ function TableRenderer(props: {
800
852
  for (const n of [10, 30, 60, 100]) {
801
853
  if (n < total) opts.push({ value: n, label: String(n) })
802
854
  }
803
- opts.push({ value: 0, label: 'All' })
855
+ opts.push({ value: 0, label: strings.paginationAllRows })
804
856
  return opts
805
857
  }
806
858
 
@@ -1006,18 +1058,18 @@ function TableRenderer(props: {
1006
1058
  }
1007
1059
 
1008
1060
  return (
1009
- <ExpandableWrapper title={tableParams.title || 'Table'} copyData={getTableCSV()} copyLabel="Copy table (CSV)" toolbarVariant={props.toolbarVariant} onExpandedChange={setSelfExpanded}>
1061
+ <ExpandableWrapper title={tableParams.title || strings.tableTitle} copyData={getTableCSV()} copyLabel={strings.tableCopyCsv} toolbarVariant={props.toolbarVariant} onExpandedChange={setSelfExpanded}>
1010
1062
  <div class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden group ${
1011
1063
  isExpanded() ? 'flex-1 min-h-0 flex flex-col' : 'h-full'
1012
1064
  } ${tableParams.className || ''}`}>
1013
- <Show when={exportable} fallback={<CopyButton getText={getTableCSV} title="Copy table (CSV)" position="top-right" />}>
1065
+ <Show when={exportable} fallback={<CopyButton getText={getTableCSV} title={strings.tableCopyCsv} position="top-right" />}>
1014
1066
  <div class="absolute right-10 top-2 z-10">
1015
1067
  <button
1016
1068
  ref={exportTriggerRef}
1017
1069
  onClick={() => setShowExportMenu(!showExportMenu())}
1018
1070
  class="opacity-60 hover:opacity-100 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm"
1019
- title="Export table"
1020
- aria-label="Export table"
1071
+ title={strings.tableExport}
1072
+ aria-label={strings.tableExport}
1021
1073
  aria-haspopup="menu"
1022
1074
  aria-expanded={showExportMenu()}
1023
1075
  >
@@ -1032,13 +1084,13 @@ function TableRenderer(props: {
1032
1084
  width={144}
1033
1085
  >
1034
1086
  <Show when={(exportFormats as string[]).includes('tsv')}>
1035
- <button onClick={() => handleExport('tsv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Copy TSV</button>
1087
+ <button onClick={() => handleExport('tsv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">{strings.tableCopyTsv}</button>
1036
1088
  </Show>
1037
1089
  <Show when={(exportFormats as string[]).includes('csv')}>
1038
- <button onClick={() => handleExport('csv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Download CSV</button>
1090
+ <button onClick={() => handleExport('csv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">{strings.tableDownloadCsv}</button>
1039
1091
  </Show>
1040
1092
  <Show when={(exportFormats as string[]).includes('json')}>
1041
- <button onClick={() => handleExport('json')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Download JSON</button>
1093
+ <button onClick={() => handleExport('json')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">{strings.tableDownloadJson}</button>
1042
1094
  </Show>
1043
1095
  </PortalDropdownMenu>
1044
1096
  </div>
@@ -1048,7 +1100,9 @@ function TableRenderer(props: {
1048
1100
  <h3 id={`${tableId}-title`} class="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex-shrink-0">
1049
1101
  {tableParams.title}
1050
1102
  <Show when={isVirtualizing()}>
1051
- <span class="ml-2 text-xs font-normal text-gray-400">(virtualized: {tableParams.rows?.length} rows)</span>
1103
+ <span class="ml-2 text-xs font-normal text-gray-400">
1104
+ {formatMCPUIString(strings.tableVirtualizedRows, { count: tableParams.rows?.length ?? 0 })}
1105
+ </span>
1052
1106
  </Show>
1053
1107
  </h3>
1054
1108
  </Show>
@@ -1069,13 +1123,16 @@ function TableRenderer(props: {
1069
1123
  type="button"
1070
1124
  onClick={() => { handleSearch(''); setSearchQuery(''); setDebouncedQuery('') }}
1071
1125
  class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-sm"
1072
- aria-label="Clear search"
1126
+ aria-label={strings.tableClearSearch}
1073
1127
  >&times;</button>
1074
1128
  </Show>
1075
1129
  </div>
1076
1130
  <Show when={debouncedQuery() && filteredRows().length !== sortedRows().length}>
1077
1131
  <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
1078
- {filteredRows().length} result{filteredRows().length !== 1 ? 's' : ''} on {sortedRows().length}
1132
+ {formatMCPUIString(
1133
+ filteredRows().length === 1 ? strings.tableSearchResultsOne : strings.tableSearchResultsMany,
1134
+ { count: filteredRows().length, total: sortedRows().length }
1135
+ )}
1079
1136
  </p>
1080
1137
  </Show>
1081
1138
  </Show>
@@ -1106,7 +1163,7 @@ function TableRenderer(props: {
1106
1163
  })()
1107
1164
  }
1108
1165
  role="region"
1109
- aria-label={tableParams.title || 'Data table'}
1166
+ aria-label={tableParams.title || strings.tableAriaLabel}
1110
1167
  tabindex="0"
1111
1168
  >
1112
1169
  <table
@@ -1122,7 +1179,7 @@ function TableRenderer(props: {
1122
1179
  class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-gray-700 first:pl-6 last:pr-6 bg-gray-100 dark:bg-gray-900 cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
1123
1180
  style={column.width ? { width: column.width } : {}}
1124
1181
  on:click={() => handleSort(column.key)}
1125
- title={`Sort by ${column.label}`}
1182
+ title={formatMCPUIString(strings.sortBy, { column: column.label })}
1126
1183
  >
1127
1184
  <span class="inline-flex items-center gap-1">
1128
1185
  {column.label}
@@ -1151,12 +1208,14 @@ function TableRenderer(props: {
1151
1208
  <Show when={tableParams.pagination}>
1152
1209
  <div class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
1153
1210
  <span>
1154
- Showing {tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1} -{' '}
1155
- {Math.min(
1156
- (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,
1157
- tableParams.pagination.totalRows
1158
- )}{' '}
1159
- of {tableParams.pagination.totalRows}
1211
+ {formatMCPUIString(strings.tableServerPageRange, {
1212
+ start: tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1,
1213
+ end: Math.min(
1214
+ (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,
1215
+ tableParams.pagination.totalRows
1216
+ ),
1217
+ total: tableParams.pagination.totalRows,
1218
+ })}
1160
1219
  </span>
1161
1220
  </div>
1162
1221
  </Show>
@@ -1165,7 +1224,7 @@ function TableRenderer(props: {
1165
1224
  <Show when={needsClientPagination()}>
1166
1225
  <div class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
1167
1226
  <span>
1168
- {clientRangeStart()}&ndash;{clientRangeEnd()} / {filteredRows().length.toLocaleString('fr-FR')}
1227
+ {clientRangeStart()}&ndash;{clientRangeEnd()} / {filteredRows().length.toLocaleString(strings.locale)}
1169
1228
  </span>
1170
1229
  <div class="flex items-center gap-2">
1171
1230
  <button
@@ -1202,7 +1261,7 @@ function TableRenderer(props: {
1202
1261
  {(opt) => <option value={opt.value}>{opt.label}</option>}
1203
1262
  </For>
1204
1263
  </select>
1205
- <span class="text-gray-400">/ page</span>
1264
+ <span class="text-gray-400">{strings.perPageSuffix}</span>
1206
1265
  </Show>
1207
1266
  </div>
1208
1267
  </div>
@@ -1217,6 +1276,7 @@ function TableRenderer(props: {
1217
1276
  * Render a metric card component
1218
1277
  */
1219
1278
  function MetricRenderer(props: { component: UIComponent }) {
1279
+ const strings = useMCPUIStrings()
1220
1280
  const metricParams = props.component.params as any
1221
1281
 
1222
1282
  // Generate copyable text for metric
@@ -1229,7 +1289,7 @@ function MetricRenderer(props: { component: UIComponent }) {
1229
1289
 
1230
1290
  return (
1231
1291
  <div class="relative w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 group">
1232
- <CopyButton getText={getMetricText} title="Copy metric" position="top-right" />
1292
+ <CopyButton getText={getMetricText} title={strings.copyMetric} position="top-right" />
1233
1293
  <div class="flex flex-col h-full justify-between">
1234
1294
  <div>
1235
1295
  <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
@@ -1277,14 +1337,17 @@ function MetricRenderer(props: { component: UIComponent }) {
1277
1337
  * Extract image data from markdown image link format
1278
1338
  * Pattern: [![alt](image-url)](link-url)\n*Photo by Author*
1279
1339
  */
1280
- function extractImageFromMarkdown(content: string): { alt: string; imageUrl: string; linkUrl: string; credit: string } | null {
1340
+ function extractImageFromMarkdown(
1341
+ content: string,
1342
+ fallbackAlt: string
1343
+ ): { alt: string; imageUrl: string; linkUrl: string; credit: string } | null {
1281
1344
  // Pattern: [![alt text](image-url)](link-url) followed by optional credit line
1282
1345
  const imagePattern = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)\s*\*([^*]+)\*/
1283
1346
  const match = content.match(imagePattern)
1284
1347
 
1285
1348
  if (match) {
1286
1349
  return {
1287
- alt: match[1] || 'Image',
1350
+ alt: match[1] || fallbackAlt,
1288
1351
  imageUrl: match[2],
1289
1352
  linkUrl: match[3],
1290
1353
  credit: match[4].trim()
@@ -1298,6 +1361,7 @@ function extractImageFromMarkdown(content: string): { alt: string; imageUrl: str
1298
1361
  * Render a text component (with optional markdown)
1299
1362
  */
1300
1363
  function TextRenderer(props: { component: UIComponent }) {
1364
+ const strings = useMCPUIStrings()
1301
1365
  const textParams = props.component.params as any
1302
1366
 
1303
1367
  // Check if this is an image markdown that should be rendered as image
@@ -1308,7 +1372,7 @@ function TextRenderer(props: { component: UIComponent }) {
1308
1372
  // markdown path; an unusable link URL only drops the anchor.
1309
1373
  const imageData = createMemo(() => {
1310
1374
  if (textParams.markdown && textParams.content) {
1311
- const extracted = extractImageFromMarkdown(textParams.content)
1375
+ const extracted = extractImageFromMarkdown(textParams.content, strings.imageAlt)
1312
1376
  if (!extracted) return null
1313
1377
  const imageUrl = safeUrl(extracted.imageUrl, { allowDataImage: true })
1314
1378
  if (!imageUrl) return null
@@ -1351,7 +1415,7 @@ function TextRenderer(props: { component: UIComponent }) {
1351
1415
  when={imageData()}
1352
1416
  fallback={
1353
1417
  <div class="relative w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 group">
1354
- <CopyButton getText={getTextContent} title="Copy text" position="top-right" />
1418
+ <CopyButton getText={getTextContent} title={strings.copyText} position="top-right" />
1355
1419
  <SafeHtml
1356
1420
  class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}
1357
1421
  html={htmlContent}
@@ -1396,7 +1460,13 @@ function TextRenderer(props: { component: UIComponent }) {
1396
1460
  * Render an iframe component
1397
1461
  */
1398
1462
  function IframeRenderer(props: { component: UIComponent }) {
1463
+ const strings = useMCPUIStrings()
1464
+ const config = useMCPUIConfig()
1399
1465
  const params = props.component.params as any
1466
+ // v6.21.0 — the host's custom trusted hosts now reach BOTH the sandbox (they
1467
+ // used not to: `getIframeSandbox(params.url)` was called with no options) and
1468
+ // the `credentialless` decision.
1469
+ const trustOptions = () => ({ customTrustedDomains: config.customTrustedIframeDomains })
1400
1470
  return (
1401
1471
  <div class="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col">
1402
1472
  <Show when={params.title}>
@@ -1406,12 +1476,25 @@ function IframeRenderer(props: { component: UIComponent }) {
1406
1476
  </Show>
1407
1477
  <iframe
1408
1478
  src={params.url}
1409
- title={params.title || 'Embedded content'}
1479
+ title={params.title || strings.iframeTitle}
1410
1480
  class="w-full border-0 flex-1"
1411
1481
  style={`height: ${params.height || '400px'}; min-height: 300px;`}
1412
- sandbox={getIframeSandbox(params.url)}
1482
+ sandbox={getIframeSandbox(params.url, trustOptions())}
1483
+ // `attr:` is required: `credentialless` is in dom-expressions'
1484
+ // `Properties` set, so a plain `credentialless={…}` compiles to a JS
1485
+ // property assignment and never reaches the DOM as an attribute.
1486
+ // `true | undefined` keeps it a BOOLEAN attribute — bare in SSR
1487
+ // markup, removed (not `="false"`) when it does not apply.
1488
+ attr:credentialless={
1489
+ shouldSetCredentialless(params.url, config, {
1490
+ // This iframe's sandbox never carries `allow-same-origin` for an
1491
+ // untrusted host, so it already runs without cookies.
1492
+ sandboxedWithoutSameOrigin: !isTrustedIframeDomain(params.url, trustOptions()),
1493
+ }) || undefined
1494
+ }
1413
1495
  loading="lazy"
1414
1496
  />
1497
+ <IframeFallbackLink url={params.url} class="border-t border-gray-200 dark:border-gray-700" />
1415
1498
  </div>
1416
1499
  )
1417
1500
  }
@@ -1420,6 +1503,7 @@ function IframeRenderer(props: { component: UIComponent }) {
1420
1503
  * Render an image component
1421
1504
  */
1422
1505
  function ImageRenderer(props: { component: UIComponent }) {
1506
+ const strings = useMCPUIStrings()
1423
1507
  const params = props.component.params as any
1424
1508
 
1425
1509
  return (
@@ -1430,11 +1514,13 @@ function ImageRenderer(props: { component: UIComponent }) {
1430
1514
  target="_blank"
1431
1515
  rel="noopener noreferrer"
1432
1516
  class="cursor-zoom-in focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded"
1433
- aria-label={`View full size: ${params.alt || 'image'}`}
1517
+ aria-label={formatMCPUIString(strings.imageViewFullSize, {
1518
+ alt: params.alt || strings.imageAltFallback,
1519
+ })}
1434
1520
  >
1435
1521
  <img
1436
1522
  src={safeUrl(params.url, { allowDataImage: true })}
1437
- alt={params.alt || 'Image'}
1523
+ alt={params.alt || strings.imageAlt}
1438
1524
  class="max-w-full max-h-[500px] object-contain rounded shadow-sm hover:opacity-95 transition-opacity"
1439
1525
  loading="lazy"
1440
1526
  />
@@ -1453,6 +1539,7 @@ function ImageRenderer(props: { component: UIComponent }) {
1453
1539
  * Render a link component
1454
1540
  */
1455
1541
  function LinkRenderer(props: { component: UIComponent }) {
1542
+ const strings = useMCPUIStrings()
1456
1543
  const params = props.component.params as any
1457
1544
 
1458
1545
  return (
@@ -1460,7 +1547,10 @@ function LinkRenderer(props: { component: UIComponent }) {
1460
1547
  href={safeUrl(params.url)}
1461
1548
  target="_blank"
1462
1549
  rel="noopener noreferrer"
1463
- aria-label={`${params.label || 'Link'}: ${params.description || params.url} (opens in new tab)`}
1550
+ aria-label={formatMCPUIString(strings.linkOpensInNewTab, {
1551
+ label: params.label || strings.linkLabel,
1552
+ description: params.description || params.url,
1553
+ })}
1464
1554
  class={`flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group h-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${params.className || ''}`}
1465
1555
  onClick={(e) => e.stopPropagation()}
1466
1556
  >
@@ -1525,6 +1615,8 @@ function ComponentRenderer(props: {
1525
1615
  // mounted above; null-checked at every dispatch site so apps that don't
1526
1616
  // opt in see zero behavior change.
1527
1617
  const telemetry = useTelemetry()
1618
+ const strings = useMCPUIStrings()
1619
+ const componentConfig = useMCPUIConfig()
1528
1620
 
1529
1621
  onMount(() => {
1530
1622
  markRenderEnd(props.component.id)
@@ -1566,8 +1658,13 @@ function ComponentRenderer(props: {
1566
1658
  }
1567
1659
  })
1568
1660
 
1569
- // Validate component before rendering
1570
- const validation = validateComponent(props.component)
1661
+ // Validate component before rendering. The host's allow-list extension
1662
+ // (v6.21.0) has to reach validation too: an unlisted host is replaced by the
1663
+ // validation card before any renderer runs.
1664
+ const validation = validateComponent(props.component, {
1665
+ iframePolicy: componentConfig.iframePolicy,
1666
+ customIframeDomains: componentConfig.customIframeDomains,
1667
+ })
1571
1668
  if (!validation.valid) {
1572
1669
  props.onError?.({
1573
1670
  type: 'validation',
@@ -1590,7 +1687,7 @@ function ComponentRenderer(props: {
1590
1687
  }
1591
1688
 
1592
1689
  const mode: ValidationErrorMode = props.errorMode ?? 'block'
1593
- const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
1690
+ const firstError = validation.errors?.[0]?.message || strings.validationUnknownError
1594
1691
 
1595
1692
  // P1.6 — an UNKNOWN component type must never produce a silent blank,
1596
1693
  // whatever the errorMode. The renderer has no branch for it, so even
@@ -1609,7 +1706,7 @@ function ComponentRenderer(props: {
1609
1706
  <div
1610
1707
  class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200"
1611
1708
  role="alert"
1612
- aria-label="Component validation warning"
1709
+ aria-label={strings.validationWarning}
1613
1710
  title={firstError}
1614
1711
  >
1615
1712
  <svg
@@ -1627,7 +1724,7 @@ function ComponentRenderer(props: {
1627
1724
  <line x1="12" y1="9" x2="12" y2="13" />
1628
1725
  <line x1="12" y1="17" x2="12.01" y2="17" />
1629
1726
  </svg>
1630
- <span>Invalid {props.component.type}</span>
1727
+ <span>{formatMCPUIString(strings.invalidComponent, { type: props.component.type })}</span>
1631
1728
  </div>
1632
1729
  )
1633
1730
  }
@@ -1635,7 +1732,7 @@ function ComponentRenderer(props: {
1635
1732
  // mode === 'block' (default, pre-v5.4.0 behavior)
1636
1733
  return (
1637
1734
  <div class="w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
1638
- <p class="text-sm font-medium text-red-900 dark:text-red-100">Validation Error</p>
1735
+ <p class="text-sm font-medium text-red-900 dark:text-red-100">{strings.validationError}</p>
1639
1736
  <p class="text-xs text-red-700 dark:text-red-300 mt-1">
1640
1737
  {firstError}
1641
1738
  </p>
@@ -1737,12 +1834,13 @@ function ComponentRenderer(props: {
1737
1834
  * 'UNKNOWN_COMPONENT_TYPE'`), so this component stays purely presentational.
1738
1835
  */
1739
1836
  function UnsupportedComponentFallback(props: { component: UIComponent }) {
1837
+ const strings = useMCPUIStrings()
1740
1838
  return (
1741
1839
  <div
1742
1840
  role="alert"
1743
1841
  class="w-full rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-200"
1744
1842
  >
1745
- Unsupported component type: <code class="font-mono">{props.component.type}</code>
1843
+ {strings.unsupportedComponentType} <code class="font-mono">{props.component.type}</code>
1746
1844
  </div>
1747
1845
  )
1748
1846
  }
@@ -1852,13 +1950,21 @@ function ActionRenderer(props: { component: UIComponent }) {
1852
1950
  * Handles {error: true, message: "...", tool: "...", suggestions: [...]} format
1853
1951
  */
1854
1952
  function ErrorCardRenderer(props: { error: any }) {
1953
+ const strings = useMCPUIStrings()
1954
+ // The timestamp depends on the viewer's time zone, which the server cannot
1955
+ // know. Render it only after mount so SSR and hydration produce the same markup.
1956
+ const [mounted, setMounted] = createSignal(false)
1957
+ onMount(() => setMounted(true))
1855
1958
  const getErrorText = () => {
1856
- return `Error in ${props.error.tool || 'unknown tool'}: ${props.error.message || 'Unknown error'}`
1959
+ return formatMCPUIString(strings.errorCopyText, {
1960
+ tool: props.error.tool || strings.errorUnknownTool,
1961
+ message: props.error.message || strings.errorUnknown,
1962
+ })
1857
1963
  }
1858
1964
 
1859
1965
  return (
1860
1966
  <div class="relative w-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 group">
1861
- <CopyButton getText={getErrorText} title="Copy error details" position="top-right" />
1967
+ <CopyButton getText={getErrorText} title={strings.copyErrorDetails} position="top-right" />
1862
1968
  <div class="flex items-start gap-3">
1863
1969
  <div class="p-2 bg-red-100 dark:bg-red-900/40 rounded-full shrink-0">
1864
1970
  <svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -1867,19 +1973,21 @@ function ErrorCardRenderer(props: { error: any }) {
1867
1973
  </div>
1868
1974
  <div class="flex-1 min-w-0">
1869
1975
  <h4 class="text-sm font-semibold text-red-800 dark:text-red-200">
1870
- Tool Error: {props.error.tool || 'Unknown'}
1976
+ {formatMCPUIString(strings.toolErrorTitle, {
1977
+ tool: props.error.tool || strings.errorUnknownToolName,
1978
+ })}
1871
1979
  </h4>
1872
1980
  <p class="text-sm text-red-700 dark:text-red-300 mt-1">
1873
- {props.error.message || 'An error occurred during tool execution'}
1981
+ {props.error.message || strings.errorToolExecution}
1874
1982
  </p>
1875
1983
  <Show when={props.error.type}>
1876
1984
  <p class="text-xs text-red-600 dark:text-red-400 mt-1">
1877
- Type: {props.error.type}
1985
+ {formatMCPUIString(strings.errorTypeLabel, { type: props.error.type })}
1878
1986
  </p>
1879
1987
  </Show>
1880
1988
  <Show when={props.error.suggestions?.length}>
1881
1989
  <div class="mt-3">
1882
- <p class="text-xs font-medium text-red-700 dark:text-red-300">Suggestions:</p>
1990
+ <p class="text-xs font-medium text-red-700 dark:text-red-300">{strings.errorSuggestions}</p>
1883
1991
  <ul class="mt-1 text-xs text-red-600 dark:text-red-400 list-disc list-inside">
1884
1992
  <For each={props.error.suggestions}>
1885
1993
  {(suggestion: string) => <li>{suggestion}</li>}
@@ -1887,9 +1995,9 @@ function ErrorCardRenderer(props: { error: any }) {
1887
1995
  </ul>
1888
1996
  </div>
1889
1997
  </Show>
1890
- <Show when={props.error.timestamp}>
1998
+ <Show when={mounted() && props.error.timestamp}>
1891
1999
  <p class="text-xs text-red-500 dark:text-red-500 mt-2">
1892
- {new Date(props.error.timestamp).toLocaleString()}
2000
+ {new Date(props.error.timestamp).toLocaleString(strings.locale)}
1893
2001
  </p>
1894
2002
  </Show>
1895
2003
  </div>
@@ -1922,6 +2030,7 @@ function isUIResource(content: any): boolean {
1922
2030
  * Handles HTML resources returned by tools like ui_show_dashboard, ui_show_health
1923
2031
  */
1924
2032
  function UIResourceHtmlRenderer(props: { resource: any }) {
2033
+ const strings = useMCPUIStrings()
1925
2034
  const htmlContent = () => {
1926
2035
  if (props.resource.content?.htmlString) {
1927
2036
  return sanitizeHtml(String(props.resource.content.htmlString), SANITIZE_PROFILES.resource)
@@ -1930,7 +2039,7 @@ function UIResourceHtmlRenderer(props: { resource: any }) {
1930
2039
  }
1931
2040
 
1932
2041
  const resourceTitle = () => {
1933
- return props.resource.metadata?.title || props.resource.uri?.replace('ui://deposium/', '') || 'Resource'
2042
+ return props.resource.metadata?.title || props.resource.uri?.replace('ui://deposium/', '') || strings.resourceTitle
1934
2043
  }
1935
2044
 
1936
2045
  return (