Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ui/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { useRenderData } from "./useRenderData.js";
export { useHostCommunication } from "./useHostCommunication.js";
140 changes: 140 additions & 0 deletions src/ui/hooks/useHostCommunication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { useCallback, useMemo } from "react";

interface SendMessageOptions {
targetOrigin?: string;
}

/** Return type for the useHostCommunication hook */
interface UseHostCommunicationResult {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are any of these types available from mcp-ui/server ?

/** Sends an intent message for the host to act on */
intent: <T = unknown>(intent: string, params?: T) => void;
/** Notifies the host of something that happened */
notify: (message: string) => void;
/** Asks the host to run a prompt */
prompt: (prompt: string) => void;
/** Asks the host to execute a tool */
tool: <T = unknown>(toolName: string, params?: T) => void;
/** Asks the host to navigate to a URL */
link: (url: string) => void;
/** Reports iframe size changes to the host */
reportSizeChange: (dimensions: { width?: number; height?: number }) => void;
}

/**
* Hook for sending UI actions to the parent window via postMessage
* This is used by iframe-based UI components to communicate back to an MCP client
*
* @example
* ```tsx
* function MyComponent() {
* const { intent, tool, link } = useHostCommunication();
*
* return <button onClick={() => intent("create-task", { title: "Buy groceries" })}>Create Task</button>;
* }
* ```
*/
export function useHostCommunication(defaultOptions?: SendMessageOptions): UseHostCommunicationResult {
const targetOrigin = defaultOptions?.targetOrigin ?? "*";

const intent = useCallback(
<T = unknown>(intentName: string, params?: T): void => {
window.parent.postMessage(
{
type: "intent",
payload: {
intent: intentName,
params,
},
},
targetOrigin
);
},
[targetOrigin]
);

const notify = useCallback(
(message: string): void => {
window.parent.postMessage(
{
type: "notify",
payload: {
message,
},
},
targetOrigin
);
},
[targetOrigin]
);

const prompt = useCallback(
(promptText: string): void => {
window.parent.postMessage(
{
type: "prompt",
payload: {
prompt: promptText,
},
},
targetOrigin
);
},
[targetOrigin]
);

const tool = useCallback(
<T = unknown>(toolName: string, params?: T): void => {
window.parent.postMessage(
{
type: "tool",
payload: {
toolName,
params,
},
},
targetOrigin
);
},
[targetOrigin]
);

const link = useCallback(
(url: string): void => {
window.parent.postMessage(
{
type: "link",
payload: {
url,
},
},
targetOrigin
);
},
[targetOrigin]
);

const reportSizeChange = useCallback(
(dimensions: { width?: number; height?: number }): void => {
window.parent.postMessage(
{
type: "ui-size-change",
payload: dimensions,
},
targetOrigin
);
},
[targetOrigin]
);

return useMemo(
() => ({
intent,
notify,
prompt,
tool,
link,
reportSizeChange,
}),
[intent, notify, prompt, tool, link, reportSizeChange]
);
Comment on lines +129 to +139
Copy link
Collaborator

@TheSonOfThomp TheSonOfThomp Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this useMemo is necessary, especially since each entry is wrapped in a useCallback

}
12 changes: 10 additions & 2 deletions src/ui/hooks/useRenderData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface UseRenderDataResult<T> {
data: T | null;
isLoading: boolean;
error: string | null;
/** The origin of the parent window, captured from the first valid message */
parentOrigin: string | null;
}

/**
Expand All @@ -24,6 +26,7 @@ interface UseRenderDataResult<T> {
* - data: The received render data (or null if not yet received)
* - isLoading: Whether data is still being loaded
* - error: Error message if message validation failed
* - parentOrigin: The origin of the parent window (for secure postMessage calls)
*
* @example
* ```tsx
Expand All @@ -32,15 +35,17 @@ interface UseRenderDataResult<T> {
* }
*
* function MyComponent() {
* const { data, isLoading, error } = useRenderData<MyData>();
* // ...
* const { data, isLoading, error, parentOrigin } = useRenderData<MyData>();
* const { intent } = useHostCommunication({ targetOrigin: parentOrigin ?? undefined });
* return <button onClick={() => intent("my-action")}>Click</button>;
* }
* ```
*/
export function useRenderData<T = unknown>(): UseRenderDataResult<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [parentOrigin, setParentOrigin] = useState<string | null>(null);

useEffect(() => {
const handleMessage = (event: MessageEvent<RenderDataMessage>): void => {
Expand All @@ -49,6 +54,8 @@ export function useRenderData<T = unknown>(): UseRenderDataResult<T> {
return;
}

setParentOrigin((current) => current ?? event.origin);

if (!event.data.payload || typeof event.data.payload !== "object") {
const errorMsg = "Invalid payload structure received";
setError(errorMsg);
Expand Down Expand Up @@ -88,5 +95,6 @@ export function useRenderData<T = unknown>(): UseRenderDataResult<T> {
data,
isLoading,
error,
parentOrigin,
};
}
Loading