|
| 1 | +import { useCallback, useMemo } from "react"; |
| 2 | + |
| 3 | +/** Options for sending a message */ |
| 4 | +interface SendMessageOptions { |
| 5 | + /** Target origin for postMessage. Defaults to "*" */ |
| 6 | + targetOrigin?: string; |
| 7 | +} |
| 8 | + |
| 9 | +/** Return type for the useUIActions hook */ |
| 10 | +interface UseUIActionsResult { |
| 11 | + /** |
| 12 | + * Sends an intent message to the host. |
| 13 | + * Indicates the user has interacted with the UI and expressed an intent for the host to act on. |
| 14 | + * @param intent - The intent identifier |
| 15 | + * @param params - Optional parameters for the intent |
| 16 | + */ |
| 17 | + intent: <T = unknown>(intent: string, params?: T) => void; |
| 18 | + |
| 19 | + /** |
| 20 | + * Sends a notify message to the host. |
| 21 | + * Indicates the iframe already acted upon user interaction and is notifying the host. |
| 22 | + * @param message - The notification message |
| 23 | + */ |
| 24 | + notify: (message: string) => void; |
| 25 | + |
| 26 | + /** |
| 27 | + * Sends a prompt message to the host. |
| 28 | + * Asks the host to run a prompt. |
| 29 | + * @param prompt - The prompt text to run |
| 30 | + */ |
| 31 | + prompt: (prompt: string) => void; |
| 32 | + |
| 33 | + /** |
| 34 | + * Sends a tool call message to the host. |
| 35 | + * Asks the host to execute a tool. |
| 36 | + * @param toolName - The name of the tool to call |
| 37 | + * @param params - Optional parameters for the tool |
| 38 | + */ |
| 39 | + tool: <T = unknown>(toolName: string, params?: T) => void; |
| 40 | + |
| 41 | + /** |
| 42 | + * Sends a link message to the host. |
| 43 | + * Asks the host to navigate to a URL. |
| 44 | + * @param url - The URL to navigate to |
| 45 | + */ |
| 46 | + link: (url: string) => void; |
| 47 | + |
| 48 | + /** |
| 49 | + * Reports a size change to the host. |
| 50 | + * Used for auto-resizing iframes. |
| 51 | + * @param dimensions - The new dimensions (width and/or height) |
| 52 | + */ |
| 53 | + reportSizeChange: (dimensions: { width?: number; height?: number }) => void; |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Hook for sending UI actions to the parent window via postMessage. |
| 58 | + * This is used by iframe-based UI components to communicate back to an MCP client. |
| 59 | + * |
| 60 | + * Implements the MCP-UI embeddable UI communication protocol. |
| 61 | + * All actions are fire-and-forget - use `useRenderData` to receive responses from the host. |
| 62 | + * |
| 63 | + * @param defaultOptions - Default options applied to all messages |
| 64 | + * @returns An object containing action methods: |
| 65 | + * - intent: Send an intent for the host to act on |
| 66 | + * - notify: Notify the host of something that happened |
| 67 | + * - prompt: Ask the host to run a prompt |
| 68 | + * - tool: Ask the host to run a tool call |
| 69 | + * - link: Ask the host to navigate to a URL |
| 70 | + * - reportSizeChange: Report iframe size changes |
| 71 | + * |
| 72 | + * @example |
| 73 | + * ```tsx |
| 74 | + * function MyComponent() { |
| 75 | + * const { data } = useRenderData<MyData>(); |
| 76 | + * const { intent, tool, link } = useUIActions(); |
| 77 | + * |
| 78 | + * const handleCreateTask = () => { |
| 79 | + * intent("create-task", { title: "Buy groceries" }); |
| 80 | + * }; |
| 81 | + * |
| 82 | + * const handleRefresh = () => { |
| 83 | + * // Ask host to run a tool, host will send new data via render data |
| 84 | + * tool("refresh-data", { id: data?.id }); |
| 85 | + * }; |
| 86 | + * |
| 87 | + * const handleOpenDocs = () => { |
| 88 | + * link("https://docs.example.com"); |
| 89 | + * }; |
| 90 | + * |
| 91 | + * return <button onClick={handleCreateTask}>Create Task</button>; |
| 92 | + * } |
| 93 | + * ``` |
| 94 | + */ |
| 95 | +export function useUIActions(defaultOptions?: SendMessageOptions): UseUIActionsResult { |
| 96 | + const targetOrigin = defaultOptions?.targetOrigin ?? "*"; |
| 97 | + |
| 98 | + const intent = useCallback( |
| 99 | + <T = unknown>(intentName: string, params?: T): void => { |
| 100 | + window.parent.postMessage( |
| 101 | + { |
| 102 | + type: "intent", |
| 103 | + payload: { |
| 104 | + intent: intentName, |
| 105 | + params, |
| 106 | + }, |
| 107 | + }, |
| 108 | + targetOrigin |
| 109 | + ); |
| 110 | + }, |
| 111 | + [targetOrigin] |
| 112 | + ); |
| 113 | + |
| 114 | + const notify = useCallback( |
| 115 | + (message: string): void => { |
| 116 | + window.parent.postMessage( |
| 117 | + { |
| 118 | + type: "notify", |
| 119 | + payload: { |
| 120 | + message, |
| 121 | + }, |
| 122 | + }, |
| 123 | + targetOrigin |
| 124 | + ); |
| 125 | + }, |
| 126 | + [targetOrigin] |
| 127 | + ); |
| 128 | + |
| 129 | + const prompt = useCallback( |
| 130 | + (promptText: string): void => { |
| 131 | + window.parent.postMessage( |
| 132 | + { |
| 133 | + type: "prompt", |
| 134 | + payload: { |
| 135 | + prompt: promptText, |
| 136 | + }, |
| 137 | + }, |
| 138 | + targetOrigin |
| 139 | + ); |
| 140 | + }, |
| 141 | + [targetOrigin] |
| 142 | + ); |
| 143 | + |
| 144 | + const tool = useCallback( |
| 145 | + <T = unknown>(toolName: string, params?: T): void => { |
| 146 | + window.parent.postMessage( |
| 147 | + { |
| 148 | + type: "tool", |
| 149 | + payload: { |
| 150 | + toolName, |
| 151 | + params, |
| 152 | + }, |
| 153 | + }, |
| 154 | + targetOrigin |
| 155 | + ); |
| 156 | + }, |
| 157 | + [targetOrigin] |
| 158 | + ); |
| 159 | + |
| 160 | + const link = useCallback( |
| 161 | + (url: string): void => { |
| 162 | + window.parent.postMessage( |
| 163 | + { |
| 164 | + type: "link", |
| 165 | + payload: { |
| 166 | + url, |
| 167 | + }, |
| 168 | + }, |
| 169 | + targetOrigin |
| 170 | + ); |
| 171 | + }, |
| 172 | + [targetOrigin] |
| 173 | + ); |
| 174 | + |
| 175 | + const reportSizeChange = useCallback( |
| 176 | + (dimensions: { width?: number; height?: number }): void => { |
| 177 | + window.parent.postMessage( |
| 178 | + { |
| 179 | + type: "ui-size-change", |
| 180 | + payload: dimensions, |
| 181 | + }, |
| 182 | + targetOrigin |
| 183 | + ); |
| 184 | + }, |
| 185 | + [targetOrigin] |
| 186 | + ); |
| 187 | + |
| 188 | + return useMemo( |
| 189 | + () => ({ |
| 190 | + intent, |
| 191 | + notify, |
| 192 | + prompt, |
| 193 | + tool, |
| 194 | + link, |
| 195 | + reportSizeChange, |
| 196 | + }), |
| 197 | + [intent, notify, prompt, tool, link, reportSizeChange] |
| 198 | + ); |
| 199 | +} |
0 commit comments