import { useRef, useEffect, useCallback, useState } from 'react' import { IconAppWindow, IconLoader } from '@tabler/icons-react' interface MCPAppRendererProps { resourceUri: string data: Record csp?: string onToolCall?: (toolName: string, args: Record) => void onSendMessage?: (content: string) => void } export function MCPAppRenderer({ resourceUri, data, onToolCall, onSendMessage, }: MCPAppRendererProps) { const iframeRef = useRef(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [iframeHeight, setIframeHeight] = useState(300) const appName = resourceUri.replace(/^app:\/\//, '') useEffect(() => { let cancelled = false setLoading(true) fetch(`/api/apps/${appName}`, { credentials: 'include' }) .then((res) => { if (!res.ok) throw new Error(`Failed to load app: ${res.status}`) return res.text() }) .then((html) => { if (cancelled) return if (iframeRef.current) { iframeRef.current.srcdoc = html } }) .catch((err: unknown) => { if (cancelled) return setError(err instanceof Error ? err.message : String(err)) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [appName]) const handleMessage = useCallback( (event: MessageEvent) => { if (!iframeRef.current?.contentWindow) return const msg = event.data if (!msg || msg.jsonrpc !== '2.0') return const params = msg.params ?? {} switch (msg.method) { case 'ui/initialize': iframeRef.current.contentWindow.postMessage( { jsonrpc: '2.0', method: 'tool/input', params: data }, '*' ) break case 'tools/call': onToolCall?.(params.name, params.arguments ?? {}) iframeRef.current.contentWindow.postMessage( { jsonrpc: '2.0', id: msg.id, result: { status: 'accepted' } }, '*' ) break case 'ui/sendMessage': if (onSendMessage && params.content) { onSendMessage(params.content) } break case 'ui/resize': if (typeof params.height === 'number') { setIframeHeight(Math.min(params.height, 800)) } break default: break } }, [data, onToolCall, onSendMessage] ) useEffect(() => { window.addEventListener('message', handleMessage) return () => { window.removeEventListener('message', handleMessage) } }, [handleMessage]) if (error) { return (
MCP App error: {error}
) } return (
{loading ? ( ) : ( )} {appName}