diff --git a/docs/src/guide/client/html-resource.md b/docs/src/guide/client/html-resource.md index edb20ca9..97997e93 100644 --- a/docs/src/guide/client/html-resource.md +++ b/docs/src/guide/client/html-resource.md @@ -14,6 +14,7 @@ export interface HTMLResourceRendererProps { proxy?: string; iframeRenderData?: Record; autoResizeIframe?: boolean | { width?: boolean; height?: boolean }; + useSrcDoc?: boolean; sandboxPermissions?: string; iframeProps?: Omit, 'src' | 'srcDoc' | 'ref' | 'style'>; } @@ -41,6 +42,7 @@ The component accepts the following props: - **`proxy`**: (Optional) A URL to a proxy script. This is useful for hosts with a strict Content Security Policy (CSP). When provided, external URLs will be rendered in a nested iframe hosted at this URL. For example, if `proxy` is `https://my-proxy.com/`, the final URL will be `https://my-proxy.com/?url=`. For your convenience, mcp-ui hosts a proxy script at `https://proxy.mcpui.dev`, which you can use as a the prop value without any setup (see `examples/external-url-demo`). - **`iframeProps`**: (Optional) Custom props for the iframe. - **`autoResizeIframe`**: (Optional) When enabled, the iframe will automatically resize based on messages from the iframe's content. This prop can be a boolean (to enable both width and height resizing) or an object (`{width?: boolean, height?: boolean}`) to control dimensions independently. +- **`useSrcDoc`**: (Optional) Defaults to `false`. When `false` (default), HTML content is rendered using a blob URL set as the iframe's `src` attribute. When `true`, HTML content is rendered using the `srcDoc` attribute with the raw HTML string. Setting this to `true` can be useful in environments with strict Content Security Policies that block blob URLs. - **`sandboxPermissions`**: (Optional) Additional iframe sandbox permissions to add to the defaults. These are merged with: - External URLs (`text/uri-list`): `'allow-scripts allow-same-origin'` - Raw HTML content (`text/html`): `'allow-scripts'` diff --git a/docs/src/guide/client/resource-renderer.md b/docs/src/guide/client/resource-renderer.md index 32324cf1..a8a34386 100644 --- a/docs/src/guide/client/resource-renderer.md +++ b/docs/src/guide/client/resource-renderer.md @@ -66,6 +66,7 @@ interface UIResourceRendererProps { - **`ref`**: Optional React ref to access the underlying iframe element - **`iframeRenderData`**: Optional `Record` to pass data to the iframe upon rendering. This enables advanced use cases where the parent application needs to provide initial state or configuration to the sandboxed iframe content. - **`autoResizeIframe`**: Optional `boolean | { width?: boolean; height?: boolean }` to automatically resize the iframe to the size of the content. + - **`useSrcDoc`**: Optional `boolean` (defaults to `false`). When `false` (default), HTML content is rendered using a blob URL set as the iframe's `src` attribute. When `true`, HTML content is rendered using the `srcDoc` attribute with the raw HTML string. Useful in environments with strict Content Security Policies that block blob URLs. - **`remoteDomProps`**: Optional props for the `` - **`library`**: Optional component library for Remote DOM resources (defaults to `basicComponentLibrary`) - **`remoteElements`**: Optional remote element definitions for Remote DOM resources. REQUIRED for Remote DOM snippets. @@ -173,6 +174,20 @@ The function provides type narrowing, so TypeScript will understand that `conten /> ``` +### Using srcDoc Instead of Blob URLs + +By default, HTML content is rendered using blob URLs for better security. If you need to use `srcDoc` instead (e.g., for environments with strict CSP policies that block blob URLs): + +```tsx + +``` + ### Passing Render-Time Data to Iframes The `iframeRenderData` prop allows you to send a data payload to an iframe as it renders. This is useful for initializing the iframe with dynamic data from the parent application. diff --git a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx index 1db9a721..b39f1367 100644 --- a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx @@ -10,6 +10,7 @@ export type HTMLResourceRendererProps = { proxy?: string; iframeRenderData?: Record; autoResizeIframe?: boolean | { width?: boolean; height?: boolean }; + useSrcDoc?: boolean; sandboxPermissions?: string; iframeProps?: Omit, 'src' | 'srcDoc' | 'style'> & { ref?: React.RefObject; @@ -37,11 +38,13 @@ export const HTMLResourceRenderer = ({ proxy, iframeRenderData, autoResizeIframe, + useSrcDoc, sandboxPermissions, iframeProps, }: HTMLResourceRendererProps) => { const iframeRef = useRef(null); useImperativeHandle(iframeProps?.ref, () => iframeRef.current as HTMLIFrameElement); + const iframeSrcBlobUrl = useRef(null); const { error, iframeSrc, iframeRenderMode, htmlString } = useMemo( () => processHTMLResource(resource, proxy), @@ -72,6 +75,10 @@ export const HTMLResourceRenderer = ({ }, ); } + if (iframeSrcBlobUrl.current && typeof window?.URL?.revokeObjectURL === 'function') { + window.URL.revokeObjectURL(iframeSrcBlobUrl.current); + iframeSrcBlobUrl.current = null; + } iframeProps?.onLoad?.(event); }, [iframeRenderData, iframeSrcToRender, iframeProps?.onLoad], @@ -159,9 +166,24 @@ export const HTMLResourceRenderer = ({ } return null; } + let iframeSrcProp: { + srcDoc?: string; + } | { + src?: string; + } = { + srcDoc: htmlString, + } + if (!useSrcDoc && typeof URL.createObjectURL === 'function') { + const blob = new Blob([htmlString], { type: 'text/html' }); + const blobUrl = URL.createObjectURL(blob); + iframeSrcBlobUrl.current = blobUrl; + iframeSrcProp = { + src: blobUrl, + }; + } return (