@fractal-mcp/mcp-ui-hooks
v2.2.5
Published
React hooks for MCP-UI cross-iframe messaging
Readme
@fractal-mcp/mcp-ui-hooks
React hooks for MCP UI compatible cross-iframe messaging. Makes it dead simple to build embeddable UIs.
Note: This package is not directly compatible with OpenAI Apps SDK. For OpenAI Apps, use
@fractal-mcp/oai-hooksinstead.
Installation
npm install @fractal-mcp/mcp-ui-hooksQuick Start
import { useUIMessenger } from '@fractal-mcp/mcp-ui-hooks';
function App() {
const { ready, renderData, emitIntent } = useUIMessenger();
if (!ready) return <div>Loading...</div>;
return (
<div>
<h1>Theme: {renderData?.theme}</h1>
<button onClick={() => emitIntent({
intent: 'button-clicked',
params: { timestamp: Date.now() }
})}>
Click Me
</button>
</div>
);
}That's it! The hook handles initialization, render data, and cleanup automatically.
API
useUIMessenger(options?)
Returns an object with messaging functions and state.
const {
ready, // boolean - messenger is initialized
renderData, // Record<string, unknown> - data from host
emitIntent, // Send intent (fire-and-forget)
emitNotify, // Send notification
emitPrompt, // Ask host to run prompt
emitTool, // Ask host to call tool
emitLink, // Ask host to navigate
requestIntent, // Send intent and wait for response
requestNotify, // Send notification and wait for response
requestPrompt, // Send prompt and wait for response
requestTool, // Send tool call and wait for response ()
requestLink, // Send link and wait for response
requestData, // Request custom data from host
} = useUIMessenger({ forceWaitForRenderData: true });Options
{
forceWaitForRenderData?: boolean; // Wait for render data even without URL param
}Usage Examples
Send Intent (Fire-and-Forget)
function TaskCreator() {
const { emitIntent } = useUIMessenger();
const createTask = () => {
emitIntent({
intent: 'create-task',
params: {
title: 'Buy groceries',
priority: 'high'
}
});
};
return <button onClick={createTask}>Create Task</button>;
}Request with Response (Basic)
function WeatherWidget() {
const { requestTool } = useUIMessenger();
const [weather, setWeather] = useState(null);
const getWeather = async () => {
const req = await requestTool({
toolName: 'get-weather',
params: { city: 'Tokyo' }
});
const result = await req.response();
setWeather(result);
};
return (
<div>
<button onClick={getWeather}>Get Weather</button>
{weather && <div>{JSON.stringify(weather)}</div>}
</div>
);
}Request with Response (Advanced: Tracking State)
Track the request lifecycle to show loading states:
function WeatherWidget() {
const { requestTool } = useUIMessenger();
const [status, setStatus] = useState('idle');
const [weather, setWeather] = useState(null);
const getWeather = async () => {
setStatus('sending');
const req = await requestTool({
toolName: 'get-weather',
params: { city: 'Tokyo' }
});
// Wait for acknowledgment from host
req.received().then(() => {
setStatus('processing'); // Host acknowledged, now processing
});
// Wait for the final response
try {
const result = await req.response();
setStatus('success');
setWeather(result);
} catch (error) {
setStatus('error');
}
};
return (
<div>
<button onClick={getWeather} disabled={status !== 'idle' && status !== 'success' && status !== 'error'}>
Get Weather
</button>
{status === 'sending' && <p>Sending request...</p>}
{status === 'processing' && <p>Host is processing...</p>}
{status === 'success' && <p>Weather: {JSON.stringify(weather)}</p>}
{status === 'error' && <p>Failed to get weather</p>}
</div>
);
}The RpcRequest object provides:
received(): Promise that resolves when host sendsui-message-received(acknowledgment)response(): Promise that resolves when host sendsui-message-response(final result)
See the mcp-ui-messenger automatic behaviors documentation for more details.
Notify Host of State Changes
function ShoppingCart() {
const { emitNotify } = useUIMessenger();
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems([...items, item]);
emitNotify({ message: 'cart-updated' });
};
return <div>{/* cart UI */}</div>;
}Request Custom Data
function PaymentMethods() {
const { requestData } = useUIMessenger();
const [methods, setMethods] = useState([]);
useEffect(() => {
requestData({
requestType: 'get-payment-methods',
params: { userId: '123' }
}).then(req => req.response())
.then(setMethods);
}, []);
return <ul>{methods.map(m => <li key={m.id}>{m.name}</li>)}</ul>;
}Use Render Data
function ThemedApp() {
const { renderData } = useUIMessenger();
const theme = renderData?.theme || 'light';
const locale = renderData?.locale || 'en';
return (
<div className={`theme-${theme}`} lang={locale}>
{/* your app */}
</div>
);
}Ask Host to Run a Prompt
function AIAssistant() {
const { requestPrompt } = useUIMessenger();
const askAI = async () => {
const req = await requestPrompt({
prompt: 'What is the weather in Tokyo?'
});
const response = await req.response();
console.log('AI response:', response);
};
return <button onClick={askAI}>Ask AI</button>;
}Alias: useFractal
For convenience, useFractal is exported as an alias:
import { useFractal } from '@fractal-mcp/mcp-ui-hooks';
function App() {
const { ready, emitIntent } = useFractal();
// ... same as useUIMessenger
}Message Types
Fire-and-Forget (emit)
emitIntent({ intent, params })- User expressed an intentemitNotify({ message })- Notify host of state changeemitPrompt({ prompt })- Ask host to run a promptemitTool({ toolName, params })- Ask host to call a toolemitLink({ url })- Ask host to navigate
Request-Response
requestIntent()- Send intent and get responserequestNotify()- Send notification and get responserequestPrompt()- Send prompt and get responserequestTool()- Send tool call and get responserequestLink()- Send link request and get responserequestData({ requestType, params })- Request custom data
TypeScript Types
All payload types are re-exported from @fractal-mcp/mcp-ui-messenger:
interface IntentPayload {
intent: string;
params?: Record<string, unknown>;
}
interface NotifyPayload {
message: string;
}
interface PromptPayload {
prompt: string;
}
interface ToolPayload {
toolName: string;
params?: Record<string, unknown>;
}
interface LinkPayload {
url: string;
}
interface RequestDataPayload {
requestType: string;
params?: Record<string, unknown>;
}How It Works
- Hook calls
initUIMessenger()on mount - Automatically handles iframe lifecycle (ready event, render data)
- Provides ready state and render data via hook return
- All messaging functions wait for initialization before sending
Automatic Message Handling
The underlying messenger automatically handles MCP-UI protocol details for you:
- Size changes: Automatically reported to the host via
ui-size-changeevents - Request tracking:
request*methods automatically handleui-message-receivedandui-message-responseevents using message IDs
For more details, see the mcp-ui-messenger automatic behaviors documentation.
Advanced: Init Before React
If you need render data before React mounts, use the base package:
import { initUIMessenger } from '@fractal-mcp/mcp-ui-hooks';
const messenger = await initUIMessenger({ rootElId: 'root' });
const renderData = messenger.getRenderData();
root.render(<App messenger={messenger} renderData={renderData} />);Then in your components, just call useUIMessenger() and it will use the existing instance.
Protocol
Implements the MCP-UI Embeddable UI Protocol.
License
See repository root for license information.
