BotDojo LogoInteractive Agent SDKBeta
About BotDojoBotDojo

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.

👁️
Resources
What the agent can see
🔧
Tools
What the agent can do
🔒
Security
You control access

Defining a ModelContext

A ModelContext defines what the AI agent can see (resources) and do (tools) in your application.

📄ModelContext structuretsx
1import { useMemo } from 'react';
2import type { ModelContext } from '@botdojo/chat-sdk';
3
4function 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',
11
12 resources: [
13 // Resources define what the agent can "see"
14 ],
15
16 tools: [
17 // Tools define what the agent can "do"
18 ],
19 }), []);
20
21 // 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).

📄BotDojoChat + ModelContext (drop-in widget)tsx
1import { useMemo, useRef, useEffect, useState } from 'react';
2import { BotDojoChat, type ModelContext } from '@botdojo/chat-sdk';
3
4export default function WidgetWithFrontendMcp() {
5 const [text, setText] = useState('Hello');
6 const textRef = useRef(text);
7
8 useEffect(() => { textRef.current = text; }, [text]);
9
10 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 }), []);
44
45 return (
46 <div>
47 <div style={{ marginBottom: 8, fontWeight: 700 }}>UI text: {text}</div>
48 <BotDojoChat
49 apiKey={token} // From useTemporaryToken() hook
50 baseUrl={process.env.NEXT_PUBLIC_IFRAME_URL || 'https://embed.botdojo.com'}
51 mode="inline"
52 modelContext={modelContext}
53 />
54 </div>
55 );
56}
📄BotDojoChatProvider + ModelContext (headless)tsx
1import { useMemo, useRef, useEffect, useState } from 'react';
2import {
3 BotDojoChatProvider,
4 useChatMessages,
5 useChatActions,
6 useChatStatus,
7 type ModelContext,
8} from '@botdojo/chat-sdk';
9
10function MinimalHeadlessUi() {
11 const { messages, isStreaming } = useChatMessages();
12 const { sendMessage } = useChatActions();
13 const { isReady } = useChatStatus();
14 const [input, setInput] = useState('');
15
16 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 <form
29 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 <input
38 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 Send
45 </button>
46 </form>
47 </div>
48 );
49}
50
51export default function HeadlessWithFrontendMcp() {
52 const [text, setText] = useState('Hello');
53 const textRef = useRef(text);
54 useEffect(() => { textRef.current = text; }, [text]);
55
56 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 }), []);
90
91 return (
92 <div>
93 <div style={{ marginBottom: 8, fontWeight: 700 }}>UI text: {text}</div>
94 <BotDojoChatProvider
95 apiKey={token} // From useTemporaryToken() hook
96 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.

📄Resource definitiontypescript
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.

📄Tool definitiontypescript
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:

📄Complete counter exampletsx
1import { useMemo, useState, useRef, useEffect } from 'react';
2import { BotDojoChat, type ModelContext } from '@botdojo/chat-sdk';
3
4function CounterApp() {
5 const [count, setCount] = useState(0);
6 const countRef = useRef(count);
7
8 useEffect(() => {
9 countRef.current = count;
10 }, [count]);
11
12 const modelContext: ModelContext = useMemo(() => ({
13 name: 'counter',
14 description: 'A simple counter with AI control',
15 toolPrefix: 'counter',
16 uri: 'counter://context',
17
18 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 ],
31
32 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 }), []);
61
62 return (
63 <div>
64 <h1>Count: {count}</h1>
65 <BotDojoChat
66 apiKey="your-api-key"
67 modelContext={modelContext}
68 mode="inline"
69 />
70 </div>
71 );
72}

💡 Key Points

  • Use useRef to access current state in tool execute functions
  • Wrap modelContext in useMemo to prevent unnecessary re-renders
  • Tools run entirely in your frontend - you control what actions are allowed
  • Use _meta to customize how tools appear in the chat UI

🚀 Try it Out

See Frontend MCP in action with our interactive Task List example.