File size: 5,176 Bytes
e1cc3bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { useEffect, useState } from "react";
import { Implementation } from "@modelcontextprotocol/sdk/types.js";
import { Client } from "@modelcontextprotocol/sdk/client";
import { App, McpUiAppCapabilities, PostMessageTransport } from "../app";
export * from "../app";

/**
 * Options for configuring the {@link useApp} hook.
 *
 * Note: This interface does NOT expose {@link App} options like `autoResize`.
 * The hook creates the `App` with default options (`autoResize: true`). If you
 * need custom `App` options, create the `App` manually instead of using this hook.
 *
 * @see {@link useApp} for the hook that uses these options
 * @see {@link useAutoResize} for manual auto-resize control with custom `App` options
 */
export interface UseAppOptions {
  /** App identification (name and version) */
  appInfo: Implementation;
  /**
   * Declares what features this app supports.
   */
  capabilities: McpUiAppCapabilities;
  /**
   * Called after {@link 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 a notification handler
   * ```typescript
   * import { McpUiToolInputNotificationSchema } from '@modelcontextprotocol/ext-apps/react';
   *
   * onAppCreated: (app) => {
   *   app.setNotificationHandler(
   *     McpUiToolInputNotificationSchema,
   *     (notification) => {
   *       console.log("Tool input:", notification.params.arguments);
   *     }
   *   );
   * }
   * ```
   */
  onAppCreated?: (app: App) => void;
}

/**
 * State returned by the {@link useApp} hook.
 */
export interface AppState {
  /** The connected {@link 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} creation and connection. It automatically
 * creates a {@link 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} 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
 * ```typescript
 * import { useApp, McpUiToolInputNotificationSchema } from '@modelcontextprotocol/ext-apps/react';
 *
 * function MyApp() {
 *   const { app, isConnected, error } = useApp({
 *     appInfo: { name: "MyApp", version: "1.0.0" },
 *     capabilities: {},
 *     onAppCreated: (app) => {
 *       // Register handlers before connection
 *       app.setNotificationHandler(
 *         McpUiToolInputNotificationSchema,
 *         (notification) => {
 *           console.log("Tool input:", notification.params.arguments);
 *         }
 *       );
 *     },
 *   });
 *
 *   if (error) return <div>Error: {error.message}</div>;
 *   if (!isConnected) return <div>Connecting...</div>;
 *   return <div>Connected!</div>;
 * }
 * ```
 *
 * @see {@link App.connect} for the underlying connection method
 * @see {@link useAutoResize} for manual auto-resize control when using custom App options
 */
export function useApp({
  appInfo,
  capabilities,
  onAppCreated,
}: UseAppOptions): AppState {
  const [app, setApp] = useState<App | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let mounted = true;

    async function connect() {
      try {
        const transport = new PostMessageTransport(
          window.parent,
          window.parent,
        );
        const app = new App(appInfo, capabilities);

        // Register handlers BEFORE connecting
        onAppCreated?.(app);

        await app.connect(transport);

        if (mounted) {
          setApp(app);
          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;
    };
  }, []); // Intentionally not including options to avoid reconnection

  return { app, isConnected, error };
}