Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions ui/desktop/src/components/McpApps/McpAppRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ interface McpAppRendererProps {
append?: (text: string) => void;
}

interface ResourceData {
html: string | null;
csp: CspMetadata | null;
prefersBorder: boolean;
}

export default function McpAppRenderer({
resourceUri,
extensionName,
Expand All @@ -42,8 +48,11 @@ export default function McpAppRenderer({
toolCancelled,
append,
}: McpAppRendererProps) {
const [resourceHtml, setResourceHtml] = useState<string | null>(null);
const [resourceCsp, setResourceCsp] = useState<CspMetadata | null>(null);
const [resource, setResource] = useState<ResourceData>({
html: null,
csp: null,
prefersBorder: true,
});
const [error, setError] = useState<string | null>(null);
const [iframeHeight, setIframeHeight] = useState(DEFAULT_IFRAME_HEIGHT);

Expand All @@ -60,11 +69,15 @@ export default function McpAppRenderer({

if (response.data) {
const content = response.data;

setResourceHtml(content.text);

const meta = content._meta as { ui?: { csp?: CspMetadata } } | undefined;
setResourceCsp(meta?.ui?.csp || null);
const meta = content._meta as
| { ui?: { csp?: CspMetadata; prefersBorder?: boolean } }
| undefined;

setResource({
html: content.text,
csp: meta?.ui?.csp || null,
prefersBorder: meta?.ui?.prefersBorder ?? true,
});
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load resource');
Expand Down Expand Up @@ -161,8 +174,8 @@ export default function McpAppRenderer({
}, []);

const { iframeRef, proxyUrl } = useSandboxBridge({
resourceHtml: resourceHtml || '',
resourceCsp,
resourceHtml: resource.html || '',
resourceCsp: resource.csp,
resourceUri,
toolInput,
toolInputPartial,
Expand All @@ -174,25 +187,20 @@ export default function McpAppRenderer({

if (error) {
return (
<div className="mt-3 p-4 border border-red-500 rounded-lg bg-red-50 dark:bg-red-900/20">
<div className="p-4 border border-red-500 rounded-lg bg-red-50 dark:bg-red-900/20">
<div className="text-red-700 dark:text-red-300">Failed to load MCP app: {error}</div>
</div>
);
}

if (!resourceHtml) {
return (
<div className="mt-3 p-4 border border-borderSubtle rounded-lg bg-bgApp">
<div className="flex items-center justify-center" style={{ minHeight: '200px' }}>
Loading MCP app...
</div>
</div>
);
}

return (
<div className={cn('mt-3 bg-bgApp', 'border border-borderSubtle rounded-lg overflow-hidden')}>
{proxyUrl ? (
<div
className={cn(
'bg-bgApp overflow-hidden',
resource.prefersBorder ? 'border border-borderSubtle rounded-lg' : 'my-6'
)}
>
{resource.html && proxyUrl ? (
Copy link
Collaborator Author

@aharvard aharvard Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DOsinga PTAL

I unified resource.html and proxyUrl as we don't need a discrete state for proxyUrl loading

Also, leaving this inline instead of extracting as a const. I noticed a hard-to-reproduce issue where the iframe ref might have gotten lost or something... the rendered mcp app would be sitting there w/ no messages being sent down. Maybe a closure issue? Not sure. But I think what I got works.

<iframe
ref={iframeRef}
src={proxyUrl}
Expand All @@ -205,16 +213,8 @@ export default function McpAppRenderer({
sandbox="allow-scripts allow-same-origin"
/>
) : (
<div
style={{
width: '100%',
minHeight: '200px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Loading...
<div className="flex items-center justify-center p-4" style={{ minHeight: '200px' }}>
Loading MCP app...
</div>
)}
</div>
Expand Down