From d8092cf00dcf68f4343bc6ad413c51a6304d228c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:30:40 +0000 Subject: [PATCH 01/33] Fix MCP Apps rendering issue and add comprehensive logging - Fixed AppRenderer useEffect dependency array to include resourceContent This ensures the component re-evaluates when resource content arrives - Added detailed console logging throughout the app lifecycle: * Resource fetch and response tracking in App.tsx * Setup conditions and AppBridge creation in AppRenderer.tsx * HTML parsing and iframe rendering steps * PostMessageTransport and AppBridge connection status * App tool filtering and selection in AppsTab.tsx - Refactored AppsTab selectedTool rendering for better tracking The issue was that resourceContent prop updates weren't triggering the AppRenderer setup effect. Now the effect properly responds to both resourceUri and resourceContent changes. Co-authored-by: Cliff Hall --- client/package.json | 1 + client/src/App.tsx | 55 ++++- client/src/components/AppRenderer.tsx | 254 ++++++++++++++++++++++ client/src/components/AppsTab.tsx | 157 ++++++++++++++ package-lock.json | 300 ++++++++++++++++++++++---- 5 files changed, 723 insertions(+), 44 deletions(-) create mode 100644 client/src/components/AppRenderer.tsx create mode 100644 client/src/components/AppsTab.tsx diff --git a/client/package.json b/client/package.json index 1feec2122..0d2a6a26e 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "cleanup:e2e": "node e2e/global-teardown.js" }, "dependencies": { + "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.25.2", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", diff --git a/client/src/App.tsx b/client/src/App.tsx index 39fc2812a..0a8d536a4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -51,6 +51,7 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { + AppWindow, Bell, Files, FolderTree, @@ -75,6 +76,7 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import TasksTab from "./components/TasksTab"; +import AppsTab from "./components/AppsTab"; import { InspectorConfig } from "./lib/configurationTypes"; import { getMCPProxyAddress, @@ -308,11 +310,13 @@ const App = () => { ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), + "apps", "ping", "sampling", "elicitations", "roots", "auth", + "metadata", ]; if (!validTabs.includes(originatingTab)) return; @@ -440,11 +444,13 @@ const App = () => { ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), + "apps", "ping", "sampling", "elicitations", "roots", "auth", + "metadata", ]; const isValidTab = validTabs.includes(hash); @@ -473,6 +479,13 @@ const App = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [mcpClient, activeTab]); + useEffect(() => { + if (mcpClient && activeTab === "apps" && serverCapabilities?.tools) { + void listTools(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mcpClient, activeTab, serverCapabilities?.tools]); + useEffect(() => { localStorage.setItem("lastCommand", command); }, [command]); @@ -757,11 +770,13 @@ const App = () => { ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), + "apps", "ping", "sampling", "elicitations", "roots", "auth", + "metadata", ]; if (validTabs.includes(originatingTab)) { @@ -851,6 +866,7 @@ const App = () => { }; const readResource = async (uri: string) => { + console.log("[App] Reading resource:", uri); lastToolCallOriginTabRef.current = currentTabRef.current; const response = await sendMCPRequest( @@ -861,12 +877,25 @@ const App = () => { ReadResourceResultSchema, "resources", ); + console.log("[App] Resource read response:", { + uri, + responseLength: JSON.stringify(response).length, + hasContents: !!(response as { contents?: unknown[] }).contents, + }); const content = JSON.stringify(response, null, 2); setResourceContent(content); - setResourceContentMap((prev) => ({ - ...prev, - [uri]: content, - })); + setResourceContentMap((prev) => { + const updated = { + ...prev, + [uri]: content, + }; + console.log("[App] Updated resourceContentMap:", { + uri, + contentLength: content.length, + mapKeys: Object.keys(updated), + }); + return updated; + }); }; const subscribeToResource = async (uri: string) => { @@ -1308,6 +1337,10 @@ const App = () => { Tasks + + + Apps + Ping @@ -1497,6 +1530,20 @@ const App = () => { error={errors.tasks} nextCursor={nextTaskCursor} /> + { + clearError("tools"); + listTools(); + }} + error={errors.tools} + mcpClient={mcpClient} + onReadResource={(uri: string) => { + clearError("resources"); + readResource(uri); + }} + resourceContentMap={resourceContentMap} + /> { diff --git a/client/src/components/AppRenderer.tsx b/client/src/components/AppRenderer.tsx new file mode 100644 index 000000000..64654c8f2 --- /dev/null +++ b/client/src/components/AppRenderer.tsx @@ -0,0 +1,254 @@ +import { useEffect, useRef, useState } from "react"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Tool } from "@modelcontextprotocol/sdk/types.js"; +import { + AppBridge, + PostMessageTransport, + getToolUiResourceUri, + buildAllowAttribute, +} from "@modelcontextprotocol/ext-apps/app-bridge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle, Loader2 } from "lucide-react"; + +interface AppRendererProps { + tool: Tool; + mcpClient: Client | null; + onReadResource: (uri: string) => void; + resourceContent: string; +} + +interface UIResourceMeta { + ui?: { + resourceUri?: string; + permissions?: Record; + csp?: string; + }; +} + +const AppRenderer = ({ + tool, + mcpClient, + onReadResource, + resourceContent, +}: AppRendererProps) => { + const iframeRef = useRef(null); + const bridgeRef = useRef(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [initialized, setInitialized] = useState(false); + + // Extract UI metadata from tool + const resourceUri = getToolUiResourceUri(tool); + const meta = (tool as Tool & { _meta?: UIResourceMeta })._meta; + const permissions = meta?.ui?.permissions; + + // Fetch UI resource when component mounts or tool changes + useEffect(() => { + console.log("[AppRenderer] Resource fetch check:", { + resourceUri, + hasResourceContent: !!resourceContent, + resourceContentLength: resourceContent?.length || 0, + }); + + if (resourceUri && !resourceContent) { + console.log("[AppRenderer] Fetching resource:", resourceUri); + setLoading(true); + setError(null); + onReadResource(resourceUri); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resourceUri, resourceContent]); + + // Set up AppBridge and render the app + useEffect(() => { + console.log("[AppRenderer] Setup check:", { + hasResourceContent: !!resourceContent, + hasIframe: !!iframeRef.current, + hasMcpClient: !!mcpClient, + resourceContentPreview: resourceContent?.substring(0, 100), + }); + + if (!resourceContent || !iframeRef.current || !mcpClient) { + console.log("[AppRenderer] Setup conditions not met, skipping"); + return; + } + + console.log("[AppRenderer] Starting app setup"); + const iframe = iframeRef.current; + let bridge: AppBridge | null = null; + let transport: PostMessageTransport | null = null; + + const setupApp = async () => { + try { + console.log("[AppRenderer] Creating AppBridge..."); + // Create AppBridge with the MCP client + bridge = new AppBridge( + mcpClient, + { name: "MCP Inspector", version: "0.19.0" }, + { + openLinks: {}, + serverTools: {}, + logging: {}, + }, + { + hostContext: { + theme: document.documentElement.classList.contains("dark") + ? "dark" + : "light", + }, + }, + ); + + // Set up event handlers + bridge.oninitialized = () => { + console.log("MCP App initialized"); + setInitialized(true); + setLoading(false); + }; + + bridge.onerror = (error: Error) => { + console.error("MCP App error:", error); + setError(error.message || "An error occurred"); + }; + + // Create the iframe document with the UI resource content + const iframeDoc = + iframe.contentDocument || iframe.contentWindow?.document; + if (!iframeDoc) { + throw new Error("Could not access iframe document"); + } + + // Parse the resource content to extract HTML + let htmlContent = resourceContent; + console.log("[AppRenderer] Parsing resource content..."); + try { + const parsed = JSON.parse(resourceContent); + console.log("[AppRenderer] Parsed JSON:", { + hasContents: !!parsed.contents, + isArray: Array.isArray(parsed.contents), + contentsLength: parsed.contents?.length, + }); + if (parsed.contents && Array.isArray(parsed.contents)) { + // MCP resource response format + const textContent = parsed.contents.find( + (c: { type: string; text?: string }) => + c.type === "text" && c.text, + ); + if (textContent?.text) { + htmlContent = textContent.text; + console.log( + "[AppRenderer] Extracted HTML from contents, length:", + htmlContent.length, + ); + } + } + } catch (err) { + console.log("[AppRenderer] Not JSON, using content as-is:", err); + } + + // Write the HTML content to the iframe + console.log("[AppRenderer] Writing HTML to iframe..."); + iframeDoc.open(); + iframeDoc.write(htmlContent); + iframeDoc.close(); + console.log("[AppRenderer] HTML written to iframe"); + + // Wait for iframe to load + await new Promise((resolve) => { + if (iframe.contentWindow) { + iframe.contentWindow.addEventListener("load", () => resolve(), { + once: true, + }); + } else { + resolve(); + } + }); + + // Create PostMessageTransport for communication + if (!iframe.contentWindow) { + throw new Error("Iframe contentWindow not available"); + } + + console.log("[AppRenderer] Creating PostMessageTransport..."); + transport = new PostMessageTransport( + iframe.contentWindow, + iframe.contentWindow, + ); + + // Connect the bridge + console.log("[AppRenderer] Connecting AppBridge..."); + await bridge.connect(transport); + console.log("[AppRenderer] AppBridge connected successfully"); + + bridgeRef.current = bridge; + } catch (err) { + console.error("Error setting up MCP App:", err); + setError(err instanceof Error ? err.message : "Failed to set up app"); + setLoading(false); + } + }; + + setupApp(); + + // Cleanup + return () => { + if (bridge) { + bridge.close().catch(console.error); + } + bridgeRef.current = null; + setInitialized(false); + }; + }, [resourceContent, mcpClient]); + + // Build iframe attributes + const allowAttribute = buildAllowAttribute(permissions); + + if (!resourceUri) { + return ( + + + + No UI resource URI found in tool metadata + + + ); + } + + return ( +
+ {loading && ( + + + Loading MCP App... + + )} + + {error && ( + + + {error} + + )} + +