"use client"; import { type McpServerInfo, MCPTransport } from "@tambo-ai/react/mcp"; import { ChevronDown, X, Trash2 } from "lucide-react"; import React from "react"; import { createPortal } from "react-dom"; import { motion } from "framer-motion"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@radix-ui/react-dropdown-menu"; import { createMarkdownComponents } from "@/components/ui/markdown-components"; import { Streamdown } from "streamdown"; import { cn } from "@/lib/utils"; /** * Modal component for configuring client-side MCP (Model Context Protocol) servers. * * This component provides a user interface for managing MCP server connections that * will be used to extend the capabilities of the tambo application. The servers are * stored in browser localStorage and connected directly from the client-side. * * @param props - Component props * @param props.isOpen - Whether the modal is currently open/visible * @param props.onClose - Callback function called when the modal should be closed * @returns The modal component or null if not open */ export const McpConfigModal = ({ isOpen, onClose, className, }: { isOpen: boolean; onClose: () => void; className?: string; }) => { // Initialize from localStorage directly to avoid conflicts const [mcpServers, setMcpServers] = React.useState(() => { if (typeof window === "undefined") return []; try { return JSON.parse(localStorage.getItem("mcp-servers") ?? "[]"); } catch { return []; } }); const [serverUrl, setServerUrl] = React.useState(""); const [serverName, setServerName] = React.useState(""); const [transportType, setTransportType] = React.useState( MCPTransport.HTTP, ); const [savedSuccess, setSavedSuccess] = React.useState(false); const [showInstructions, setShowInstructions] = React.useState(false); // Handle Escape key to close modal React.useEffect(() => { const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === "Escape") { onClose(); } }; if (isOpen) { document.addEventListener("keydown", handleEscapeKey); } return () => { document.removeEventListener("keydown", handleEscapeKey); }; }, [isOpen, onClose]); // Save servers to localStorage when updated and emit events React.useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("mcp-servers", JSON.stringify(mcpServers)); // Emit custom event to notify other components in the same tab window.dispatchEvent( new CustomEvent("mcp-servers-updated", { detail: mcpServers, }), ); if (mcpServers.length > 0) { setSavedSuccess(true); const timer = setTimeout(() => setSavedSuccess(false), 2000); return () => clearTimeout(timer); } } }, [mcpServers]); const addServer = (e: React.FormEvent) => { e.preventDefault(); if (serverUrl.trim()) { const serverConfig = { url: serverUrl.trim(), transport: transportType, ...(serverName.trim() ? { name: serverName.trim() } : {}), }; setMcpServers((prev) => [...prev, serverConfig]); // Reset form fields setServerUrl(""); setServerName(""); setTransportType(MCPTransport.HTTP); } }; const removeServer = (index: number) => { setMcpServers((prev) => prev.filter((_, i) => i !== index)); }; // Helper function to get server display information const getServerInfo = (server: McpServerInfo) => { if (typeof server === "string") { return { url: server, transport: "SSE (default)", name: null }; } else { return { url: server.url, transport: server.transport ?? "SSE (default)", name: server.name ?? null, }; } }; const handleBackdropClick = (e: React.MouseEvent) => { // Close modal when clicking on backdrop (not on the modal content) if (e.target === e.currentTarget) { onClose(); } }; const getTransportDisplayText = (transport: MCPTransport) => { return transport === MCPTransport.HTTP ? "HTTP (default)" : "SSE"; }; if (!isOpen) return null; const instructions = ` ### After configuring your MCP servers below, integrate them into your application. #### 1. Import the required components \`\`\`tsx import { useMcpServers } from "@/components/tambo/mcp-config-modal"; import { TamboMcpProvider } from "@tambo-ai/react/mcp"; \`\`\` #### 2. Load MCP servers and wrap your components: \`\`\`tsx const mcpServers = useMcpServers(); \`\`\` #### 3. Example implementation: \`\`\`tsx function MyApp() { const mcpServers = useMcpServers(); // Reactive - updates when servers change return ( {/* Your app components */} ); } \`\`\` `; const modalContent = (

MCP Server Configuration

{/* Content */}
{showInstructions && ( {instructions} )}
{/* Description */}

Configure{" "} client-side{" "} MCP servers to extend the capabilities of your tambo application. These servers will be connected{" "} from the browser {" "} and exposed as tools to tambo.

{/* Form */}
{/* Server URL */}
setServerUrl(e.target.value)} placeholder="https://your-mcp-server-url.com" className="w-full px-3 py-2.5 border border-muted rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all duration-150 text-sm" required />
{/* Server Name */}
setServerName(e.target.value)} placeholder="Custom server name" className="w-full px-3 py-2.5 border border-muted rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all duration-150 text-sm" />
{/* Transport Type */}
setTransportType(MCPTransport.HTTP)} > HTTP (default) setTransportType(MCPTransport.SSE)} > SSE
{/* Success Message */} {savedSuccess && (
Servers saved to browser storage
)} {/* Server List */} {mcpServers.length > 0 ? (

Connected Servers ({mcpServers.length})

{mcpServers.map((server, index) => { const serverInfo = getServerInfo(server); return (
{serverInfo.url}
{serverInfo.name && (
Name:{" "} {serverInfo.name}
)}
Transport:{" "} {serverInfo.transport}
); })}
) : (

No MCP servers configured yet

Add your first server above to get started

)} {/* Info Section */}

What is MCP?

The{" "} Model Context Protocol (MCP) {" "} is a standard that allows applications to communicate with external tools and services. By configuring MCP servers, your tambo application will be able to make calls to these tools.

Learn more:{" "} client-side {" "} |{" "} server-side {" "} MCP configuration.

); // Use portal to render outside current DOM tree to avoid nested forms return typeof window !== "undefined" ? createPortal(modalContent, document.body) : null; }; /** * Type for MCP Server entries */ export type McpServer = string | { url: string }; /** * Load and reactively track MCP server configurations from browser localStorage. * * This hook retrieves saved MCP server configurations and automatically updates * when servers are added/removed from the modal or other tabs. It deduplicates * servers by URL and handles parsing errors gracefully. * * @returns Array of unique MCP server configurations that updates automatically or empty array if none found or in SSR context * * @example * ```tsx * function MyApp() { * const mcpServers = useMcpServers(); // Reactive - updates automatically * // Returns: [{ url: "https://api.example.com" }, "https://api2.example.com"] * * return ( * * {children} * * ); * } * ``` */ export function useMcpServers(): McpServer[] { const [servers, setServers] = React.useState(() => { if (typeof window === "undefined") return []; const savedServersData = localStorage.getItem("mcp-servers"); if (!savedServersData) return []; try { const servers = JSON.parse(savedServersData); // Deduplicate servers by URL to prevent multiple tool registrations const uniqueUrls = new Set(); return servers.filter((server: McpServer) => { const url = typeof server === "string" ? server : server.url; if (uniqueUrls.has(url)) return false; uniqueUrls.add(url); return true; }); } catch (e) { console.error("Failed to parse saved MCP servers", e); return []; } }); React.useEffect(() => { const updateServers = () => { if (typeof window === "undefined") return; const savedServersData = localStorage.getItem("mcp-servers"); if (!savedServersData) { setServers([]); return; } try { const newServers = JSON.parse(savedServersData); // Deduplicate servers by URL const uniqueUrls = new Set(); const deduped = newServers.filter((server: McpServer) => { const url = typeof server === "string" ? server : server.url; if (uniqueUrls.has(url)) return false; uniqueUrls.add(url); return true; }); setServers(deduped); } catch (e) { console.error("Failed to parse saved MCP servers", e); setServers([]); } }; // Listen for custom events (same tab updates) const handleCustomEvent = () => updateServers(); window.addEventListener("mcp-servers-updated", handleCustomEvent); // Listen for storage events (cross-tab updates) const handleStorageEvent = (e: StorageEvent) => { if (e.key === "mcp-servers") { updateServers(); } }; window.addEventListener("storage", handleStorageEvent); return () => { window.removeEventListener("mcp-servers-updated", handleCustomEvent); window.removeEventListener("storage", handleStorageEvent); }; }, []); return servers; }