diff --git a/docs/src/guide/client/using-a-proxy.md b/docs/src/guide/client/using-a-proxy.md index 9b7332cb..5c800b06 100644 --- a/docs/src/guide/client/using-a-proxy.md +++ b/docs/src/guide/client/using-a-proxy.md @@ -1,11 +1,14 @@ -# Using a Proxy Script for External URLs +# Using a Proxy Script for External URLs and Raw HTML -When rendering external URLs (`text/uri-list`), you may need to use a "proxy" to comply with your host's restrictive Content Security Policy (CSP). The proxy domain must be whitelisted as a `frame-src`. The `proxy` prop on `` allows you to specify a URL for a proxy script that will render the external content in a nested iframe. +When rendering external URLs (`text/uri-list`) or raw HTML (`text/html`), you may need to use a "proxy" to comply with your host's restrictive Content Security Policy (CSP). The proxy domain must be whitelisted as a `frame-src`. The `proxy` prop on `` allows you to specify a URL for a proxy script that will render the content in a nested iframe on a different origin. -When `proxy` is set, the external URL is encoded and appended to the proxy URL. For example, if `proxy` is `https://my-proxy.com/`, the final URL will be `https://my-proxy.com/?url=`. +There are two proxy flows: + +- External URLs: the external URL is encoded and appended as `?url=` to the proxy URL. For example: `https://my-proxy.com/?url=`. +- Raw HTML: the proxy is loaded with `?contentType=rawhtml`, and the HTML and inner iframe sandbox are delivered via `postMessage` after the proxy iframe signals it's ready. ::: tip Important -The term "proxy" in this context does not refer to a real proxy server. It is a static, client-side script that nests the external URL content within an iframe. This process occurs locally in the user's browser. User data never reaches a remote server. +The term "proxy" in this context does not refer to a real proxy server. It is a static, client-side script that nests the UI resource's iframe within a "proxy" iframe. This process occurs locally in the user's browser. User data never reaches a remote server. ::: ## Using the Hosted Proxy @@ -28,6 +31,28 @@ Please verify that the host whitelists `https://proxy.mcpui.dev` as a `frame-src You can find a complete example for a site with restrictive CSP that uses the hosted proxy at `examples/external-url-demo`. +## Architecture + +```mermaid +sequenceDiagram + participant Host as Host Page + participant Proxy as Proxy iframe + participant Inner as Inner iframe (UI widget) + Host->>Proxy: Load proxy (with "?url" or "?contentType=rawhtml") + alt External URL + Proxy->>Inner: Create with src = decoded url + else rawHtml + Proxy-->>Host: ui-proxy-iframe-ready message + Host->>Proxy: ui-html-content message ({ html, sandbox }) + Proxy->>Inner: Create with sandbox + Proxy->>Inner: Set srcDoc to HTML + end + Inner-->>Proxy: Messages (e.g., UI actions) + Proxy-->>Host: Relay (Inner -> Host) + Host-->>Proxy: Message responses + Proxy-->>Inner: Relay (Host -> Inner) +``` + ## Self-Hosting the Proxy Script If you prefer to host your own proxy script, you can create a simple HTML file with embedded JavaScript. This is a useful alternative to the hosted version when you want more control or a custom domain. @@ -38,14 +63,17 @@ If you prefer to host your own proxy script, you can create a simple HTML file w A valid proxy script must: -1. **Accept a `url` query parameter**: The script should retrieve the target URL from the `url` query parameter in its own URL. -2. **Validate the URL**: It must validate that the provided URL is a valid `http:` or `https:` URL to prevent abuse. -3. **Render in an Iframe**: The script should dynamically create an iframe and set its `src` to the validated target URL. -4. **Sandbox the Iframe**: The iframe must be sandboxed to restrict its capabilities. A minimal sandbox policy would be `allow-scripts allow-same-origin`. -5. **Forward `postMessage` Events**: To allow communication between the host application and the embedded external URL, the proxy needs to forward `message` events between `window.parent` and the iframe's `contentWindow`. For security, it's critical to use a specific `targetOrigin` instead of `*` in `postMessage` calls whenever possible. The `targetOrigin` for messages to the iframe should be the external URL's origin; Messages to the parent will resort to `*`. +1. **External URLs (`url` query parameter)**: Retrieve `url` from the query string, validate it as `http:`/`https:`, and render it in a nested iframe. +2. **Raw HTML (`contentType=rawhtml`)**: When `contentType=rawhtml` is present, the proxy must: + - Create a nested iframe and emit a ready signal (`ui-proxy-iframe-ready`) to `window.parent`. + - Receive a single `postMessage` with `{ html: string, sandbox?: string }` (message type e.g. `ui-html-content`). + - Apply `sandbox` to the inner iframe, then set the inner iframe `srcdoc` to the provided HTML. +3. **Sandbox the Iframe**: The nested iframe must be sandboxed to restrict capabilities. For external URLs a minimal policy is `allow-scripts allow-same-origin`; for raw HTML a minimal policy is `allow-scripts` unless you explicitly need additional capabilities. +4. **Forward `postMessage` Events**: To allow communication between the host application and the embedded external URL, the proxy needs to forward `message` events between `window.parent` and the iframe's `contentWindow`. For security, it's critical to use a specific `targetOrigin` instead of `*` in `postMessage` calls whenever possible. The `targetOrigin` for messages to the iframe should be the external URL's origin; Messages to the parent will default to `*`. +5. **Permissive Proxy CSP**: Serve the proxy page with a permissive CSP that does not block nested iframe content (e.g., allowing scripts, styles, images) since the host CSP is intentionally not applied on the proxy origin. ### Example Self-Hosted Proxy -Here is an example of a self-hosted proxy script that meets these requirements. You can find this file in `sdks/typescript/client/scripts/proxy/index.html`. +Here is an example of a self-hosted proxy script that meets these requirements (supports both `url` for external URLs and `contentType=rawhtml` + `postMessage` for raw HTML). You can find this file in `sdks/typescript/client/scripts/proxy/index.html`. <<< @/../../sdks/typescript/client/scripts/proxy/index.html diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 457387d1..9ea58da5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -404,6 +404,9 @@ importers: '@testing-library/react': specifier: ^14.0.0 version: 14.3.1(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/jsdom': + specifier: ^27.0.0 + version: 27.0.0 '@types/react': specifier: ^18.2.0 version: 18.3.23 @@ -3003,6 +3006,9 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/jsdom@27.0.0': + resolution: {integrity: sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3077,6 +3083,9 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -10934,6 +10943,12 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/jsdom@27.0.0': + dependencies: + '@types/node': 20.19.1 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + '@types/json-schema@7.0.15': {} '@types/leaflet@1.9.19': @@ -11018,6 +11033,8 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': optional: true diff --git a/sdks/typescript/client/package.json b/sdks/typescript/client/package.json index b1bcf0e2..036cc651 100644 --- a/sdks/typescript/client/package.json +++ b/sdks/typescript/client/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", + "@types/jsdom": "^27.0.0", "@types/react": "^18.2.0", "@vitejs/plugin-react": "^4.0.0", "esbuild": "^0.25.5", diff --git a/sdks/typescript/client/scripts/proxy/index.html b/sdks/typescript/client/scripts/proxy/index.html index 5460628d..67263db8 100644 --- a/sdks/typescript/client/scripts/proxy/index.html +++ b/sdks/typescript/client/scripts/proxy/index.html @@ -2,6 +2,8 @@ + + MCP-UI Proxy