🌤️
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 WebsiteCustom 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';67const config = {8 baseUrl: process.env.NEXT_PUBLIC_IFRAME_URL || 'https://embed.botdojo.com',9};1011interface HeadlessDemoProps {12 onNewChat?: () => void;13}1415// Simple city coordinate lookup16const 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};3536export default function HeadlessDemo({ onNewChat }: HeadlessDemoProps = {}) {37 // Get temporary JWT token for secure API access38 const { token, loading: tokenLoading, error: tokenError } = useTemporaryToken();3940 // Session key to force new session41 const [sessionKey, setSessionKey] = useState(0);4243 const handleNewChat = () => {44 setSessionKey(prev => prev + 1);45 onNewChat?.();46 };4748 // Define ModelContext with weather tool and resource49 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',5455 // 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 ],7273 // 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 App87 _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 widget95 execute: async (params: { latitude?: number; longitude?: number; city?: string }) => {96 try {97 // Default to Washington DC if no coordinates provided98 let lat = params.latitude || 38.8894;99 let lon = params.longitude || -77.0352;100 let locationName = params.city || 'Washington, DC';101102 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 }113114 // Step 1: Get grid point from coordinates115 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 );124125 if (!pointsResponse.ok) {126 throw new Error(`Weather API error: ${pointsResponse.status}`);127 }128129 const pointsData = await pointsResponse.json();130 const forecastUrl = pointsData.properties?.forecast;131132 if (!forecastUrl) {133 throw new Error('Could not get forecast URL from weather service');134 }135136 // Step 2: Get forecast137 const forecastResponse = await fetch(forecastUrl, {138 headers: {139 'User-Agent': '(BotDojo SDK Playground, contact@botdojo.com)',140 'Accept': 'application/geo+json',141 },142 });143144 if (!forecastResponse.ok) {145 throw new Error(`Forecast API error: ${forecastResponse.status}`);146 }147148 const forecastData = await forecastResponse.json();149 const periods = forecastData.properties?.periods || [];150 const current = periods[0];151152 // Return structured weather data for the widget to display153 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 ],176177 prompts: [],178 }), []);179180 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 }197198 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 }218219 return (220 <BotDojoChatProvider221 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 Assistant246 </div>247 <button248 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 Chat264 </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>275276 {/* Messages */}277 <div style={{ flex: 1, overflow: 'auto' }}>278 <MessageList />279 </div>280281 {/* Input */}282 <ChatInput />283 </div>284 </BotDojoChatProvider>285 );286}287288// Quick action button component289function QuickButton({ text }: { text: string }) {290 return (291 <button292 onClick={() => {293 // Dispatch a custom event that ChatInput can listen for294 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.