import { useEffect, useState } from "react"; import { Implementation } from "@modelcontextprotocol/sdk/types.js"; import { Client } from "@modelcontextprotocol/sdk/client"; import { App, AppOptions, McpUiAppCapabilities, PostMessageTransport, } from "../app"; export * from "../app"; /** * Options for configuring the {@link useApp `useApp`} hook. * * The `autoResize` and `strict` options are forwarded to the underlying * {@link App `App`} instance. For other {@link AppOptions `AppOptions`}, * create the `App` manually instead of using this hook. * * @see {@link useApp `useApp`} for the hook that uses these options * @see {@link useAutoResize `useAutoResize`} for manual auto-resize control with custom `App` options */ export interface UseAppOptions extends Pick< AppOptions, "autoResize" | "strict" > { /** App identification (name and version) */ appInfo: Implementation; /** * Declares what features this app supports. */ capabilities: McpUiAppCapabilities; /** * Called after {@link App `App`} is created but before connection. * * Use this to register request/notification handlers that need to be in place * before the initialization handshake completes. * * @param app - The newly created `App` instance * * @example Register an event handler * ```tsx source="./useApp.examples.tsx#useApp_registerHandler" * useApp({ * appInfo: { name: "MyApp", version: "1.0.0" }, * capabilities: {}, * onAppCreated: (app) => { * app.ontoolresult = (result) => { * console.log("Tool result:", result); * }; * }, * }); * ``` */ onAppCreated?: (app: App) => void; } /** * State returned by the {@link useApp `useApp`} hook. */ export interface AppState { /** The connected {@link App `App`} instance, null during initialization */ app: App | null; /** Whether initialization completed successfully */ isConnected: boolean; /** Connection error if initialization failed, null otherwise */ error: Error | null; } /** * React hook to create and connect an MCP App. * * This hook manages {@link App `App`} creation and connection. It automatically * creates a {@link PostMessageTransport `PostMessageTransport`} to window.parent and handles * initialization. * * This hook is part of the optional React integration. The core SDK (`App`, * `PostMessageTransport`) is framework-agnostic and can be used with any UI * framework or vanilla JavaScript. * * **Important**: The hook intentionally does NOT re-run when options change * to avoid reconnection loops. Options are only used during the initial mount. * Furthermore, the `App` instance is NOT closed on unmount. This avoids cleanup * issues during React Strict Mode's double-mount cycle. If you need to * explicitly close the `App`, call {@link App.close `App.close`} manually. * * @param options - Configuration for the app * @returns Current connection state and app instance. If connection fails during * initialization, the `error` field will contain the error (typically connection * timeouts, initialization handshake failures, or transport errors). * * @example Basic usage of useApp hook with common event handlers * ```tsx source="./useApp.examples.tsx#useApp_basicUsage" * function MyApp() { * const [hostContext, setHostContext] = useState< * McpUiHostContext | undefined * >(undefined); * * const { app, isConnected, error } = useApp({ * appInfo: { name: "MyApp", version: "1.0.0" }, * capabilities: {}, * onAppCreated: (app) => { * app.ontoolinput = (input) => { * console.log("Tool input:", input); * }; * app.ontoolresult = (result) => { * console.log("Tool result:", result); * }; * app.ontoolcancelled = (params) => { * console.log("Tool cancelled:", params.reason); * }; * app.onerror = (error) => { * console.log("Error:", error); * }; * app.onhostcontextchanged = (ctx) => { * setHostContext((prev) => ({ ...prev, ...ctx })); * }; * }, * }); * * if (error) return
Error: {error.message}
; * if (!isConnected) return
Connecting...
; * return
Theme: {hostContext?.theme}
; * } * ``` * * @see {@link App.connect `App.connect`} for the underlying connection method * @see {@link useAutoResize `useAutoResize`} for manual auto-resize control when using custom App options */ export function useApp({ appInfo, capabilities, onAppCreated, autoResize = true, strict, }: UseAppOptions): AppState { const [app, setApp] = useState(null); const [isConnected, setIsConnected] = useState(false); const [error, setError] = useState(null); useEffect(() => { let mounted = true; let appInstance: App | undefined; async function connect() { try { const transport = new PostMessageTransport( window.parent, window.parent, ); appInstance = new App(appInfo, capabilities, { autoResize, strict }); // Register handlers BEFORE connecting onAppCreated?.(appInstance); await appInstance.connect(transport); if (!mounted) { // Unmounted during the handshake (e.g. React StrictMode dev // double-invoke). Close so the abandoned transport's window // `message` listener doesn't keep receiving host postMessages // alongside the second mount's instance. void appInstance.close(); return; } setApp(appInstance); setIsConnected(true); setError(null); } catch (error) { if (mounted) { setApp(null); setIsConnected(false); setError( error instanceof Error ? error : new Error("Failed to connect"), ); } } } connect(); return () => { mounted = false; void appInstance?.close(); }; }, []); // Intentionally not including options to avoid reconnection return { app, isConnected, error }; }