import { IconArrowLeft, IconCheck, IconExternalLink, IconLoader2, IconSearch, } from "@tabler/icons-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { agentNativePath } from "../api-path.js"; import { openAgentSettings } from "../CommandMenu.js"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../components/ui/dialog.js"; import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js"; import { useT } from "../i18n.js"; import { cn } from "../utils.js"; import { buildMcpOAuthStartUrl, createMcpIntegrationFormDefaults, filterMcpIntegrations, getMcpIntegrationApiFallback, getDefaultMcpIntegrations, isCustomMcpIntegrationEnabled, navigateToMcpOAuthStart, resolveMcpIntegrationScope, type DefaultMcpIntegration, } from "./mcp-integration-catalog.js"; import { formatMcpServerError, getMcpUrlValidationError, testMcpServerUrl, useMcpServers, type CreateMcpServerArgs, type McpServerScope, } from "./use-mcp-servers.js"; type DialogMode = "catalog" | "form"; export interface McpIntegrationDialogProps { open: boolean; onOpenChange: (open: boolean) => void; initialIntegrationId?: string | null; defaultScope: McpServerScope; canCreateOrgMcp: boolean; hasOrg: boolean; onCreateMcpServer: (args: CreateMcpServerArgs) => Promise; onCreated?: () => void; integrations?: DefaultMcpIntegration[]; } interface TestResult { ok: boolean; message: string; } function IntegrationLogo({ name, logoUrl }: { name: string; logoUrl: string }) { const [loaded, setLoaded] = useState(false); const [loadFailed, setLoadFailed] = useState(false); useEffect(() => { setLoaded(false); setLoadFailed(false); }, [logoUrl]); return (
{ setLoadFailed(false); setLoaded(true); }} onError={() => { setLoadFailed(true); setLoaded(false); }} />
); } function parseHeaderLines(text: string): Record | undefined { const out: Record = {}; for (const line of text.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed) continue; const idx = trimmed.indexOf(":"); if (idx <= 0) continue; const key = trimmed.slice(0, idx).trim(); const value = trimmed.slice(idx + 1).trim(); if (!key || !value) continue; out[key] = value; } return Object.keys(out).length > 0 ? out : undefined; } function compareUrl(value: string): string { try { const url = new URL(value.trim()); url.hash = ""; return url.toString().replace(/\/+$/, ""); } catch { return value.trim().replace(/\/+$/, ""); } } export function McpIntegrationDialog({ open, onOpenChange, initialIntegrationId = null, defaultScope, canCreateOrgMcp, hasOrg, onCreateMcpServer, onCreated, integrations, }: McpIntegrationDialogProps) { const t = useT(); const [mode, setMode] = useState("catalog"); const [query, setQuery] = useState(""); const [selected, setSelected] = useState(null); const safeDefaultScope = resolveMcpIntegrationScope( defaultScope, hasOrg, canCreateOrgMcp, ); const [scope, setScope] = useState(safeDefaultScope); const [name, setName] = useState(""); const [url, setUrl] = useState(""); const [description, setDescription] = useState(""); const [headersText, setHeadersText] = useState(""); const [busy, setBusy] = useState(false); const [quickBusyId, setQuickBusyId] = useState(null); const [error, setError] = useState(null); const [testResult, setTestResult] = useState(null); const inputRef = useRef(null); const mcpServersQuery = useMcpServers(); const defaultIntegrations = useMemo( () => integrations ?? getDefaultMcpIntegrations(), [integrations], ); const customIntegrationEnabled = useMemo( () => isCustomMcpIntegrationEnabled(), [], ); const showCatalog = defaultIntegrations.length > 0; const connectedUrls = useMemo(() => { const servers = [ ...(mcpServersQuery.data?.user ?? []), ...(mcpServersQuery.data?.org ?? []), ]; // A saved server is not necessarily a working connection. The settings // page reports failed and unknown health states separately, so only mark // catalog entries as connected after the health probe succeeds. return new Set( servers .filter((server) => server.status.state === "connected") .map((server) => compareUrl(server.url)), ); }, [mcpServersQuery.data]); const filteredIntegrations = useMemo( () => filterMcpIntegrations(query, defaultIntegrations), [defaultIntegrations, query], ); const selectedRequiresSetup = Boolean( selected && (selected.connectionMode === "manual" || selected.availability === "provider-setup" || selected.availability === "client-restricted"), ); useEffect(() => { if (!open) return; const initialIntegration = initialIntegrationId ? defaultIntegrations.find( (integration) => integration.id === initialIntegrationId, ) : null; const initialDefaults = createMcpIntegrationFormDefaults(initialIntegration); setMode(initialIntegration || !showCatalog ? "form" : "catalog"); setQuery(""); setSelected(initialIntegration ?? null); setScope(safeDefaultScope); setName(initialDefaults.name); setUrl(initialDefaults.url); setDescription(initialDefaults.description); setHeadersText(initialDefaults.headersText); setBusy(false); setQuickBusyId(null); setError(null); setTestResult(null); }, [ defaultIntegrations, initialIntegrationId, open, safeDefaultScope, showCatalog, ]); useEffect(() => { if (open && mode === "form") { const timer = window.setTimeout(() => inputRef.current?.focus(), 60); return () => window.clearTimeout(timer); } }, [mode, open]); const clearFeedback = () => { setError(null); setTestResult(null); }; const openForm = (integration?: DefaultMcpIntegration | null) => { const defaults = createMcpIntegrationFormDefaults(integration); setSelected(integration ?? null); setScope(safeDefaultScope); setName(defaults.name); setUrl(defaults.url); setDescription(defaults.description); setHeadersText(defaults.headersText); setError(null); setTestResult(null); setMode("form"); }; const beginOAuth = ( args: { name: string; url: string; description: string; }, options?: { quickId?: string }, ) => { const validationError = getMcpUrlValidationError(args.url); if (validationError) { setError(validationError); setTestResult(null); return; } setBusy(true); setQuickBusyId(options?.quickId ?? null); const returnUrl = typeof window === "undefined" ? "/" : window.location.pathname + window.location.search + window.location.hash; navigateToMcpOAuthStart( agentNativePath( buildMcpOAuthStartUrl({ name: args.name, url: args.url, description: args.description, scope: safeDefaultScope, returnUrl, }), ), ); }; const connectWithOAuth = ( integration: DefaultMcpIntegration, options?: { quickId?: string }, ) => beginOAuth( { name: integration.name, url: integration.url, description: integration.description, }, options, ); const connectCustomWithOAuth = () => { if (!name.trim()) { setError(t("mcpIntegrations.serverNameRequired")); return; } beginOAuth({ name: name.trim(), url: url.trim(), description: description.trim(), }); }; const createServer = async ( args: CreateMcpServerArgs, options?: { quickId?: string }, ) => { const validationError = getMcpUrlValidationError(args.url); if (validationError) { setError(validationError); setTestResult(null); return; } if (options?.quickId) setQuickBusyId(options.quickId); setBusy(true); setError(null); try { await onCreateMcpServer(args); onOpenChange(false); onCreated?.(); } catch (err) { setError(formatMcpServerError(err)); } finally { setBusy(false); setQuickBusyId(null); } }; const submitForm = () => { const trimmedName = name.trim(); const trimmedUrl = url.trim(); if (!trimmedName || !trimmedUrl || busy) return; void createServer({ scope, name: trimmedName, url: trimmedUrl, headers: parseHeaderLines(headersText), description: description.trim() || undefined, }); }; const quickConnect = (integration: DefaultMcpIntegration) => { if ( integration.connectionMode === "manual" || integration.availability === "provider-setup" ) { return; } if (integration.authMode === "oauth") { connectWithOAuth(integration, { quickId: integration.id }); return; } if (integration.authMode === "headers") { openForm(integration); return; } void createServer( { scope: safeDefaultScope, name: integration.name, url: integration.url, description: integration.description, }, { quickId: integration.id }, ); }; const runTest = async () => { const trimmedUrl = url.trim(); if (!trimmedUrl || busy) return; const validationError = getMcpUrlValidationError(trimmedUrl); if (validationError) { setTestResult({ ok: false, message: validationError }); setError(null); return; } setBusy(true); setError(null); setTestResult(null); try { const res = await testMcpServerUrl( trimmedUrl, parseHeaderLines(headersText), ); setTestResult( res.ok ? { ok: true, message: t("mcpIntegrations.toolsAvailable", { count: res.toolCount ?? 0, }), } : { ok: false, message: res.error ?? t("mcpIntegrations.failed") }, ); } catch (err) { setTestResult({ ok: false, message: formatMcpServerError(err) }); } finally { setBusy(false); } }; const renderScopeSelector = () => { const orgTooltip = !hasOrg ? t("mcpIntegrations.orgNoOrg") : !canCreateOrgMcp ? t("mcpIntegrations.orgAdminOnly") : null; return (
{orgTooltip && {orgTooltip}}
); }; if (!showCatalog && !customIntegrationEnabled) return null; return ( {mode === "catalog" ? ( <> {t("mcpIntegrations.title")} {t("mcpIntegrations.description", { count: defaultIntegrations.length, })}
{error && (
{error}
)}
{filteredIntegrations.map((integration) => { const apiFallback = getMcpIntegrationApiFallback(integration); const connected = connectedUrls.has( compareUrl(integration.url), ); const requiresHeaders = integration.authMode === "headers"; const setupOnly = integration.connectionMode === "manual" || integration.availability === "provider-setup" || integration.availability === "client-restricted"; return (

{integration.name}

{integration.availability !== "ready" && ( {integration.availability === "beta" ? t("mcpIntegrations.status.beta") : integration.availability === "client-restricted" ? t("mcpIntegrations.status.clientRestricted") : t("mcpIntegrations.status.setupRequired")} )} {integration.verification === "verified" ? t("mcpIntegrations.status.verified") : integration.verification === "restricted" ? t("mcpIntegrations.status.restricted") : t("mcpIntegrations.status.preflightOnly")}

{t(integration.descriptionKey)}

{connected ? ( ) : setupOnly ? ( <> {apiFallback && ( )} {t("mcpIntegrations.viewSetup")} ) : ( )} {integration.docsUrl && ( {t("mcpIntegrations.docsLabel", { name: integration.name, })} )}
); })}
{filteredIntegrations.length === 0 && (
{t("mcpIntegrations.noMatches")}
)}
) : ( <> {selected ? t("mcpIntegrations.configureTitle", { name: selected.name, }) : t("mcpIntegrations.customTitle")} {selected ? selected.authMode === "none" ? t("mcpIntegrations.presetNoAuthDescription") : t("mcpIntegrations.presetAuthDescription") : t("mcpIntegrations.customDescription")}
{renderScopeSelector()} {selected?.setupNoteKey && (
{t(selected.setupNoteKey)}
)} {selected?.authMode === "oauth" && (
{t("mcpIntegrations.oauthNotice")}
)} {selected?.authMode !== "oauth" && (