import { IconArrowUpRight, IconLoader2, IconPlugConnected, IconX, } from "@tabler/icons-react"; import { useEffect, useMemo, useState } from "react"; import { agentNativePath } from "../api-path.js"; import { openAgentSettings } from "../CommandMenu.js"; import { useT } from "../i18n.js"; import { clearMcpConnectionResume, notifyMcpConnectionComplete, saveMcpConnectionResume, } from "./mcp-connection-resume.js"; import { buildMcpOAuthStartUrl, findMcpIntegrationForText, getMcpIntegrationApiFallback, getDefaultMcpIntegrations, isMcpConnectionFailureText, navigateToMcpOAuthStart, type DefaultMcpIntegration, } from "./mcp-integration-catalog.js"; import { McpIntegrationDialog } from "./McpIntegrationDialog.js"; import { useCreateMcpServer, useMcpServers, type McpServer, } from "./use-mcp-servers.js"; export type McpConnectionSuggestionVariant = "composer" | "response"; export interface McpConnectionSuggestionProps { text: string; contextText?: string; variant?: McpConnectionSuggestionVariant; integrations?: DefaultMcpIntegration[]; } export function findMcpConnectionSuggestionIntegration({ text, contextText = "", variant = "composer", integrations = getDefaultMcpIntegrations(), }: McpConnectionSuggestionProps): DefaultMcpIntegration | null { return findMcpIntegrationForText( variant === "response" ? contextText : text, integrations, ); } function compareUrl(value: string): string { try { const url = new URL(value.trim()); url.hash = ""; return url.toString().replace(/\/+$/, ""); } catch { return value.trim().replace(/\/+$/, ""); } } function isConnected( integration: DefaultMcpIntegration, servers: McpServer[], ): boolean { const targetUrl = compareUrl(integration.url); return servers.some( (server) => server.status.state === "connected" && compareUrl(server.url) === targetUrl, ); } function returnUrl(): string { if (typeof window === "undefined") return "/"; return ( window.location.pathname + window.location.search + window.location.hash ); } function canStartOAuth(integration: DefaultMcpIntegration): boolean { return ( integration.authMode === "oauth" && integration.connectionMode === "oauth" && integration.availability === "ready" ); } function hasApiFallback( apiFallback: DefaultMcpIntegration["apiFallback"] | null, ): boolean { return Boolean(apiFallback); } export function McpConnectionSuggestion({ text, contextText = "", variant = "composer", integrations: integrationOptions, }: McpConnectionSuggestionProps) { const t = useT(); const mcpServersQuery = useMcpServers(); const createMcpServer = useCreateMcpServer(); const [dialogOpen, setDialogOpen] = useState(false); const [dismissedId, setDismissedId] = useState(null); const [connecting, setConnecting] = useState(false); const [error, setError] = useState(null); const integrations = useMemo( () => integrationOptions ?? getDefaultMcpIntegrations(), [integrationOptions], ); const integration = useMemo( () => findMcpConnectionSuggestionIntegration({ text, contextText, variant, integrations, }), [contextText, integrations, text, variant], ); const apiFallback = integration ? getMcpIntegrationApiFallback(integration) : null; const servers = useMemo( () => [ ...(mcpServersQuery.data?.user ?? []), ...(mcpServersQuery.data?.org ?? []), ], [mcpServersQuery.data], ); const connected = integration ? isConnected(integration, servers) : false; const shouldSuggest = mcpServersQuery.isSuccess && integration && !connected && dismissedId !== integration.id && (variant === "composer" || isMcpConnectionFailureText(text)); useEffect(() => { setError(null); setConnecting(false); }, [integration?.id, variant]); if (!shouldSuggest) return null; const connect = async () => { if (!integration || connecting) return; setError(null); if (apiFallback) { openAgentSettings(`secrets:${apiFallback.secretKey}`); return; } if (canStartOAuth(integration)) { setConnecting(true); saveMcpConnectionResume(variant === "response" ? contextText : text); navigateToMcpOAuthStart( agentNativePath( buildMcpOAuthStartUrl({ name: integration.name, url: integration.url, description: integration.description, scope: "user", returnUrl: returnUrl(), }), ), ); return; } if ( integration.authMode === "none" && integration.connectionMode === "direct" ) { saveMcpConnectionResume(variant === "response" ? contextText : text); setConnecting(true); try { await createMcpServer.mutateAsync({ scope: "user", name: integration.name, url: integration.url, description: integration.description, }); setDismissedId(integration.id); notifyMcpConnectionComplete(); } catch (cause) { clearMcpConnectionResume(); setError( cause instanceof Error ? cause.message : t("mcpIntegrations.failed"), ); } finally { setConnecting(false); } return; } saveMcpConnectionResume(variant === "response" ? contextText : text); setDialogOpen(true); }; const actionLabel = hasApiFallback(apiFallback) ? t("mcpIntegrations.useApiToken") : canStartOAuth(integration) ? t("mcpIntegrations.connectWithOAuth") : integration.availability === "client-restricted" || integration.availability === "provider-setup" ? t("mcpIntegrations.viewSetup") : t("mcpIntegrations.connect"); return ( <>
{integration.logoUrl && ( { event.currentTarget.hidden = true; }} /> )}
{t( hasApiFallback(apiFallback) ? "mcpIntegrations.connectSuggestionWithApiToken" : "mcpIntegrations.connectSuggestion", { name: integration.name }, )}
{error && (
{error}
)} { setDialogOpen(open); if (!open) clearMcpConnectionResume(); }} initialIntegrationId={integration.id} defaultScope="user" canCreateOrgMcp={false} hasOrg={Boolean(mcpServersQuery.data?.orgId)} onCreateMcpServer={(args) => createMcpServer.mutateAsync(args)} onCreated={() => { setDismissedId(integration.id); saveMcpConnectionResume(variant === "response" ? contextText : text); notifyMcpConnectionComplete(); }} integrations={integrations} /> ); }