import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation"; import styles from "./index.module.css"; /** * Check if a tool is visible to the model (not app-only). * Tools with `visibility: ["app"]` should not be shown in tool lists. */ function isToolVisibleToModel(tool: { _meta?: Record }): boolean { const result = McpUiToolMetaSchema.safeParse(tool._meta?.ui); if (!result.success) return true; // default: visible to model const visibility = result.data.visibility; if (!visibility) return true; // default: visible to model return visibility.includes("model"); } /** Compare tools: UI-enabled first, then alphabetically by name. */ function compareTools(a: Tool, b: Tool): number { const aHasUi = !!getToolUiResourceUri(a); const bHasUi = !!getToolUiResourceUri(b); if (aHasUi && !bHasUi) return -1; if (!aHasUi && bHasUi) return 1; return a.name.localeCompare(b.name); } /** * Extract default values from a tool's JSON Schema inputSchema. * Returns a formatted JSON string with defaults, or "{}" if none found. */ function getToolDefaults(tool: Tool | undefined): string { if (!tool?.inputSchema?.properties) return "{}"; const defaults: Record = {}; for (const [key, prop] of Object.entries(tool.inputSchema.properties)) { if (prop && typeof prop === "object" && "default" in prop) { defaults[key] = prop.default; } } return Object.keys(defaults).length > 0 ? JSON.stringify(defaults, null, 2) : "{}"; } // Host passes serversPromise to CallToolPanel interface HostProps { serversPromise: Promise; } type ToolCallEntry = ToolCallInfo & { id: number }; let nextToolCallId = 0; function Host({ serversPromise }: HostProps) { const [toolCalls, setToolCalls] = useState([]); const [destroyingIds, setDestroyingIds] = useState>(new Set()); const requestClose = (id: number) => { setDestroyingIds((s) => new Set(s).add(id)); }; const completeClose = (id: number) => { setDestroyingIds((s) => { const next = new Set(s); next.delete(id); return next; }); setToolCalls((calls) => calls.filter((c) => c.id !== id)); }; return ( <> {toolCalls.map((info) => ( requestClose(info.id)} onCloseComplete={() => completeClose(info.id)} /> ))} setToolCalls([...toolCalls, { ...info, id: nextToolCallId++ }])} /> ); } // CallToolPanel renders the unified form with Suspense around ServerSelect interface CallToolPanelProps { serversPromise: Promise; addToolCall: (info: ToolCallInfo) => void; } function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) { const [selectedServer, setSelectedServer] = useState(null); const [selectedTool, setSelectedTool] = useState(""); const [inputJson, setInputJson] = useState("{}"); // Filter out app-only tools, prioritize tools with UIs const toolNames = selectedServer ? Array.from(selectedServer.tools.values()) .filter((tool) => isToolVisibleToModel(tool)) .sort(compareTools) .map((tool) => tool.name) : []; const isValidJson = useMemo(() => { try { JSON.parse(inputJson); return true; } catch { return false; } }, [inputJson]); const handleServerSelect = (server: ServerInfo) => { setSelectedServer(server); // Filter out app-only tools, prioritize tools with UIs const visibleTools = Array.from(server.tools.values()) .filter((tool) => isToolVisibleToModel(tool)) .sort(compareTools); const firstTool = visibleTools[0]?.name ?? ""; setSelectedTool(firstTool); // Set input JSON to tool defaults (if any) setInputJson(getToolDefaults(server.tools.get(firstTool))); }; const handleToolSelect = (toolName: string) => { setSelectedTool(toolName); // Set input JSON to tool defaults (if any) setInputJson(getToolDefaults(selectedServer?.tools.get(toolName))); }; const handleSubmit = () => { if (!selectedServer) return; const toolCallInfo = callTool(selectedServer, selectedTool, JSON.parse(inputJson)); addToolCall(toolCallInfo); }; return (
{ e.preventDefault(); handleSubmit(); }}>