import { Suspense, useCallback, useState, useEffect, useRef } from "react"; import { useAgent } from "agents/react"; import { useAgentChat } from "@cloudflare/ai-chat/react"; import { isToolUIPart, getToolName } from "ai"; import type { UIMessage } from "ai"; import type { MCPServersState } from "agents"; import { Button, Badge, InputArea, Empty, Surface, Text } from "@cloudflare/kumo"; import { ConnectionIndicator, ModeToggle, PoweredByAgents, type ConnectionStatus } from "@cloudflare/agents-ui"; import { PaperPlaneRightIcon, StopIcon, TrashIcon, CheckCircleIcon, XCircleIcon, GearIcon, CloudSunIcon, PlugsConnectedIcon, PlusIcon, SignInIcon, XIcon, WrenchIcon } from "@phosphor-icons/react"; function getMessageText(message: UIMessage): string { return message.parts .filter((part) => part.type === "text") .map((part) => (part as { type: "text"; text: string }).text) .join(""); } function Chat() { const [connectionStatus, setConnectionStatus] = useState("connecting"); const [input, setInput] = useState(""); const messagesEndRef = useRef(null); const [mcpState, setMcpState] = useState({ prompts: [], resources: [], servers: {}, tools: [] }); const [showMcpPanel, setShowMcpPanel] = useState(false); const [mcpName, setMcpName] = useState(""); const [mcpUrl, setMcpUrl] = useState(""); const [isAddingServer, setIsAddingServer] = useState(false); const mcpPanelRef = useRef(null); const agent = useAgent({ agent: "ChatAgent", onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => setConnectionStatus("disconnected"), []), onError: useCallback( (error: Event) => console.error("WebSocket error:", error), [] ), onMcpUpdate: useCallback((state: MCPServersState) => { setMcpState(state); }, []) }); // Close MCP panel when clicking outside useEffect(() => { if (!showMcpPanel) return; function handleClickOutside(e: MouseEvent) { if ( mcpPanelRef.current && !mcpPanelRef.current.contains(e.target as Node) ) { setShowMcpPanel(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [showMcpPanel]); const handleAddServer = async () => { if (!mcpName.trim() || !mcpUrl.trim()) return; setIsAddingServer(true); try { await agent.call("addServer", [ mcpName.trim(), mcpUrl.trim(), window.location.origin ]); setMcpName(""); setMcpUrl(""); } catch (e) { console.error("Failed to add MCP server:", e); } finally { setIsAddingServer(false); } }; const handleRemoveServer = async (serverId: string) => { try { await agent.call("removeServer", [serverId]); } catch (e) { console.error("Failed to remove MCP server:", e); } }; const serverEntries = Object.entries(mcpState.servers); const mcpToolCount = mcpState.tools.length; const { messages, sendMessage, clearHistory, addToolApprovalResponse, stop, status } = useAgentChat({ agent, // Custom data sent with every request (available in options.body on server) body: { clientVersion: "1.0.0" }, // Handle client-side tools (tools without server execute function) onToolCall: async ({ toolCall, addToolOutput }) => { if (toolCall.toolName === "getUserTimezone") { addToolOutput({ toolCallId: toolCall.toolCallId, output: { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, localTime: new Date().toLocaleTimeString() } }); } } }); const isStreaming = status === "streaming"; const isConnected = connectionStatus === "connected"; useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const send = useCallback(() => { const text = input.trim(); if (!text || isStreaming) return; setInput(""); sendMessage({ role: "user", parts: [{ type: "text", text }] }); }, [input, isStreaming, sendMessage]); return (
{/* Header */}

AI Chat

Tools + Approval
{/* MCP Dropdown Panel */} {showMcpPanel && (
{/* Panel Header */}
MCP Servers {serverEntries.length > 0 && ( {serverEntries.length} )}
{/* Add Server Form */}
{ e.preventDefault(); handleAddServer(); }} className="space-y-2" > setMcpName(e.target.value)} placeholder="Server name" className="w-full px-3 py-1.5 text-sm rounded-lg border border-kumo-line bg-kumo-base text-kumo-default placeholder:text-kumo-inactive focus:outline-none focus:ring-1 focus:ring-kumo-accent" />
setMcpUrl(e.target.value)} placeholder="https://mcp.example.com" className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-kumo-line bg-kumo-base text-kumo-default placeholder:text-kumo-inactive focus:outline-none focus:ring-1 focus:ring-kumo-accent font-mono" />
{/* Server List */} {serverEntries.length > 0 && (
{serverEntries.map(([id, server]) => (
{server.name} {server.state}
{server.server_url} {server.state === "failed" && server.error && ( {server.error} )}
{server.state === "authenticating" && server.auth_url && ( )}
))}
)} {/* Tool Summary */} {mcpToolCount > 0 && (
{mcpToolCount} tool {mcpToolCount !== 1 ? "s" : ""} available from MCP servers
)}
)}
{/* Messages */}
{messages.length === 0 && ( } title="Start a conversation" description='Try "What is the weather in London?" or "What timezone am I in?" or "What is 5000 + 3000?"' /> )} {messages.map((message, index) => { const isUser = message.role === "user"; const isLastAssistant = message.role === "assistant" && index === messages.length - 1; if (isUser) { return (
{getMessageText(message)}
); } // Assistant: render parts in order return (
{message.parts.map((part, partIndex) => { // Text if (part.type === "text") { if (!part.text) return null; const isLastTextPart = message.parts .slice(partIndex + 1) .every((p) => p.type !== "text"); return (
{part.text} {isLastAssistant && isLastTextPart && isStreaming && ( )}
); } // Reasoning if (part.type === "reasoning") { if (!part.text) return null; return (
Thinking
{part.text}
); } // Tool invocations if (!isToolUIPart(part)) return null; const toolName = getToolName(part); // Tool completed if (part.state === "output-available") { return (
{toolName} Done
{JSON.stringify(part.output, null, 2)}
); } // Tool needs approval if ( "approval" in part && part.state === "approval-requested" ) { const approvalId = (part.approval as { id?: string })?.id; return (
Approval needed: {toolName}
{JSON.stringify(part.input, null, 2)}
); } // Tool denied if (part.state === "output-denied") { return (
{toolName} Denied
); } // Tool executing if ( part.state === "input-available" || part.state === "input-streaming" ) { return (
Running {toolName}...
); } return null; })}
); })}
{/* Input */}
{ e.preventDefault(); send(); }} className="max-w-3xl mx-auto px-5 py-4" >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder="Try: What's the weather in Paris?" disabled={!isConnected || isStreaming} rows={2} className="flex-1 !ring-0 focus:!ring-0 !shadow-none !bg-transparent !outline-none" /> {isStreaming ? (
); } export default function App() { return ( Loading...
} > ); }