From 99de0926d5f89b245591375da8827c58875321ea Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Thu, 5 Feb 2026 14:05:36 -0500 Subject: [PATCH 01/59] refactor(ui): use @mcp-ui/client AppRenderer for MCP Apps - Update @mcp-ui/client from ^5.17.3 to ^6.0.0 - Replace custom useSandboxBridge hook with AppRenderer component - Split monolithic handleMcpRequest into separate handler functions: - handleOpenLink, handleMessage, handleCallTool, handleReadResource - Add convertCspToMcpUi helper for CSP format conversion - Delete unused useSandboxBridge.ts file This aligns with the official MCP Apps specification and may help with secure context requirements for Web Payments SDK integration. --- ui/desktop/package-lock.json | 279 ++++++++++- ui/desktop/package.json | 2 +- .../src/components/McpApps/McpAppRenderer.tsx | 450 +++++++++++------- .../components/McpApps/useSandboxBridge.ts | 300 ------------ 4 files changed, 540 insertions(+), 491 deletions(-) delete mode 100644 ui/desktop/src/components/McpApps/useSandboxBridge.ts diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 8531e6a1f847..a34b1313003b 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -9,7 +9,7 @@ "version": "1.23.0", "license": "Apache-2.0", "dependencies": { - "@mcp-ui/client": "^5.17.3", + "@mcp-ui/client": "^6.0.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", @@ -3027,27 +3027,151 @@ } }, "node_modules/@mcp-ui/client": { - "version": "5.17.3", - "resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-5.17.3.tgz", - "integrity": "sha512-Xxi8d5NYbCBUdBEqhwnm7PgJE6+F1wOV2sPA0AWnfLoudpiq8iomovL/AMFI+alVZwhSBxzqJKqUdbPD2yz5tQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-6.0.0.tgz", + "integrity": "sha512-dHIQGjFOoBWBntSRUJH5YFeq7xi2rEPS0EwokeNAnMg6xrjGjvNd6vTWDHFRC04OlO/ogvM1r5+xUoo0OETaaQ==", "license": "Apache-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.22.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", + "@modelcontextprotocol/sdk": "^1.24.0", "@quilted/threads": "^3.1.3", "@r2wc/react-to-web-component": "^2.0.4", "@remote-dom/core": "^1.8.0", - "@remote-dom/react": "^1.2.2" + "@remote-dom/react": "^1.2.2", + "zod": "^3.23.8" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-0.3.1.tgz", + "integrity": "sha512-Iivz2KwWK8xlRbiWwFB/C4NXqE8VJBoRCbBkJCN98ST2UbQvA6kfyebcLsypiqylJS467XOOaBcI9DeQ3t+zqA==", + "hasInstallScript": true, + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "optionalDependencies": { + "@oven/bun-darwin-aarch64": "^1.2.21", + "@oven/bun-darwin-x64": "^1.2.21", + "@oven/bun-darwin-x64-baseline": "^1.2.21", + "@oven/bun-linux-aarch64": "^1.2.21", + "@oven/bun-linux-aarch64-musl": "^1.2.21", + "@oven/bun-linux-x64": "^1.2.21", + "@oven/bun-linux-x64-baseline": "^1.2.21", + "@oven/bun-linux-x64-musl": "^1.2.21", + "@oven/bun-linux-x64-musl-baseline": "^1.2.21", + "@oven/bun-windows-x64": "^1.2.21", + "@oven/bun-windows-x64-baseline": "^1.2.21", + "@rollup/rollup-darwin-arm64": "^4.53.3", + "@rollup/rollup-darwin-x64": "^4.53.3", + "@rollup/rollup-linux-arm64-gnu": "^4.53.3", + "@rollup/rollup-linux-x64-gnu": "^4.53.3", + "@rollup/rollup-win32-arm64-msvc": "^4.53.3", + "@rollup/rollup-win32-x64-msvc": "^4.53.3" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.25.2", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", @@ -3214,6 +3338,149 @@ "node": ">=10" } }, + "node_modules/@oven/bun-darwin-aarch64": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.8.tgz", + "integrity": "sha512-hPERz4IgXCM6Y6GdEEsJAFceyJMt29f3HlFzsvE/k+TQjChRhar6S+JggL35b9VmFfsdxyCOOTPqgnSrdV0etA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-darwin-x64": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.8.tgz", + "integrity": "sha512-SaWIxsRQYiT/eA60bqA4l8iNO7cJ6YD8ie82RerRp9voceBxPIZiwX4y20cTKy5qNaSGr9LxfYq7vDywTipiog==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-darwin-x64-baseline": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.8.tgz", + "integrity": "sha512-ArHVWpCRZI3vGLoN2/8ud8Kzqlgn1Gv+fNw+pMB9x18IzgAEhKxFxsWffnoaH21amam4tAOhpeewRIgdNtB0Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-linux-aarch64": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.8.tgz", + "integrity": "sha512-rq0nNckobtS+ONoB95/Frfqr8jCtmSjjjEZlN4oyUx0KEBV11Vj4v3cDVaWzuI34ryL8FCog3HaqjfKn8R82Tw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-aarch64-musl": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.8.tgz", + "integrity": "sha512-HvJmhrfipL7GtuqFz6xNpmf27NGcCOMwCalPjNR6fvkLpe8A7Z1+QbxKKjOglelmlmZc3Vi2TgDUtxSqfqOToQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.8.tgz", + "integrity": "sha512-YDgqVx1MI8E0oDbCEUSkAMBKKGnUKfaRtMdLh9Bjhu7JQacQ/ZCpxwi4HPf5Q0O1TbWRrdxGw2tA2Ytxkn7s1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64-baseline": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.8.tgz", + "integrity": "sha512-3IkS3TuVOzMqPW6Gg9/8FEoKF/rpKZ9DZUfNy9GQ54+k4PGcXpptU3+dy8D4iDFCt4qe6bvoiAOdM44OOsZ+Wg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64-musl": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.8.tgz", + "integrity": "sha512-o7Jm5zL4aw9UBs3BcZLVbgGm2V4F10MzAQAV+ziKzoEfYmYtvDqRVxgKEq7BzUOVy4LgfrfwzEXw5gAQGRrhQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64-musl-baseline": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.8.tgz", + "integrity": "sha512-5g8XJwHhcTh8SGoKO7pR54ILYDbuFkGo+68DOMTiVB5eLxuLET+Or/camHgk4QWp3nUS5kNjip4G8BE8i0rHVQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-windows-x64": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.8.tgz", + "integrity": "sha512-UDI3rowMm/tI6DIynpE4XqrOhr+1Ztk1NG707Wxv2nygup+anTswgCwjfjgmIe78LdoRNFrux2GpeolhQGW6vQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oven/bun-windows-x64-baseline": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.8.tgz", + "integrity": "sha512-K6qBUKAZLXsjAwFxGTG87dsWlDjyDl2fqjJr7+x7lmv2m+aSEzmLOK+Z5pSvGkpjBp3LXV35UUgj8G0UTd0pPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { "version": "11.16.2", "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.16.2.tgz", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 3166488f49ec..d562d37c26ca 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -40,7 +40,7 @@ "start-alpha-gui": "ALPHA=true npm run start-gui" }, "dependencies": { - "@mcp-ui/client": "^5.17.3", + "@mcp-ui/client": "^6.0.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", diff --git a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx index 1c9d8fab6ccc..4503acce2806 100644 --- a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx +++ b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx @@ -2,28 +2,26 @@ import { AppEvents } from '../../constants/events'; /** * MCP Apps Renderer * - * Temporary Goose implementation while waiting for official SDK components. + * Uses the official @mcp-ui/client AppRenderer component for rendering MCP Apps. * * @see SEP-1865 https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx + * @see @mcp-ui/client https://github.com/MCP-UI-Org/mcp-ui */ -import { useState, useCallback, useEffect } from 'react'; -import { useSandboxBridge } from './useSandboxBridge'; -import { - ToolInput, - ToolInputPartial, - ToolResult, - ToolCancelled, - CspMetadata, - PermissionsMetadata, - McpMethodParams, - McpMethodResponse, -} from './types'; +import { useState, useCallback, useEffect, useMemo } from 'react'; +import { AppRenderer } from '@mcp-ui/client'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { + McpUiSizeChangedNotification, + McpUiResourceCsp, +} from '@modelcontextprotocol/ext-apps/app-bridge'; +import { ToolInput, ToolInputPartial, ToolResult, ToolCancelled, CspMetadata } from './types'; import { cn } from '../../utils'; -import { DEFAULT_IFRAME_HEIGHT } from './utils'; +import { DEFAULT_IFRAME_HEIGHT, fetchMcpAppProxyUrl } from './utils'; import { readResource, callTool } from '../../api'; import { errorMessage } from '../../utils/conversionUtils'; import { isProtocolSafe, getProtocol } from '../../utils/urlSecurity'; +import { useTheme } from '../../contexts/ThemeContext'; interface McpAppRendererProps { resourceUri: string; @@ -41,7 +39,7 @@ interface McpAppRendererProps { interface ResourceData { html: string | null; csp: CspMetadata | null; - permissions: PermissionsMetadata | null; + permissions: string | null; prefersBorder: boolean; } @@ -57,6 +55,7 @@ export default function McpAppRenderer({ fullscreen = false, cachedHtml, }: McpAppRendererProps) { + const { resolvedTheme } = useTheme(); const [resource, setResource] = useState({ html: cachedHtml || null, csp: null, @@ -66,13 +65,24 @@ export default function McpAppRenderer({ const [error, setError] = useState(null); const [iframeHeight, setIframeHeight] = useState(DEFAULT_IFRAME_HEIGHT); const [iframeWidth, setIframeWidth] = useState(null); + const [sandboxUrl, setSandboxUrl] = useState(null); + // Fetch the sandbox proxy URL + useEffect(() => { + fetchMcpAppProxyUrl(resource.csp).then((url) => { + if (url) { + setSandboxUrl(new URL(url)); + } + }); + }, [resource.csp]); + + // Fetch the resource HTML and metadata useEffect(() => { if (!sessionId) { return; } - const fetchResource = async () => { + const fetchResourceData = async () => { try { const response = await readResource({ body: { @@ -85,14 +95,20 @@ export default function McpAppRenderer({ if (response.data) { const content = response.data; const meta = content._meta as - | { ui?: { csp?: CspMetadata; permissions?: PermissionsMetadata; prefersBorder?: boolean } } + | { + ui?: { + csp?: CspMetadata; + permissions?: { sandbox?: string }; + prefersBorder?: boolean; + }; + } | undefined; if (content.text !== cachedHtml) { setResource({ html: content.text, csp: meta?.ui?.csp || null, - permissions: meta?.ui?.permissions || null, + permissions: meta?.ui?.permissions?.sandbox || null, prefersBorder: meta?.ui?.prefersBorder ?? true, }); } @@ -106,154 +122,227 @@ export default function McpAppRenderer({ } }; - fetchResource(); + fetchResourceData(); }, [resourceUri, extensionName, sessionId, cachedHtml]); - const handleMcpRequest = useCallback( - async ( - method: string, - params: Record = {}, - _id?: string | number - ): Promise => { - // Methods that require a session - const requiresSession = ['tools/call', 'resources/read']; - if (requiresSession.includes(method) && !sessionId) { - throw new Error('Session not initialized for MCP request'); + // Handler for open-link requests from the guest UI + const handleOpenLink = useCallback(async ({ url }: { url: string }) => { + // Safe protocols open directly, unknown protocols require confirmation + // Dangerous protocols are blocked by main.ts in the open-external handler + if (isProtocolSafe(url)) { + await window.electron.openExternal(url); + return { status: 'success' as const }; + } + + const protocol = getProtocol(url); + if (!protocol) { + return { status: 'error' as const, message: 'Invalid URL' }; + } + + const result = await window.electron.showMessageBox({ + type: 'question', + buttons: ['Cancel', 'Open'], + defaultId: 0, + title: 'Open External Link', + message: `Open ${protocol} link?`, + detail: `This will open: ${url}`, + }); + + if (result.response !== 1) { + return { status: 'error' as const, message: 'User cancelled' }; + } + + await window.electron.openExternal(url); + return { status: 'success' as const }; + }, []); + + // Handler for message requests from the guest UI + const handleMessage = useCallback( + async ({ content }: { content: Array<{ type: string; text?: string }> }) => { + if (!append) { + throw new Error('Message handler not available in this context'); } - switch (method) { - case 'ui/open-link': { - const { url } = params as McpMethodParams['ui/open-link']; - - // Safe protocols open directly, unknown protocols require confirmation - // Dangerous protocols are blocked by main.ts in the open-external handler - if (isProtocolSafe(url)) { - await window.electron.openExternal(url); - } else { - const protocol = getProtocol(url); - if (!protocol) { - return { - status: 'error', - message: 'Invalid URL', - } as McpMethodResponse['ui/open-link']; - } - - const result = await window.electron.showMessageBox({ - type: 'question', - buttons: ['Cancel', 'Open'], - defaultId: 0, - title: 'Open External Link', - message: `Open ${protocol} link?`, - detail: `This will open: ${url}`, - }); - if (result.response !== 1) { - return { - status: 'error', - message: 'User cancelled', - } as McpMethodResponse['ui/open-link']; - } - await window.electron.openExternal(url); - } + if (!Array.isArray(content)) { + throw new Error('Invalid message format: content must be an array of ContentBlock'); + } - return { - status: 'success', - message: 'Link opened successfully', - } satisfies McpMethodResponse['ui/open-link']; - } + // Extract first text block from content, ignoring other block types + const textContent = content.find((block) => block.type === 'text'); + if (!textContent || !textContent.text) { + throw new Error('Invalid message format: content must contain a text block'); + } - case 'ui/message': { - const { content } = params as McpMethodParams['ui/message']; - if (!append) { - throw new Error('Message handler not available in this context'); - } + append(textContent.text); + window.dispatchEvent(new CustomEvent(AppEvents.SCROLL_CHAT_TO_BOTTOM)); + return {}; + }, + [append] + ); - if (!Array.isArray(content)) { - throw new Error('Invalid message format: content must be an array of ContentBlock'); - } + // Handler for tools/call requests from the guest UI + const handleCallTool = useCallback( + async ({ + name, + arguments: args, + }: { + name: string; + arguments?: Record; + }): Promise => { + if (!sessionId) { + throw new Error('Session not initialized for MCP request'); + } - // Extract first text block from content, ignoring other block types - const textContent = content.find((block) => block.type === 'text'); - if (!textContent) { - throw new Error('Invalid message format: content must contain a text block'); + const fullToolName = `${extensionName}__${name}`; + const response = await callTool({ + body: { + session_id: sessionId, + name: fullToolName, + arguments: args || {}, + }, + }); + + // Map from snake_case API response to camelCase SDK types + const content = response.data?.content || []; + return { + content: content.map((item) => { + if ('text' in item && item.text !== undefined) { + return { type: 'text' as const, text: item.text }; + } + if ('data' in item && item.data !== undefined) { + return { + type: 'image' as const, + data: item.data, + mimeType: item.mimeType || 'image/png', + }; } + // Default to text type for unknown content + return { type: 'text' as const, text: JSON.stringify(item) }; + }), + isError: response.data?.is_error || false, + }; + }, + [sessionId, extensionName] + ); - // MCP Apps can send other content block types, but we only append text blocks for now + // Handler for resources/read requests from the guest UI + const handleReadResource = useCallback( + async ({ uri }: { uri: string }) => { + if (!sessionId) { + throw new Error('Session not initialized for MCP request'); + } - append(textContent.text); - window.dispatchEvent(new CustomEvent(AppEvents.SCROLL_CHAT_TO_BOTTOM)); - return {} satisfies McpMethodResponse['ui/message']; - } + const response = await readResource({ + body: { + session_id: sessionId, + uri, + extension_name: extensionName, + }, + }); - case 'tools/call': { - const { name, arguments: args } = params as McpMethodParams['tools/call']; - const fullToolName = `${extensionName}__${name}`; - const response = await callTool({ - body: { - session_id: sessionId!, - name: fullToolName, - arguments: args || {}, - }, - }); - return { - content: response.data?.content || [], - isError: response.data?.is_error || false, - structuredContent: (response.data as Record)?.structured_content as - | Record - | undefined, - } satisfies McpMethodResponse['tools/call']; - } + // Map from API response to SDK types + const data = response.data; + if (!data) { + return { contents: [] }; + } - case 'resources/read': { - const { uri } = params as McpMethodParams['resources/read']; - const response = await readResource({ - body: { - session_id: sessionId!, - uri, - extension_name: extensionName, - }, - }); - return { - contents: response.data ? [response.data] : [], - } satisfies McpMethodResponse['resources/read']; - } + // Convert to the expected format with required uri field + const resourceContent = { + uri: data.uri || uri, + text: data.text, + mimeType: data.mimeType || undefined, + }; - case 'notifications/message': { - const { level, logger, data } = params as McpMethodParams['notifications/message']; - console.log( - `[MCP App Notification]${logger ? ` [${logger}]` : ''} ${level || 'info'}:`, - data - ); - return {} satisfies McpMethodResponse['notifications/message']; - } + return { + contents: [resourceContent], + }; + }, + [sessionId, extensionName] + ); - case 'ping': - return {} satisfies McpMethodResponse['ping']; + // Handler for logging messages from the guest UI + const handleLoggingMessage = useCallback( + ({ level, logger, data }: { level?: string; logger?: string; data?: unknown }) => { + console.log( + `[MCP App Notification]${logger ? ` [${logger}]` : ''} ${level || 'info'}:`, + data + ); + }, + [] + ); - default: - throw new Error(`Unknown method: ${method}`); + // Handler for size change notifications from the guest UI + const handleSizeChanged = useCallback( + ({ height, width }: McpUiSizeChangedNotification['params']) => { + if (height !== undefined) { + const newHeight = Math.max(DEFAULT_IFRAME_HEIGHT, height); + setIframeHeight(newHeight); } + setIframeWidth(width ?? null); }, - [append, sessionId, extensionName] + [] ); - const handleSizeChanged = useCallback((height: number, width?: number) => { - const newHeight = Math.max(DEFAULT_IFRAME_HEIGHT, height); - setIframeHeight(newHeight); - setIframeWidth(width ?? null); + // Handler for errors + const handleError = useCallback((err: Error) => { + console.error('[MCP App Error]:', err); + setError(errorMessage(err)); }, []); - const { iframeRef, proxyUrl } = useSandboxBridge({ - resourceHtml: resource.html || '', - resourceCsp: resource.csp, - resourcePermissions: resource.permissions, - resourceUri, - toolInput, - toolInputPartial, - toolResult, - toolCancelled, - onMcpRequest: handleMcpRequest, - onSizeChanged: handleSizeChanged, - }); + // Convert CspMetadata to McpUiResourceCsp (handle null -> undefined) + const convertCspToMcpUi = useCallback((csp: CspMetadata | null): McpUiResourceCsp | undefined => { + if (!csp) return undefined; + return { + connectDomains: csp.connectDomains ?? undefined, + resourceDomains: csp.resourceDomains ?? undefined, + }; + }, []); + + // Sandbox configuration + const sandboxConfig = useMemo(() => { + if (!sandboxUrl) return null; + return { + url: sandboxUrl, + permissions: resource.permissions || 'allow-scripts allow-same-origin allow-forms', + csp: convertCspToMcpUi(resource.csp), + }; + }, [sandboxUrl, resource.permissions, resource.csp, convertCspToMcpUi]); + + // Host context for the guest UI + const hostContext = useMemo( + () => ({ + theme: resolvedTheme, + displayMode: fullscreen ? ('fullscreen' as const) : ('inline' as const), + availableDisplayModes: ['inline' as const, 'fullscreen' as const], + }), + [resolvedTheme, fullscreen] + ); + + // Convert toolResult to CallToolResult format expected by AppRenderer + const appToolResult = useMemo((): CallToolResult | undefined => { + if (!toolResult) return undefined; + // Map from snake_case to camelCase + const content = toolResult.content || []; + return { + content: content.map((item) => { + if ('text' in item && item.text !== undefined) { + return { type: 'text' as const, text: item.text }; + } + if ('data' in item && item.data !== undefined) { + return { + type: 'image' as const, + data: item.data, + mimeType: item.mimeType || 'image/png', + }; + } + return { type: 'text' as const, text: JSON.stringify(item) }; + }), + isError: toolResult.is_error || false, + }; + }, [toolResult]); + + // Convert toolCancelled to boolean + const isToolCancelled = toolCancelled?.reason !== undefined; if (error) { return ( @@ -263,58 +352,51 @@ export default function McpAppRenderer({ ); } - if (fullscreen) { - return proxyUrl ? ( -