Frontend MCP
Turn your frontend into a secure MCP server. Give AI agents visibility into what users see and the ability to take actions on their behalf.
What is Frontend MCP?
MCP (Model Context Protocol) is a standard for connecting AI agents to external data and tools. Frontend MCP lets you run an MCP-like server directly in the browser, giving agents secure access to your UI state.
Defining a ModelContext
A ModelContext defines what the AI agent can see (resources) and do (tools) in your application.
1import { useMemo } from 'react';2import type { ModelContext } from '@botdojo/chat-sdk';34function MyComponent() {5 // Define your ModelContext (Frontend MCP)6 const modelContext: ModelContext = useMemo(() => ({7 name: 'my_app',8 description: 'Frontend MCP for my application',9 toolPrefix: 'app',10 uri: 'app://context',1112 resources: [13 // Resources define what the agent can "see"14 ],1516 tools: [17 // Tools define what the agent can "do"18 ],19 }), []);2021 // Pass modelContext to BotDojoChat (widget) OR BotDojoChatProvider (headless)22 return null;23}
Using ModelContext with Chat
The same modelContext can be used with the drop-in widget (BotDojoChat) or the headless provider (BotDojoChatProvider).
1import { useMemo, useRef, useEffect, useState } from 'react';2import { BotDojoChat, type ModelContext } from '@botdojo/chat-sdk';34export default function WidgetWithFrontendMcp() {5 const [text, setText] = useState('Hello');6 const textRef = useRef(text);78 useEffect(() => { textRef.current = text; }, [text]);910 const modelContext = useMemo<ModelContext>(() => ({11 name: 'demo',12 description: 'Frontend MCP demo for BotDojoChat',13 toolPrefix: 'demo',14 uri: 'demo://context',15 resources: [16 {17 uri: 'demo://text',18 name: 'Current Text',19 description: 'Current UI state',20 mimeType: 'application/json',21 getContent: async () => ({22 uri: 'demo://text',23 mimeType: 'application/json',24 text: JSON.stringify({ text: textRef.current }),25 }),26 },27 ],28 tools: [29 {30 name: 'setText',31 description: 'Update UI state',32 inputSchema: {33 type: 'object',34 properties: { text: { type: 'string' } },35 required: ['text'],36 },37 execute: async ({ text }: { text: string }) => {38 setText(text);39 return { success: true };40 },41 },42 ],43 }), []);4445 return (46 <div>47 <div style={{ marginBottom: 8, fontWeight: 700 }}>UI text: {text}</div>48 <BotDojoChat49 apiKey={token} // From useTemporaryToken() hook50 baseUrl={process.env.NEXT_PUBLIC_IFRAME_URL || 'https://embed.botdojo.com'}51 mode="inline"52 modelContext={modelContext}53 />54 </div>55 );56}
1import { useMemo, useRef, useEffect, useState } from 'react';2import {3 BotDojoChatProvider,4 useChatMessages,5 useChatActions,6 useChatStatus,7 type ModelContext,8} from '@botdojo/chat-sdk';910function MinimalHeadlessUi() {11 const { messages, isStreaming } = useChatMessages();12 const { sendMessage } = useChatActions();13 const { isReady } = useChatStatus();14 const [input, setInput] = useState('');1516 return (17 <div style={{ border: '1px solid #e2e8f0', borderRadius: 12, overflow: 'hidden' }}>18 <div style={{ padding: 12, borderBottom: '1px solid #e2e8f0', fontWeight: 700 }}>19 Headless Chat {isReady ? '' : '(connecting...)'} {isStreaming ? '(streaming...)' : ''}20 </div>21 <div style={{ padding: 12, height: 240, overflow: 'auto' }}>22 {messages.map((m) => (23 <div key={m.id} style={{ marginBottom: 8 }}>24 <strong>{m.role}:</strong> {m.content}25 </div>26 ))}27 </div>28 <form29 onSubmit={(e) => {30 e.preventDefault();31 if (!input.trim()) return;32 sendMessage(input.trim());33 setInput('');34 }}35 style={{ display: 'flex', gap: 8, padding: 12, borderTop: '1px solid #e2e8f0' }}36 >37 <input38 value={input}39 onChange={(e) => setInput(e.target.value)}40 placeholder="Type a message..."41 style={{ flex: 1, padding: 10, borderRadius: 8, border: '1px solid #e2e8f0' }}42 />43 <button type="submit" style={{ padding: '10px 14px', borderRadius: 8, border: 'none', background: '#6366f1', color: 'white', fontWeight: 700 }}>44 Send45 </button>46 </form>47 </div>48 );49}5051export default function HeadlessWithFrontendMcp() {52 const [text, setText] = useState('Hello');53 const textRef = useRef(text);54 useEffect(() => { textRef.current = text; }, [text]);5556 const modelContext = useMemo<ModelContext>(() => ({57 name: 'demo',58 description: 'Frontend MCP demo for BotDojoChatProvider',59 toolPrefix: 'demo',60 uri: 'demo://context',61 resources: [62 {63 uri: 'demo://text',64 name: 'Current Text',65 description: 'Current UI state',66 mimeType: 'application/json',67 getContent: async () => ({68 uri: 'demo://text',69 mimeType: 'application/json',70 text: JSON.stringify({ text: textRef.current }),71 }),72 },73 ],74 tools: [75 {76 name: 'setText',77 description: 'Update UI state',78 inputSchema: {79 type: 'object',80 properties: { text: { type: 'string' } },81 required: ['text'],82 },83 execute: async ({ text }: { text: string }) => {84 setText(text);85 return { success: true };86 },87 },88 ],89 }), []);9091 return (92 <div>93 <div style={{ marginBottom: 8, fontWeight: 700 }}>UI text: {text}</div>94 <BotDojoChatProvider95 apiKey={token} // From useTemporaryToken() hook96 baseUrl={process.env.NEXT_PUBLIC_IFRAME_URL || 'https://embed.botdojo.com'}97 modelContext={modelContext}98 >99 <MinimalHeadlessUi />100 </BotDojoChatProvider>101 </div>102 );103}
👁️ Defining Resources
Resources let the agent "see" your UI state. Define what data the agent can access.
1resources: [2 {3 uri: 'app://user-data',4 name: 'User Data',5 description: 'Current user profile and preferences',6 mimeType: 'application/json',7 getContent: async () => ({8 uri: 'app://user-data',9 mimeType: 'application/json',10 text: JSON.stringify({11 name: 'John Doe',12 preferences: { theme: 'dark' }13 }),14 }),15 },16]
🔧 Defining Tools
Tools let the agent take actions. The execute function runs in your frontend with access to your React state.
1tools: [2 {3 name: 'updateTheme',4 description: 'Change the application theme',5 inputSchema: {6 type: 'object',7 properties: {8 theme: {9 type: 'string',10 enum: ['light', 'dark'],11 description: 'The theme to apply'12 },13 },14 required: ['theme'],15 },16 execute: async (params: { theme: 'light' | 'dark' }) => {17 // This runs in your frontend!18 setTheme(params.theme);19 return {20 success: true,21 message: `Theme changed to ${params.theme}`22 };23 },24 _meta: {25 'botdojo/display-name': 'Change Theme',26 },27 },28]
Complete Example
Here's a complete example putting it all together:
1import { useMemo, useState, useRef, useEffect } from 'react';2import { BotDojoChat, type ModelContext } from '@botdojo/chat-sdk';34function CounterApp() {5 const [count, setCount] = useState(0);6 const countRef = useRef(count);78 useEffect(() => {9 countRef.current = count;10 }, [count]);1112 const modelContext: ModelContext = useMemo(() => ({13 name: 'counter',14 description: 'A simple counter with AI control',15 toolPrefix: 'counter',16 uri: 'counter://context',1718 resources: [19 {20 uri: 'counter://value',21 name: 'Counter Value',22 description: 'The current counter value',23 mimeType: 'application/json',24 getContent: async () => ({25 uri: 'counter://value',26 mimeType: 'application/json',27 text: JSON.stringify({ value: countRef.current }),28 }),29 },30 ],3132 tools: [33 {34 name: 'getCount',35 description: 'Get the current counter value',36 inputSchema: {37 type: 'object',38 properties: {39 go: { type: 'boolean', description: 'Pass true' },40 },41 },42 execute: async () => ({ count: countRef.current }),43 },44 {45 name: 'increment',46 description: 'Increase the counter by a specified amount',47 inputSchema: {48 type: 'object',49 properties: {50 amount: { type: 'number', description: 'Amount to add' },51 },52 required: ['amount'],53 },54 execute: async ({ amount }: { amount: number }) => {55 setCount(prev => prev + amount);56 return { success: true, newValue: countRef.current + amount };57 },58 },59 ],60 }), []);6162 return (63 <div>64 <h1>Count: {count}</h1>65 <BotDojoChat66 apiKey="your-api-key"67 modelContext={modelContext}68 mode="inline"69 />70 </div>71 );72}
💡 Key Points
- Use
useRefto access current state in tool execute functions - Wrap
modelContextinuseMemoto prevent unnecessary re-renders - Tools run entirely in your frontend - you control what actions are allowed
- Use
_metato customize how tools appear in the chat UI
🚀 Try it Out
See Frontend MCP in action with our interactive Task List example.