Skip to content

Commit b7c994d

Browse files
committed
fix: validate URL
1 parent 42cf1cc commit b7c994d

File tree

7 files changed

+97
-12
lines changed

7 files changed

+97
-12
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The primary payload returned from the server to the client is the `UIResource`:
5252
interface UIResource {
5353
type: 'resource';
5454
resource: {
55-
uri: string; // ui://component/id
55+
uri: string; // e.g., ui://component/id
5656
mimeType: 'text/html' | 'text/uri-list' | 'application/vnd.mcp-ui.remote-dom'; // text/html for HTML content, text/uri-list for URL content, application/vnd.mcp-ui.remote-dom for remote-dom content (Javascript)
5757
text?: string; // Inline HTML, external URL, or remote-dom script
5858
blob?: string; // Base64-encoded HTML, URL, or remote-dom script
@@ -63,7 +63,7 @@ interface UIResource {
6363
* **`uri`**: Unique identifier for caching and routing
6464
* `ui://…` — UI resources (rendering method determined by mimeType)
6565
* **`mimeType`**: `text/html` for HTML content (iframe srcDoc), `text/uri-list` for URL content (iframe src), `application/vnd.mcp-ui.remote-dom` for remote-dom content (Javascript)
66-
* **MCP-UI requires a single URL**: While `text/uri-list` format supports multiple URLs, MCP-UI uses only the first valid URL and logs others
66+
* **MCP-UI requires a single URL**: While `text/uri-list` format supports multiple URLs, MCP-UI uses only the first valid `http/s` URL and warns if additional URLs are found
6767
* **`text` vs. `blob`**: Choose `text` for simple strings; use `blob` for larger or encoded content.
6868

6969
### Resource Renderer
@@ -96,7 +96,7 @@ Rendered using the `<HTMLResourceRenderer />` component, which displays content
9696

9797
* **`mimeType`**:
9898
* `text/html`: Renders inline HTML content.
99-
* `text/uri-list`: Renders an external URL. MCP-UI uses the first valid URL.
99+
* `text/uri-list`: Renders an external URL. MCP-UI uses the first valid `http/s` URL.
100100

101101
#### Remote DOM (`application/vnd.mcp-ui.remote-dom`)
102102

docs/src/guide/client/html-resource.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ The component accepts the following props:
3636
2. **Handles URI Schemes**:
3737
- For resources with `mimeType: 'text/uri-list'`:
3838
- Expects `resource.text` or `resource.blob` to contain a single URL in URI list format
39-
- **MCP-UI requires a single URL**: While the format supports multiple URLs, only the first valid URL is used
39+
- **MCP-UI requires a single URL**: While the format supports multiple URLs, only the first valid `http/s` URL is used
4040
- Multiple URLs are supported for fallback specification but will trigger warnings
4141
- Ignores comment lines starting with `#` and empty lines
4242
- If using `blob`, it decodes it from Base64.

docs/src/guide/protocol-details.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface UIResource {
4040

4141
## URI List Format Support
4242

43-
When using `mimeType: 'text/uri-list'`, the content follows the standard URI list format (RFC 2483). However, **MCP-UI requires a single URL** for rendering.
43+
When using `mimeType: 'text/uri-list'`, the content follows the standard URI list format (RFC 2483). However, **MCP-UI requires a single URL** for rendering. For security reasons, the protocol must be `http/s`.
4444

4545
- **Single URL Requirement**: MCP-UI will use only the first valid URL found
4646
- **Multiple URLs**: If multiple URLs are provided, the client will use the first valid URL and log a warning about the ignored alternatives

packages/client/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The primary payload returned from the server to the client is the `UIResource`:
5252
interface UIResource {
5353
type: 'resource';
5454
resource: {
55-
uri: string; // ui://component/id
55+
uri: string; // e.g., ui://component/id
5656
mimeType: 'text/html' | 'text/uri-list' | 'application/vnd.mcp-ui.remote-dom'; // text/html for HTML content, text/uri-list for URL content, application/vnd.mcp-ui.remote-dom for remote-dom content (Javascript)
5757
text?: string; // Inline HTML, external URL, or remote-dom script
5858
blob?: string; // Base64-encoded HTML, URL, or remote-dom script
@@ -63,7 +63,7 @@ interface UIResource {
6363
* **`uri`**: Unique identifier for caching and routing
6464
* `ui://…` — UI resources (rendering method determined by mimeType)
6565
* **`mimeType`**: `text/html` for HTML content (iframe srcDoc), `text/uri-list` for URL content (iframe src), `application/vnd.mcp-ui.remote-dom` for remote-dom content (Javascript)
66-
* **MCP-UI requires a single URL**: While `text/uri-list` format supports multiple URLs, MCP-UI uses only the first valid URL and logs others
66+
* **MCP-UI requires a single URL**: While `text/uri-list` format supports multiple URLs, MCP-UI uses only the first valid `http/s` URL and warns if additional URLs are found
6767
* **`text` vs. `blob`**: Choose `text` for simple strings; use `blob` for larger or encoded content.
6868

6969
### Resource Renderer
@@ -96,7 +96,7 @@ Rendered using the `<HTMLResourceRenderer />` component, which displays content
9696

9797
* **`mimeType`**:
9898
* `text/html`: Renders inline HTML content.
99-
* `text/uri-list`: Renders an external URL. MCP-UI uses the first valid URL.
99+
* `text/uri-list`: Renders an external URL. MCP-UI uses the first valid `http/s` URL.
100100

101101
#### Remote DOM (`application/vnd.mcp-ui.remote-dom`)
102102

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { processHTMLResource } from '../processResource';
3+
4+
describe('text/uri-list', () => {
5+
it('should process a valid https URL', () => {
6+
const resource = {
7+
mimeType: 'text/uri-list',
8+
text: 'https://example.com',
9+
};
10+
const result = processHTMLResource(resource);
11+
expect(result.error).toBeUndefined();
12+
expect(result.iframeSrc).toBe('https://example.com');
13+
expect(result.iframeRenderMode).toBe('src');
14+
});
15+
16+
it('should process a valid http URL', () => {
17+
const resource = {
18+
mimeType: 'text/uri-list',
19+
text: 'http://example.com',
20+
};
21+
const result = processHTMLResource(resource);
22+
expect(result.error).toBeUndefined();
23+
expect(result.iframeSrc).toBe('http://example.com');
24+
expect(result.iframeRenderMode).toBe('src');
25+
});
26+
27+
it('should return an error for an invalid URL (javascript:alert)', () => {
28+
const resource = {
29+
mimeType: 'text/uri-list',
30+
text: 'javascript:alert("pwned")',
31+
};
32+
const result = processHTMLResource(resource);
33+
expect(result.error).toBe('No valid URLs found in uri-list content.');
34+
expect(result.iframeSrc).toBeUndefined();
35+
});
36+
37+
it('should return an error for a blob URL', () => {
38+
const resource = {
39+
mimeType: 'text/uri-list',
40+
text: 'blob:https://example.com/some-uuid',
41+
};
42+
const result = processHTMLResource(resource);
43+
expect(result.error).toBe('No valid URLs found in uri-list content.');
44+
expect(result.iframeSrc).toBeUndefined();
45+
});
46+
47+
it('should extract the first valid URL from a list', () => {
48+
const resource = {
49+
mimeType: 'text/uri-list',
50+
text: '# comment\ninvalid-url\nhttps://first-valid.com\nhttps://second-valid.com',
51+
};
52+
const result = processHTMLResource(resource);
53+
expect(result.error).toBeUndefined();
54+
expect(result.iframeSrc).toBe('https://first-valid.com');
55+
});
56+
57+
it('should handle empty or commented-out content', () => {
58+
const resource = {
59+
mimeType: 'text/uri-list',
60+
text: '# just a comment\n# another comment',
61+
};
62+
const result = processHTMLResource(resource);
63+
expect(result.error).toBe('No valid URLs found in uri-list content.');
64+
});
65+
66+
it('should return error for content with no valid URLs', () => {
67+
const resource = {
68+
mimeType: 'text/uri-list',
69+
text: 'just-some-string\nanother-string',
70+
};
71+
const result = processHTMLResource(resource);
72+
expect(result.error).toBe('No valid URLs found in uri-list content.');
73+
});
74+
});

packages/client/src/utils/processResource.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ type ProcessResourceResult = {
77
htmlString?: string;
88
};
99

10+
function isValidHttpUrl(string: string): boolean {
11+
let url;
12+
try {
13+
url = new URL(string);
14+
} catch (e) {
15+
console.error('Error parsing URL:', e);
16+
return false;
17+
}
18+
return url.protocol === 'http:' || url.protocol === 'https:';
19+
}
20+
1021
export function processHTMLResource(resource: Partial<Resource>): ProcessResourceResult {
1122
if (resource.mimeType !== 'text/html' && resource.mimeType !== 'text/uri-list') {
1223
return {
@@ -51,7 +62,7 @@ export function processHTMLResource(resource: Partial<Resource>): ProcessResourc
5162
const lines = urlContent
5263
.split('\n')
5364
.map((line) => line.trim())
54-
.filter((line) => line && !line.startsWith('#'));
65+
.filter((line) => line && !line.startsWith('#') && isValidHttpUrl(line));
5566

5667
if (lines.length === 0) {
5768
return {

packages/server/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The primary payload returned from the server to the client is the `UIResource`:
5252
interface UIResource {
5353
type: 'resource';
5454
resource: {
55-
uri: string; // ui://component/id
55+
uri: string; // e.g., ui://component/id
5656
mimeType: 'text/html' | 'text/uri-list' | 'application/vnd.mcp-ui.remote-dom'; // text/html for HTML content, text/uri-list for URL content, application/vnd.mcp-ui.remote-dom for remote-dom content (Javascript)
5757
text?: string; // Inline HTML, external URL, or remote-dom script
5858
blob?: string; // Base64-encoded HTML, URL, or remote-dom script
@@ -63,7 +63,7 @@ interface UIResource {
6363
* **`uri`**: Unique identifier for caching and routing
6464
* `ui://…` — UI resources (rendering method determined by mimeType)
6565
* **`mimeType`**: `text/html` for HTML content (iframe srcDoc), `text/uri-list` for URL content (iframe src), `application/vnd.mcp-ui.remote-dom` for remote-dom content (Javascript)
66-
* **MCP-UI requires a single URL**: While `text/uri-list` format supports multiple URLs, MCP-UI uses only the first valid URL and logs others
66+
* **MCP-UI requires a single URL**: While `text/uri-list` format supports multiple URLs, MCP-UI uses only the first valid `http/s` URL and warns if additional URLs are found
6767
* **`text` vs. `blob`**: Choose `text` for simple strings; use `blob` for larger or encoded content.
6868

6969
### Resource Renderer
@@ -96,7 +96,7 @@ Rendered using the `<HTMLResourceRenderer />` component, which displays content
9696

9797
* **`mimeType`**:
9898
* `text/html`: Renders inline HTML content.
99-
* `text/uri-list`: Renders an external URL. MCP-UI uses the first valid URL.
99+
* `text/uri-list`: Renders an external URL. MCP-UI uses the first valid `http/s` URL.
100100

101101
#### Remote DOM (`application/vnd.mcp-ui.remote-dom`)
102102

0 commit comments

Comments
 (0)