BotDojo LogoInteractive Agent SDKBeta
About BotDojoBotDojo

useMcpApp Hook Guide

Complete guide to building MCP App widgets with the useMcpApp hook.

1. Define Your Tool with _meta

Start by defining your MCP tool with the _meta.ui properties. The _meta field is defined in the MCP specification (SEP-1865):

📄Example: Product Card Tooltypescript
1{
2 name: 'show_product_card',
3 description: 'Display interactive product card',
4 inputSchema: {
5 type: 'object',
6 properties: {
7 productId: { type: 'string' },
8 name: { type: 'string' },
9 price: { type: 'number' },
10 imageUrl: { type: 'string' }
11 },
12 required: ['productId', 'name', 'price']
13 },
14 _meta: {
15 'botdojo/display-name': 'Product Card',
16 'botdojo/no-cache': true, // Disable caching during development
17 ui: {
18 resourceUri: 'ui://my-app/product-card',
19 preferredFrameSize: { width: 600, height: 400 },
20 stateless: false, // Enable state persistence
21 csp: {
22 resourceDomains: ['https://cdn.example.com'], // For images
23 connectDomains: ['https://api.example.com'] // For fetch() calls
24 }
25 }
26 },
27 execute: async (args, context) => {
28 // Optional: Send progress updates
29 context?.notifyToolInputPartial?.({
30 kind: 'botdojo-tool-progress',
31 stepId: 'loading',
32 stepLabel: 'Loading product...'
33 });
34
35 // Do work...
36
37 return { content: [{ type: 'text', text: 'Product card displayed' }] };
38 }
39}

_meta Properties Reference

PropertyDescription
botdojo/display-nameHuman-readable name shown in chat UI
botdojo/hide-stepHide entire step from UI
botdojo/hide-step-detailsShow step name only, hide args/result
botdojo/no-cacheDisable caching (use during development)
ui.resourceUriLocation of MCP App (ui://... or https://...)
ui.preferredFrameSizeInitial iframe size {width, height}
ui.statelessSet to true to disable state persistence
ui.csp.resourceDomainsOrigins allowed for images, fonts, stylesheets
ui.csp.connectDomainsOrigins allowed for fetch/XHR requests

2. Understanding the Lifecycle

When an MCP App loads, it goes through a specific lifecycle:

1
iframe Created
Host creates sandboxed iframe
2
useMcpApp Initializes
Widget calls useMcpApp hook → sends "client-ready"
3
Host Sends ui/initialize
Host sends appInfo, hostContext, capabilities
4
Widget Confirms
Widget sends "initialized" → isInitialized = true
5
Tool Execution Updates
tool-input-partial (0..n) → tool-input → tool-result → status = complete
📄Basic Widget Setuptypescript
1import { useMcpApp } from '@botdojo/chat-sdk/mcp-app-view/react';
2
3function MyWidget() {
4 const containerRef = useRef<HTMLDivElement>(null);
5
6 const {
7 isInitialized, // true after handshake completes
8 appInfo, // Info about this app instance
9 hostContext, // State, theme, viewport, toolInfo
10 tool, // Tool arguments, status, result
11 sendMessage, // Send message to chat
12 callTool, // Call another tool
13 openLink, // Request link open
14 reportSize, // Manual size reporting
15 client // Raw client for advanced use
16 } = useMcpApp({
17 containerRef,
18 autoReportSize: true,
19 onToolInputPartial: (params) => {
20 // Handle streaming updates
21 }
22 });
23
24 if (!isInitialized) {
25 return <div>Loading...</div>;
26 }
27
28 return <div ref={containerRef}>...</div>;
29}

3. Host Context & App Info

appInfo

Contains information about your app instance:

📄appInfo Exampletypescript
1// appInfo structure
2{
3 uri: 'ui://my-app/product-card',
4 name: 'Product Card'
5}

hostContext

Contains state, theme, viewport info, and tool metadata:

📄hostContext Exampletypescript
1// hostContext structure
2{
3 state: {
4 // Your persisted state (see State & Persistence section)
5 counter: 5,
6 userPreferences: { theme: 'dark' }
7 },
8 toolInfo: {
9 tool: {
10 name: 'show_product_card',
11 meta: { /* tool _meta properties */ }
12 }
13 },
14 theme: {
15 // Current theme info
16 },
17 viewport: {
18 width: 1920,
19 height: 1080
20 }
21}

4. Tool Arguments & Results

The tool object provides access to arguments passed when the tool was called and the execution result once the tool completes:

📄Example: Product Card Widgettypescript
1import { useMcpApp } from '@botdojo/chat-sdk/mcp-app-view/react';
2
3function ProductCardWidget() {
4 const { tool, isInitialized } = useMcpApp();
5
6 if (!isInitialized) {
7 return <div>Loading...</div>;
8 }
9
10 // Access tool arguments passed from the tool invocation
11 const { productId, name, price, imageUrl } = tool.arguments || {};
12
13 // Check if tool is still streaming updates
14 const isLoading = tool.status === 'streaming';
15 const isComplete = tool.status === 'complete';
16
17 // Access the tool result (if available)
18 const result = tool.result;
19
20 return (
21 <div>
22 <h2>{name || 'Product'}</h2>
23 <p>ID: {productId}</p>
24 <p>Price: ${price || 0}</p>
25
26 {isLoading && <div>Processing...</div>}
27 {isComplete && result && (
28 <div>{result.message || 'Complete'}</div>
29 )}
30
31 {imageUrl && <img src={imageUrl} alt={name} />}
32 </div>
33 );
34}

Tool Object Structure

📄Tool Object Typetypescript
1// The tool object structure:
2{
3 name: 'show_product_card', // Tool name
4 arguments: { // Arguments passed to the tool
5 productId: '123',
6 name: 'Widget',
7 price: 49.99,
8 imageUrl: 'https://...'
9 },
10 partialUpdate: null, // Streaming argument updates (LLM partial JSON)
11 toolProgress: { // Progress notifications during execution
12 kind: 'botdojo-tool-progress',
13 stepId: 'loading',
14 stepLabel: 'Loading product...',
15 percent: 50
16 },
17 result: { // Result after tool execution completes
18 content: [{ type: 'text', text: 'Product card displayed' }]
19 },
20 status: 'complete', // 'idle' | 'streaming' | 'complete' | 'error'
21 isStreaming: false // Convenience flag for streaming state
22}

5. State & Persistence

MCP Apps can persist state that survives page refreshes, navigation, and chat session reopening. This is critical for remembering user actions and preferences.

Hydration vs First Load

  • First Load: User triggers tool for the first time → hostContext.state is empty or has defaults
  • Hydration: User returns to existing chat or refreshes page → hostContext.state contains previously saved data
📄Reading Persisted State (Hydration)typescript
1const { hostContext } = useMcpApp();
2const [counter, setCounter] = useState(0);
3const [clickedItems, setClickedItems] = useState<Set<string>>(new Set());
4
5// On mount, restore state from hostContext
6useEffect(() => {
7 if (hostContext?.state) {
8 // Restore counter
9 if (typeof hostContext.state.counter === 'number') {
10 setCounter(hostContext.state.counter);
11 }
12
13 // Restore clicked items
14 if (Array.isArray(hostContext.state.clickedItems)) {
15 setClickedItems(new Set(hostContext.state.clickedItems));
16 }
17 }
18}, [hostContext]);
📄Persisting Statetypescript
1const { client } = useMcpApp();
2
3const saveState = async (newCounter: number, clicked: Set<string>) => {
4 await client.sendRequest('ui/message', {
5 role: 'user',
6 content: {
7 type: 'botdojo/persist',
8 state: {
9 counter: newCounter,
10 clickedItems: Array.from(clicked) // Convert Set to Array
11 }
12 }
13 });
14};
15
16// Example: Save when user clicks a button
17const handleClick = async () => {
18 const newCounter = counter + 1;
19 setCounter(newCounter);
20
21 await saveState(newCounter, clickedItems);
22};

Use cases for state persistence: Remember which items a user clicked, save form data, remember UI preferences (active tab, collapsed sections), track user progress through a workflow.

6. Streaming & Progress Updates

Your widget can receive streaming progress updates while the tool is executing:

📄In Your Tool: Send Progresstypescript
1execute: async (args, context) => {
2 // Send progress notification
3 context?.notifyToolInputPartial?.({
4 kind: 'botdojo-tool-progress',
5 stepId: 'step-1',
6 stepLabel: 'Loading product data...',
7 percent: 33
8 });
9
10 // Do some work...
11 await fetchProductData();
12
13 context?.notifyToolInputPartial?.({
14 kind: 'botdojo-tool-progress',
15 stepId: 'step-2',
16 stepLabel: 'Processing images...',
17 percent: 66
18 });
19
20 // More work...
21
22 return { content: [{ type: 'text', text: 'Complete!' }] };
23}
📄In Your Widget: Receive Progresstypescript
1const [progress, setProgress] = useState<{
2 stepId?: string;
3 stepLabel?: string;
4 percent?: number;
5} | null>(null);
6
7const { tool } = useMcpApp({
8 onToolInputPartial: (params) => {
9 if (params.arguments?.kind === 'botdojo-tool-progress') {
10 setProgress({
11 stepId: params.arguments.stepId,
12 stepLabel: params.arguments.stepLabel,
13 percent: params.arguments.percent
14 });
15 }
16 }
17});
18
19const isStreaming = tool.isStreaming;
20const isComplete = tool.status === 'complete' || tool.status === 'idle';
21
22return (
23 <div>
24 {isStreaming && progress && (
25 <div>
26 <div>{progress.stepLabel}</div>
27 <ProgressBar percent={progress.percent ?? 0} />
28 </div>
29 )}
30 {isComplete && <div>✅ Complete!</div>}
31 </div>
32);

7. MCP App Actions

Your widget can perform actions that communicate with the host:

sendMessage()

📄Codetypescript
1const { sendMessage } = useMcpApp();
2
3// Send a message to the chat
4await sendMessage([
5 { type: 'text', text: 'User clicked the Save button!' }
6]);

callTool()

📄Codetypescript
1const { callTool } = useMcpApp();
2
3// Call another MCP tool
4const result = await callTool('save_product', {
5 productId: '123',
6 data: { name: 'Widget', price: 49.99 }
7});
8
9console.log('Tool result:', result);

openLink()

📄Codetypescript
1const { openLink } = useMcpApp();
2
3// Request to open an external link
4// Host will prompt user for confirmation
5await openLink('https://docs.botdojo.com');

reportSize()

📄Codetypescript
1const { reportSize } = useMcpApp();
2
3// Manually report size changes
4// (Usually not needed with autoReportSize: true)
5reportSize(600, 400);

8. Caching

When using BotDojoChat, provide a cacheKey prop to enable MCP App HTML caching. Without it, caching is disabled and performance suffers.

📄Enable Cachingtypescript
1<BotDojoChat
2 apiKey="your-api-key"
3 cacheKey="user-123-flow-abc" // Unique ID for your agent/flow instance
4 modelContext={modelContext}
5 // ... other props
6/>

How caching works: The cacheKey is a unique identifier for your agent or flow instance. It's combined with the MCP App resource URI (e.g., ui://my-app/widget) to create a unique cache entry. This prevents MCP App HTML from being fetched repeatedly, improving load times significantly.

Note: Inline HTML (delivered via text/html;profile=mcp-app MIME type) is cached by the proxy but won't benefit from traditional browser caching like remote URLs.

During development: Use botdojo/no-cache: true in your tool _meta to disable caching and see changes immediately.

9. Complete Example

See the Tool Progress example for a complete working implementation that demonstrates:

  • Streaming progress updates
  • State persistence (counter that survives refresh)
  • All MCP actions (sendMessage, callTool, openLink)
  • Hydration (restoring state on reload)