import { useEffect, useRef, useState } from "react"; import { GearIcon, KeyIcon, EyeIcon, EyeSlashIcon, ArrowsClockwiseIcon, CaretDownIcon } from "@phosphor-icons/react"; import { Button } from "@cloudflare/kumo"; import { Streamdown } from "streamdown"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import type { Prompt } from "@modelcontextprotocol/sdk/types.js"; import type { Resource } from "@modelcontextprotocol/sdk/types.js"; import type { Playground, PlaygroundState } from "../server"; import type { useAgent } from "agents/react"; import LocalhostWarningModal from "./LocalhostWarningModal"; import { McpInfoIcon } from "./Icons"; export type McpServerInfo = { id: string; name?: string; url?: string; state: string; error?: string | null; }; export type McpServersComponentState = { servers: McpServerInfo[]; tools: Tool[]; prompts: Prompt[]; resources: Resource[]; }; type McpServersProps = { agent: ReturnType>; mcpState: McpServersComponentState; mcpLogs: Array<{ timestamp: number; status: string; serverUrl?: string }>; }; export function McpServers({ agent, mcpState, mcpLogs }: McpServersProps) { const [serverUrl, setServerUrl] = useState(""); const [showSettings, setShowSettings] = useState(false); const [showLocalhostWarning, setShowLocalhostWarning] = useState(false); const [error, setError] = useState(""); const [isConnecting, setIsConnecting] = useState(false); const [disconnectingServerId, setDisconnectingServerId] = useState< string | null >(null); const hasConnectingServer = mcpState.servers.some( (s) => s.state === "discovering" || s.state === "connecting" || s.state === "connected" || s.state === "authenticating" ); const authenticatingServer = mcpState.servers.find( (s) => s.state === "authenticating" ); const logRef = useRef(null); const [showAuth, setShowAuth] = useState(false); const [headerKey, setHeaderKey] = useState(() => { return sessionStorage.getItem("mcpHeaderKey") || "Authorization"; }); const [bearerToken, setBearerToken] = useState(() => { return sessionStorage.getItem("mcpBearerToken") || ""; }); const [showToken, setShowToken] = useState(false); const [expandedTools, setExpandedTools] = useState>(new Set()); const toggleToolExpansion = (toolName: string) => { setExpandedTools((prev) => { const newSet = new Set(prev); if (newSet.has(toolName)) { newSet.delete(toolName); } else { newSet.add(toolName); } return newSet; }); }; const clearAuthFields = () => { setHeaderKey("Authorization"); setBearerToken(""); sessionStorage.removeItem("mcpHeaderKey"); sessionStorage.removeItem("mcpBearerToken"); }; const handleConnect = async () => { if (!serverUrl) { setError("Please enter a server URL"); return; } try { const url = new URL(serverUrl); const hostname = url.hostname.toLowerCase(); if ( hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1" ) { setShowLocalhostWarning(true); return; } } catch (_err) { // Invalid URL, let the server handle it } setIsConnecting(true); setError(""); try { let headers: Record | undefined; if (headerKey && bearerToken) { headers = { [headerKey]: `Bearer ${bearerToken}` }; } const result = (await agent.stub.connectMCPServer(serverUrl, headers)) as | { authUrl?: string } | undefined; if (result?.authUrl) { openOAuthPopup(result.authUrl); } setServerUrl(""); clearAuthFields(); setShowAuth(false); } catch (err: unknown) { console.error("[McpServers] Connection error:", err); setError( err instanceof Error ? err.message : "Failed to connect to MCP server" ); } finally { setIsConnecting(false); } }; const handleDisconnect = async (serverId: string) => { setDisconnectingServerId(serverId); setError(""); try { await agent.stub.disconnectMCPServer(serverId); } catch (err: unknown) { console.error("[McpServers] Disconnect error:", err); setError( err instanceof Error ? err.message : "Failed to disconnect from MCP server" ); } finally { setDisconnectingServerId(null); } }; const openOAuthPopup = (authUrl: string) => { window.open( authUrl, "mcpOAuthWindow", "width=600,height=800,resizable=yes,scrollbars=yes,toolbar=yes,menubar=no,location=no,directories=no,status=yes" ); }; // Auto-scroll debug log when new entries arrive useEffect(() => { if (logRef.current) { logRef.current.scrollTop = logRef.current.scrollHeight; } }, [mcpLogs]); const statusColors: Record = { discovering: "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-500/15 dark:text-blue-400 dark:border-blue-400/30", authenticating: "bg-amber-100 text-amber-700 border-amber-300 dark:bg-amber-500/15 dark:text-amber-400 dark:border-amber-400/30", connecting: "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-500/15 dark:text-blue-400 dark:border-blue-400/30", connected: "bg-green-100 text-green-700 border-green-300 dark:bg-green-500/15 dark:text-green-400 dark:border-green-400/30", ready: "bg-green-100 text-green-700 border-green-300 dark:bg-green-500/15 dark:text-green-400 dark:border-green-400/30", failed: "bg-red-100 text-red-700 border-red-300 dark:bg-red-500/15 dark:text-red-400 dark:border-red-400/30", "not-connected": "bg-gray-100 text-gray-700 border-gray-300 dark:bg-gray-500/15 dark:text-gray-400 dark:border-gray-400/30" }; const statusLabel: Record = { discovering: "Discovering", authenticating: "Authenticating", connecting: "Connecting", connected: "Connected", ready: "Ready", failed: "Failed", "not-connected": "Not Connected" }; const getStatusBadge = (state: string) => ( {statusLabel[state] || "Unknown"} ); return (
MCP Servers
{error && (
{error}
)}
{/* Add new server form */}
setServerUrl(e.target.value)} />
{/* Auth dropdown */} {showAuth && (
{ const newValue = e.target.value; setHeaderKey(newValue); sessionStorage.setItem("mcpHeaderKey", newValue); }} />
{ const newValue = e.target.value; setBearerToken(newValue); sessionStorage.setItem("mcpBearerToken", newValue); }} />
{headerKey && bearerToken && (
Will send: {headerKey}: Bearer •••••••
)}
)}
{/* Connected Servers List */} {mcpState.servers.length > 0 && (
{mcpState.servers.map((server) => (
{getStatusBadge(server.state)} {server.url}
{server.state === "failed" && server.error && (
{server.error}
)}
))}
)} {/* Debug Log */} {showSettings && (
Debug Log
{mcpLogs.map((log) => { const level = log.status === "failed" ? "error" : log.status === "connecting" || log.status === "connected" || log.status === "discovering" || log.status === "authenticating" || log.status === "ready" ? "info" : "debug"; const time = new Date(log.timestamp).toLocaleTimeString(); return (
[{level}] {time} - {log.status}
); })}
)} {/* Available Tools */} {mcpState.servers.some( (s) => s.state === "connected" || s.state === "ready" || s.state === "discovering" ) && (
Tools ({mcpState.tools.length})
{mcpState.tools.length > 0 ? (
{mcpState.tools.map((tool: Tool) => { const isExpanded = expandedTools.has(tool.name); return (
{tool.description && isExpanded && (
{tool.description}
)}
); })}
) : (
{mcpState.servers.some((s) => s.state === "discovering") ? "Discovering tools..." : "No tools available. Click refresh."}
)}
)}
setShowLocalhostWarning(false)} />
); }