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 (