import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge"; import type { Tool } from "@modelcontextprotocol/client"; 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 { getTheme, toggleTheme, onThemeChange, type Theme } from "./theme"; 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; // Parse URL query params for debugging: ?server=name&tool=name&call=true&theme=hide function getQueryParams() { const params = new URLSearchParams(window.location.search); return { server: params.get("server"), tool: params.get("tool"), call: params.get("call") === "true", hideThemeToggle: params.get("theme") === "hide", }; } /** * Theme toggle button with light/dark icons. */ function ThemeToggle() { const [theme, setTheme] = useState(getTheme); useEffect(() => { return onThemeChange(setTheme); }, []); return ( ); } function Host({ serversPromise }: HostProps) { const [toolCalls, setToolCalls] = useState([]); const [destroyingIds, setDestroyingIds] = useState>(new Set()); const queryParams = useMemo(() => getQueryParams(), []); 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 ( <> {!queryParams.hideThemeToggle && } {toolCalls.map((info) => ( requestClose(info.id)} onCloseComplete={() => completeClose(info.id)} /> ))} setToolCalls([...toolCalls, { ...info, id: nextToolCallId++ }])} initialServer={queryParams.server} initialTool={queryParams.tool} autoCall={queryParams.call} /> ); } // CallToolPanel renders the unified form with Suspense around ServerSelect interface CallToolPanelProps { serversPromise: Promise; addToolCall: (info: ToolCallInfo) => void; initialServer?: string | null; initialTool?: string | null; autoCall?: boolean; } function CallToolPanel({ serversPromise, addToolCall, initialServer, initialTool, autoCall }: CallToolPanelProps) { const [selectedServer, setSelectedServer] = useState(null); const [selectedTool, setSelectedTool] = useState(""); const [inputJson, setInputJson] = useState("{}"); const [hasAutoCalledRef] = useState({ called: false }); // 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, preferredTool?: string) => { setSelectedServer(server); // Filter out app-only tools, prioritize tools with UIs const visibleTools = Array.from(server.tools.values()) .filter((tool) => isToolVisibleToModel(tool)) .sort(compareTools); // Use preferred tool if it exists and is visible, otherwise first visible tool const targetTool = preferredTool && visibleTools.some(t => t.name === preferredTool) ? preferredTool : visibleTools[0]?.name ?? ""; setSelectedTool(targetTool); // Set input JSON to tool defaults (if any) setInputJson(getToolDefaults(server.tools.get(targetTool))); }; const handleToolSelect = (toolName: string) => { setSelectedTool(toolName); // Set input JSON to tool defaults (if any) setInputJson(getToolDefaults(selectedServer?.tools.get(toolName))); }; // Submit with optional override for server/tool (used by auto-call) const handleSubmit = (overrideServer?: ServerInfo, overrideTool?: string) => { const server = overrideServer ?? selectedServer; const tool = overrideTool ?? selectedTool; if (!server) return; const toolCallInfo = callTool(server, tool, JSON.parse(inputJson)); addToolCall(toolCallInfo); // Update URL for easy refresh/sharing (without triggering navigation) const url = new URL(window.location.href); url.searchParams.set("server", server.name); url.searchParams.set("tool", tool); url.searchParams.set("call", "true"); // Auto-call on refresh history.replaceState(null, "", url.toString()); }; return (
{ e.preventDefault(); handleSubmit(); }}>