Overview
WebMCP tools can be called directly by AI agents running in your frontend application. This allows you to build AI copilots and assistants that can interact with your website’s functionality through registered MCP tools.
Frontend AI frameworks like Assistant-UI and AG-UI provide runtimes that support this pattern.
Register tools on your website
Tools are defined and registered on your website using navigator.modelContext
Connect an MCP client
Your AI runtime connects to the website’s tools using an MCP client
Register tools with AI runtime
The MCP tools are registered with your frontend AI framework (Assistant-UI, AG-UI, etc.)
AI calls tools
When the AI needs to use a tool, it calls through your runtime → MCP client → WebMCP
Tool executes on frontend
The tool runs in the browser, returns results to the AI
Installation
Install the required packages:
npm install @mcp-b/react-webmcp @mcp-b/transports @modelcontextprotocol/sdk
yarn add @mcp-b/react-webmcp @mcp-b/transports @modelcontextprotocol/sdk
pnpm add @mcp-b/react-webmcp @mcp-b/transports @modelcontextprotocol/sdk
Basic Example
First, register tools that your AI can call using the useWebMCP hook:
import { useWebMCP } from '@mcp-b/react-webmcp' ;
import { z } from 'zod' ;
function ShoppingCart () {
// Register a tool
useWebMCP ({
name: 'add_to_cart' ,
description: 'Add a product to the shopping cart' ,
inputSchema: {
productId: z . string (),
quantity: z . number (). min ( 1 )
},
handler : async ( input ) => {
// Add to cart logic
const cart = await addToCart ( input . productId , input . quantity );
return {
message: `Added ${ input . quantity } x product ${ input . productId } to cart` ,
cart
};
}
});
return < div > { /* Your cart UI */ } </ div > ;
}
The useWebMCP hook automatically handles tool registration and cleanup when the component unmounts.
2. Set Up MCP Client with React
Use the McpClientProvider to connect to your website’s tools:
import { McpClientProvider , useMcpClient } from '@mcp-b/react-webmcp' ;
import { Client } from '@modelcontextprotocol/sdk/client/index.js' ;
import { TabClientTransport } from '@mcp-b/transports' ;
// Create client and transport
const client = new Client ({
name: 'MyAIAssistant' ,
version: '1.0.0'
});
const transport = new TabClientTransport ( 'mcp' , {
clientInstanceId: 'my-ai-assistant'
});
// Wrap your app with the provider
function App () {
return (
< McpClientProvider client = { client } transport = { transport } >
< AIAssistant />
</ McpClientProvider >
);
}
// Use the client in your components
function AIAssistant () {
const { client , tools , isConnected , isLoading } = useMcpClient ();
if ( isLoading ) return < div > Connecting... </ div > ;
if ( ! isConnected ) return < div > Not connected </ div > ;
return (
< div >
< p > Available tools: { tools . length } </ p >
< ul >
{ tools . map ( tool => (
< li key = { tool . name } > { tool . name } - { tool . description } </ li >
)) }
</ ul >
</ div >
);
}
3. Register with AI Runtime
Register the MCP tools with your AI framework:
Assistant-UI
AG-UI
Custom Runtime
import { useEffect } from 'react' ;
import { useMcpClient } from '@mcp-b/react-webmcp' ;
import { tool , useAssistantRuntime } from '@assistant-ui/react' ;
function useWebMCPTools () {
const { client , tools , isConnected } = useMcpClient ();
const runtime = useAssistantRuntime ();
useEffect (() => {
if ( ! isConnected || tools . length === 0 ) return ;
// Convert MCP tools to Assistant-UI format
const assistantTools = tools . map ( mcpTool =>
tool ({
type: 'frontend' ,
description: mcpTool . description ,
parameters: mcpTool . inputSchema ,
execute : async ( args ) => {
// Call through MCP client
const result = await client . callTool ({
name: mcpTool . name ,
arguments: args
});
// Return text content
return result . content
. filter ( c => c . type === 'text' )
. map ( c => c . text )
. join ( ' \n ' );
}
})
);
// Register with Assistant-UI runtime
const unregister = runtime . registerModelContextProvider ({
getModelContext : () => ({
tools: Object . fromEntries (
tools . map (( t , i ) => [ t . name , assistantTools [ i ]])
)
})
});
return () => unregister ();
}, [ client , tools , isConnected , runtime ]);
}
import { useMemo } from 'react' ;
import { useMcpClient } from '@mcp-b/react-webmcp' ;
import { createAgent } from '@ag-ui/core' ;
function MyAGAssistant () {
const { client , tools , isConnected } = useMcpClient ();
const agent = useMemo (() => {
if ( ! isConnected || tools . length === 0 ) return null ;
return createAgent ({
tools: tools . map ( mcpTool => ({
name: mcpTool . name ,
description: mcpTool . description ,
parameters: mcpTool . inputSchema ,
execute : async ( args ) => {
const result = await client . callTool ({
name: mcpTool . name ,
arguments: args
});
return result . content
. filter ( c => c . type === 'text' )
. map ( c => c . text )
. join ( ' \n ' );
}
}))
});
}, [ client , tools , isConnected ]);
return agent ;
}
import { useMcpClient } from '@mcp-b/react-webmcp' ;
function MyCustomAssistant () {
const { client , tools , isConnected } = useMcpClient ();
const callTool = async ( toolName : string , args : any ) => {
if ( ! isConnected ) {
throw new Error ( 'MCP client not connected' );
}
const result = await client . callTool ({
name: toolName ,
arguments: args
});
// Extract text from MCP response
return result . content
. filter ( c => c . type === 'text' )
. map ( c => c . text )
. join ( ' \n ' );
};
const handleAddToCart = async () => {
const response = await callTool ( 'add_to_cart' , {
productId: 'ABC123' ,
quantity: 2
});
console . log ( response );
};
return (
< button onClick = { handleAddToCart } disabled = { ! isConnected } >
Add to Cart
</ button >
);
}
Complete Example with React
Here’s a full example showing tool registration, MCP client setup, and AI runtime integration:
import { useEffect , useState } from 'react' ;
import { McpClientProvider , useMcpClient , useWebMCP } from '@mcp-b/react-webmcp' ;
import { Client } from '@modelcontextprotocol/sdk/client/index.js' ;
import { TabClientTransport } from '@mcp-b/transports' ;
import { tool , useAssistantRuntime } from '@assistant-ui/react' ;
import { z } from 'zod' ;
// Create client and transport outside component
const client = new Client ({
name: 'MyAssistant' ,
version: '1.0.0'
});
const transport = new TabClientTransport ( 'mcp' , {
clientInstanceId: 'my-assistant'
});
// Main app with provider
export function App () {
return (
< McpClientProvider client = { client } transport = { transport } >
< MyAIAssistant />
</ McpClientProvider >
);
}
// Tool provider component
function ToolProvider () {
const [ counter , setCounter ] = useState ( 0 );
// Register a tool using useWebMCP hook
useWebMCP ({
name: 'get_user_info' ,
description: 'Get current user information' ,
inputSchema: {},
handler : async () => {
const user = getCurrentUser (); // Your app logic
return { user };
}
});
// Register a counter tool
useWebMCP ({
name: 'increment_counter' ,
description: 'Increment the counter' ,
inputSchema: {
amount: z . number (). min ( 1 ). default ( 1 )
},
handler : async ( input ) => {
setCounter ( prev => prev + input . amount );
return { counter: counter + input . amount };
}
});
return null ; // This component just registers tools
}
// AI Assistant component
function MyAIAssistant () {
const { client , tools , isConnected , isLoading } = useMcpClient ();
const runtime = useAssistantRuntime ();
// Register MCP tools with Assistant-UI runtime
useEffect (() => {
if ( ! isConnected || tools . length === 0 ) return ;
const assistantTools = tools . map ( mcpTool =>
tool ({
type: 'frontend' ,
description: mcpTool . description ,
parameters: mcpTool . inputSchema ,
execute : async ( args ) => {
const result = await client . callTool ({
name: mcpTool . name ,
arguments: args
});
return result . content
. filter ( c => c . type === 'text' )
. map ( c => c . text )
. join ( ' \n ' );
}
})
);
const unregister = runtime . registerModelContextProvider ({
getModelContext : () => ({
tools: Object . fromEntries (
tools . map (( t , i ) => [ t . name , assistantTools [ i ]])
)
})
});
return () => unregister ();
}, [ client , tools , isConnected , runtime ]);
if ( isLoading ) return < div > Connecting to WebMCP... </ div > ;
if ( ! isConnected ) return < div > Not connected </ div > ;
return (
< div >
< ToolProvider />
< p > Available tools: { tools . length } </ p >
{ /* Your AI chat UI here */ }
</ div >
);
}
Framework Support
WebMCP tools work with any frontend AI framework that supports tool calling:
Key Concepts
With WebMCP frontend tool calling, tools execute in the browser :
✅ Direct access to DOM, localStorage, app state
✅ No server roundtrip required
✅ Works offline
✅ Real-time UI updates
MCP Client as Bridge
The MCP client acts as a bridge between your AI runtime and WebMCP:
AI Runtime → MCP Client → WebMCP → Tool Function → Result
Tools can be registered/unregistered dynamically, and the AI runtime stays synchronized:
// Register a new tool
const registration = navigator . modelContext . registerTool ({
name: 'new_feature' ,
description: 'A new feature' ,
inputSchema: { type: 'object' , properties: {} },
async execute () {
return { content: [{ type: 'text' , text: 'Done!' }] };
}
});
// Tool is immediately available to the AI
// Later, remove it
registration . unregister ();
Best Practices
Use the onError callback to handle errors: useWebMCP ({
name: 'delete_item' ,
description: 'Delete an item' ,
inputSchema: {
itemId: z . string ()
},
handler : async ( input ) => {
// Errors thrown here are automatically caught
const result = await performAction ( input . itemId );
return { success: true , itemId: input . itemId };
},
onError : ( error , input ) => {
// Log or handle the error
console . error ( 'Tool failed:' , error . message , 'for input:' , input );
// Optionally show UI notification
showToast ( `Failed to delete item: ${ error . message } ` );
}
});
Return structured content
The useWebMCP hook automatically formats your return value: useWebMCP ({
name: 'get_cart' ,
description: 'Get shopping cart' ,
inputSchema: {},
handler : async () => {
const cart = await getCart ();
// Return structured data - automatically formatted
return {
items: cart . items ,
total: cart . total ,
itemCount: cart . items . length
};
},
// Custom formatter for text representation
formatOutput : ( output ) => {
return `Cart has ${ output . itemCount } items (total: $ ${ output . total } )` ;
}
});
Next Steps