BotDojo LogoInteractive Agent SDKBeta
About BotDojoBotDojo
🌤️
Frontend MCP + Custom UI

Headless Chat

Build your own chat interface with full design control. This demo shows a Frontend MCP that fetches live weather data from the National Weather Service API and displays it in a beautiful MCP App. Use React hooks and providers to access chat state, actions, and streaming events.

Data Flow

🎨
Your Website
Custom Chat UI
📡
SDK Hooks
☁️
BotDojo Cloud
🔌Your API
Integrations
🔗MCP Servers
Click Animate to see the flow

What This Demo Shows

🌤️Live Weather API integration
🔧Frontend MCP with tools
🎨Beautiful MCP App display
📡React hooks for state
Full design control
Real-time streaming

Live Demo

Loading...

Code

📄samples/headless-chat/HeadlessDemo.tsxtsx
1import { useMemo, useState } from 'react';
2import { BotDojoChatProvider, type ModelContext } from '@botdojo/chat-sdk';
3import { MessageList } from './MessageList';
4import { ChatInput } from './ChatInput';
5import { useTemporaryToken } from '@/hooks/useTemporaryToken';
6
7const config = {
8 baseUrl: process.env.NEXT_PUBLIC_IFRAME_URL || 'https://embed.botdojo.com',
9};
10
11interface HeadlessDemoProps {
12 onNewChat?: () => void;
13}
14
15// Simple city coordinate lookup
16const cityCoords: Record<string, { lat: number; lon: number; name: string }> = {
17 'new york': { lat: 40.7128, lon: -74.0060, name: 'New York, NY' },
18 'los angeles': { lat: 34.0522, lon: -118.2437, name: 'Los Angeles, CA' },
19 'chicago': { lat: 41.8781, lon: -87.6298, name: 'Chicago, IL' },
20 'houston': { lat: 29.7604, lon: -95.3698, name: 'Houston, TX' },
21 'phoenix': { lat: 33.4484, lon: -112.0740, name: 'Phoenix, AZ' },
22 'philadelphia': { lat: 39.9526, lon: -75.1652, name: 'Philadelphia, PA' },
23 'san antonio': { lat: 29.4241, lon: -98.4936, name: 'San Antonio, TX' },
24 'san diego': { lat: 32.7157, lon: -117.1611, name: 'San Diego, CA' },
25 'dallas': { lat: 32.7767, lon: -96.7970, name: 'Dallas, TX' },
26 'san francisco': { lat: 37.7749, lon: -122.4194, name: 'San Francisco, CA' },
27 'seattle': { lat: 47.6062, lon: -122.3321, name: 'Seattle, WA' },
28 'denver': { lat: 39.7392, lon: -104.9903, name: 'Denver, CO' },
29 'boston': { lat: 42.3601, lon: -71.0589, name: 'Boston, MA' },
30 'miami': { lat: 25.7617, lon: -80.1918, name: 'Miami, FL' },
31 'atlanta': { lat: 33.7490, lon: -84.3880, name: 'Atlanta, GA' },
32 'washington': { lat: 38.8894, lon: -77.0352, name: 'Washington, DC' },
33 'washington dc': { lat: 38.8894, lon: -77.0352, name: 'Washington, DC' },
34};
35
36export default function HeadlessDemo({ onNewChat }: HeadlessDemoProps = {}) {
37 // Get temporary JWT token for secure API access
38 const { token, loading: tokenLoading, error: tokenError } = useTemporaryToken();
39
40 // Session key to force new session
41 const [sessionKey, setSessionKey] = useState(0);
42
43 const handleNewChat = () => {
44 setSessionKey(prev => prev + 1);
45 onNewChat?.();
46 };
47
48 // Define ModelContext with weather tool and resource
49 const modelContext: ModelContext = useMemo(() => ({
50 name: 'weather_service',
51 description: 'Frontend MCP that provides weather information using the National Weather Service API',
52 toolPrefix: 'weather',
53 uri: 'weather://context',
54
55 // Define resources - what the agent can "see"
56 resources: [
57 {
58 uri: 'ui://headless-chat/weather',
59 name: 'Weather Display Widget',
60 description: 'Beautiful weather display MCP App showing current conditions and forecast',
61 mimeType: 'text/html;profile=mcp-app',
62 getContent: async () => {
63 const { fetchMcpAppHtml } = await import('@/utils/fetchMcpApp');
64 return {
65 uri: 'ui://headless-chat/weather',
66 mimeType: 'text/html;profile=mcp-app',
67 text: await fetchMcpAppHtml('weather'),
68 };
69 },
70 },
71 ],
72
73 // Define tools - what the agent can "do"
74 tools: [
75 {
76 name: 'get_weather',
77 description: 'Get current weather and forecast for a location. Uses the National Weather Service API. Provide latitude and longitude, or a city name (will use approximate coordinates for major US cities).',
78 inputSchema: {
79 type: 'object',
80 properties: {
81 latitude: { type: 'number', description: 'Latitude of the location (e.g., 38.8894 for Washington DC)' },
82 longitude: { type: 'number', description: 'Longitude of the location (e.g., -77.0352 for Washington DC)' },
83 city: { type: 'string', description: 'City name (optional, will use approximate coordinates for major US cities)' },
84 },
85 },
86 // Reference the UI resource - this tells the system to render the MCP App
87 _meta: {
88 'botdojo/no-cache': true,
89 ui: {
90 resourceUri: 'ui://headless-chat/weather',
91 },
92 'botdojo/display-name': 'Get Weather',
93 },
94 // Tool execute fetches weather data and returns it to the widget
95 execute: async (params: { latitude?: number; longitude?: number; city?: string }) => {
96 try {
97 // Default to Washington DC if no coordinates provided
98 let lat = params.latitude || 38.8894;
99 let lon = params.longitude || -77.0352;
100 let locationName = params.city || 'Washington, DC';
101
102 if (params.city) {
103 const cityKey = params.city.toLowerCase();
104 const found = cityCoords[cityKey];
105 if (found) {
106 lat = found.lat;
107 lon = found.lon;
108 locationName = found.name;
109 } else {
110 locationName = params.city;
111 }
112 }
113
114 // Step 1: Get grid point from coordinates
115 const pointsResponse = await fetch(
116 `https://api.weather.gov/points/${lat},${lon}`,
117 {
118 headers: {
119 'User-Agent': '(BotDojo SDK Playground, contact@botdojo.com)',
120 'Accept': 'application/geo+json',
121 },
122 }
123 );
124
125 if (!pointsResponse.ok) {
126 throw new Error(`Weather API error: ${pointsResponse.status}`);
127 }
128
129 const pointsData = await pointsResponse.json();
130 const forecastUrl = pointsData.properties?.forecast;
131
132 if (!forecastUrl) {
133 throw new Error('Could not get forecast URL from weather service');
134 }
135
136 // Step 2: Get forecast
137 const forecastResponse = await fetch(forecastUrl, {
138 headers: {
139 'User-Agent': '(BotDojo SDK Playground, contact@botdojo.com)',
140 'Accept': 'application/geo+json',
141 },
142 });
143
144 if (!forecastResponse.ok) {
145 throw new Error(`Forecast API error: ${forecastResponse.status}`);
146 }
147
148 const forecastData = await forecastResponse.json();
149 const periods = forecastData.properties?.periods || [];
150 const current = periods[0];
151
152 // Return structured weather data for the widget to display
153 return {
154 location: locationName,
155 temperature: current?.temperature || 0,
156 temperatureUnit: current?.temperatureUnit || 'F',
157 shortForecast: current?.shortForecast || 'Unknown',
158 windSpeed: current?.windSpeed || 'N/A',
159 windDirection: current?.windDirection || '',
160 humidity: current?.relativeHumidity?.value,
161 forecast: periods.slice(1, 5).map((p: any) => ({
162 name: p.name,
163 temperature: p.temperature,
164 temperatureUnit: p.temperatureUnit,
165 shortForecast: p.shortForecast,
166 })),
167 };
168 } catch (error) {
169 return {
170 error: error instanceof Error ? error.message : 'Unknown error',
171 };
172 }
173 },
174 },
175 ],
176
177 prompts: [],
178 }), []);
179
180 if (tokenLoading) {
181 return (
182 <div style={{
183 display: 'flex',
184 flexDirection: 'column',
185 height: '100%',
186 background: '#f9fafb',
187 borderRadius: '12px',
188 padding: '24px',
189 alignItems: 'center',
190 justifyContent: 'center',
191 border: '1px solid #e5e7eb',
192 }}>
193 <div style={{ color: '#6b7280', fontWeight: 600 }}>Loading...</div>
194 </div>
195 );
196 }
197
198 if (tokenError || !token) {
199 return (
200 <div style={{
201 display: 'flex',
202 flexDirection: 'column',
203 height: '100%',
204 background: '#fef2f2',
205 borderRadius: '12px',
206 padding: '24px',
207 alignItems: 'center',
208 justifyContent: 'center',
209 border: '1px solid #fecaca',
210 }}>
211 <div style={{ color: '#991b1b', fontWeight: 600, marginBottom: '8px' }}>Missing API key</div>
212 <div style={{ color: '#b91c1c', fontSize: '14px', textAlign: 'center' }}>
213 Run <code style={{ background: 'white', padding: '2px 6px', borderRadius: '4px' }}>pnpm setup-playground</code> or set <code style={{ background: 'white', padding: '2px 6px', borderRadius: '4px' }}>BOTDOJO_MODEL_CONTEXT_API</code>
214 </div>
215 </div>
216 );
217 }
218
219 return (
220 <BotDojoChatProvider
221 key={sessionKey}
222 apiKey={token}
223 baseUrl={config.baseUrl}
224 newSession={sessionKey > 0}
225 modelContext={modelContext}
226 debug={true}
227 >
228 <div style={{
229 display: 'flex',
230 flexDirection: 'column',
231 height: '100%',
232 background: '#f9fafb',
233 borderRadius: '12px',
234 overflow: 'hidden',
235 border: '1px solid #e5e7eb',
236 }}>
237 {/* Welcome message with prompt buttons */}
238 <div style={{
239 padding: '16px',
240 background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
241 color: 'white',
242 }}>
243 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
244 <div style={{ fontSize: '16px', fontWeight: 700 }}>
245 🌤️ Weather Assistant
246 </div>
247 <button
248 onClick={handleNewChat}
249 style={{
250 padding: '6px 12px',
251 background: 'rgba(255,255,255,0.2)',
252 border: '1px solid rgba(255,255,255,0.3)',
253 borderRadius: '6px',
254 color: 'white',
255 fontSize: '12px',
256 fontWeight: 600,
257 cursor: 'pointer',
258 display: 'flex',
259 alignItems: 'center',
260 gap: '4px',
261 }}
262 >
263 ✨ New Chat
264 </button>
265 </div>
266 <div style={{ fontSize: '13px', opacity: 0.9, marginBottom: '12px' }}>
267 Ask me about the weather in any US city!
268 </div>
269 <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
270 <QuickButton text="What's the weather in Seattle?" />
271 <QuickButton text="Weather forecast for Miami" />
272 <QuickButton text="Is it sunny in Denver?" />
273 </div>
274 </div>
275
276 {/* Messages */}
277 <div style={{ flex: 1, overflow: 'auto' }}>
278 <MessageList />
279 </div>
280
281 {/* Input */}
282 <ChatInput />
283 </div>
284 </BotDojoChatProvider>
285 );
286}
287
288// Quick action button component
289function QuickButton({ text }: { text: string }) {
290 return (
291 <button
292 onClick={() => {
293 // Dispatch a custom event that ChatInput can listen for
294 window.dispatchEvent(new CustomEvent('quick-message', { detail: text }));
295 }}
296 style={{
297 padding: '6px 12px',
298 background: 'rgba(255,255,255,0.2)',
299 border: '1px solid rgba(255,255,255,0.3)',
300 borderRadius: '16px',
301 color: 'white',
302 fontSize: '12px',
303 cursor: 'pointer',
304 transition: 'all 0.2s',
305 }}
306 onMouseEnter={(e) => {
307 e.currentTarget.style.background = 'rgba(255,255,255,0.3)';
308 }}
309 onMouseLeave={(e) => {
310 e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
311 }}
312 >
313 {text}
314 </button>
315 );
316}
317

Key Concepts

ModelContext (Frontend MCP)

Define tools that run in your browser. This demo's get_weather tool fetches data from weather.gov and returns both text results and a beautiful HTML widget via uiResource.

useChatMessages()

Access the message list, streaming content, and current message. Updates in real-time as messages arrive.

useChatActions()

Send messages, abort requests, and clear history. Control the chat flow programmatically.

useChatStatus()

Get connection status, loading state, and errors. Know when the chat is ready for input.