Using a Proxy Script for External URLs
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 <UIResourceRenderer>
allows you to specify a URL for a proxy script that will render the external content in a nested iframe.
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=<encoded_original_url>
.
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.
Using the Hosted Proxy
For convenience, mcp-ui
provides a hosted proxy script at https://proxy.mcpui.dev
. You can use this URL directly as the proxy
prop value without any additional setup.
import { UIResourceRenderer } from '@mcp-ui/client';
<UIResourceRenderer
resource={mcpResource.resource}
htmlProps={{
proxy: 'https://proxy.mcpui.dev'
}}
onUIAction={handleUIAction}
/>
2
3
4
5
6
7
8
9
Please verify that the host whitelists https://proxy.mcpui.dev
as a frame-src
in the CSP.
You can find a complete example for a site with restrictive CSP that uses the hosted proxy at examples/external-url-demo
.
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.
IMPORTANT: For security reasons, you MUST NOT host the proxy script on the same origin as your main application. mcp-ui/client
will automatically log an error and fallback to direct iframe if the same origin is detected.
Proxy Script Requirements
A valid proxy script must:
- Accept a
url
query parameter: The script should retrieve the target URL from theurl
query parameter in its own URL. - Validate the URL: It must validate that the provided URL is a valid
http:
orhttps:
URL to prevent abuse. - Render in an Iframe: The script should dynamically create an iframe and set its
src
to the validated target URL. - Sandbox the Iframe: The iframe must be sandboxed to restrict its capabilities. A minimal sandbox policy would be
allow-scripts allow-same-origin
. - Forward
postMessage
Events: To allow communication between the host application and the embedded external URL, the proxy needs to forwardmessage
events betweenwindow.parent
and the iframe'scontentWindow
. For security, it's critical to use a specifictargetOrigin
instead of*
inpostMessage
calls whenever possible. ThetargetOrigin
for messages to the iframe should be the external URL's origin; Messages to the parent will resort to*
.
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
.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>MCP-UI Proxy</title>
<style>
html,
body {
margin: 0;
height: 100vh;
width: 100vw;
}
body {
display: flex;
flex-direction: column;
}
* {
box-sizing: border-box;
}
iframe {
background-color: transparent;
border: 0px none transparent;
padding: 0px;
overflow: hidden;
flex-grow: 1;
}
</style>
</head>
<body>
<script>
const target = new URLSearchParams(location.search).get('url');
// Validate that the URL is a valid HTTP or HTTPS URL
function isValidHttpUrl(string) {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (error) {
return false;
}
}
if (!target) {
document.body.textContent = 'Error: missing url parameter';
} else if (!isValidHttpUrl(target)) {
document.body.textContent = 'Error: invalid URL. Only HTTP and HTTPS URLs are allowed.';
} else {
const inner = document.createElement('iframe');
inner.src = target;
inner.style = 'width:100%; height:100%; border:none;';
inner.sandbox = 'allow-same-origin allow-scripts';
document.body.appendChild(inner);
const urlOrigin = new URL(target).origin;
window.addEventListener('message', (event) => {
if (event.source === window.parent) {
// listen for messages from the parent and send them to the iframe
inner.contentWindow.postMessage(event.data, urlOrigin);
} else if (event.source === inner.contentWindow) {
// listen for messages from the iframe and send them to the parent
window.parent.postMessage(event.data, '*');
}
});
}
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67