import { authorizeConnectAttempt, ConnectPopupClosedError, ConnectController, createBrowserConnectNavigation, reserveBrowserConnectNavigation, type ConnectAttempt, type ConnectNavigation, type ConnectOwnership, } from "@opengeni/connect"; import type { CapabilityCatalogItem } from "@opengeni/sdk"; import type { OpenGeniBrowserClient as OpenGeniClient } from "@opengeni/sdk/browser"; import { useEffect, useRef, useState } from "react"; import { useConnect } from "../hooks/use-connect"; import { attachSessionCapability, sessionCapabilityTools } from "../session-capability-policy"; import { SessionCapabilityFrame } from "./session-capability-frame"; import { matchingActiveMcpConnections } from "../mcp-connection-status"; import { useOpenGeniText } from "../messages/runtime"; export type SessionMcpCapabilityCardProps = { client: OpenGeniClient; workspaceId: string; sessionId: string; capabilityId: string; name: string; rationale?: string; /** Exact host route; no console-specific routing is assumed. */ returnUrl: string; onConfigured?: (() => void | Promise) | undefined; }; export type McpConnectionCardProps = Omit & { /** Omit for connection management: no session tool selection is written. */ sessionId?: string; dialogOnly?: boolean; onClose?: (() => void) | undefined; /** * The provider only ever connects someone's own account (official Gmail, * Slack's hosted MCP): no ownership choice, always personal. */ personalOnly?: boolean; /** The connect button's words, "Connect Gmail". Defaults to "Continue to ". */ connectLabel?: string; /** The line under the dialog title. Defaults to the kind and provider domain. */ dialogSubtitle?: string; /** The host's product copy for this connection, instead of the catalog description. */ description?: string; /** The host's logo for this connection, when the catalog has no asset. */ logoSrc?: string | null; /** The host's words for the ownership choice. */ ownershipCopy?: { legend: string; workspace: string; personal: string }; }; /** Native OAuth recommendation flow. Identity and endpoint come from the live * catalog, never from model-authored recommendation text. */ export function SessionMcpCapabilityCard(props: SessionMcpCapabilityCardProps) { return ; } export function McpConnectionCard(props: McpConnectionCardProps) { const [client, setClient] = useState(props.client); const [generation, setGeneration] = useState(0); if (client !== props.client) { setClient(props.client); setGeneration(generation + 1); } return ( ); } function ScopedCard({ client, workspaceId, sessionId, capabilityId, name, rationale = "", returnUrl, onConfigured, dialogOnly = false, onClose, personalOnly = false, connectLabel, dialogSubtitle, description: hostDescription, logoSrc, ownershipCopy, }: McpConnectionCardProps) { const t = useOpenGeniText(); const [controller] = useState( () => new ConnectController(client.connectTransport(), workspaceId), ); const view = useConnect(controller); const [item, setItem] = useState(null); const [logo, setLogo] = useState(null); const [expanded, setExpanded] = useState(dialogOnly); const [complete, setComplete] = useState(false); const [connected, setConnected] = useState(false); const [waiting, setWaiting] = useState(false); const [reconciling, setReconciling] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [retryFresh, setRetryFresh] = useState(false); const [ownership, setOwnership] = useState( personalOnly ? "personal" : "workspace", ); const lifetime = useRef(null); const authorization = useRef(null); const operation = useRef(null); const reservedPopup = useRef<(() => void) | null>(null); const startKey = useRef(crypto.randomUUID()); const advanceKey = useRef(crypto.randomUUID()); const startInput = useRef(null); const storageKey = sessionId ? `opengeni:session-connect:${workspaceId}:${sessionId}:${capabilityId}` : `opengeni:workspace-connect:${workspaceId}:${capabilityId}`; const current = () => !!lifetime.current && !lifetime.current.signal.aborted; async function load(active: () => boolean = current) { const invocation = lifetime.current; const [catalog, session] = await Promise.all([ client.listCapabilities(workspaceId), sessionId ? client.getSession(workspaceId, sessionId) : Promise.resolve(null), ]); if (!invocation || invocation.signal.aborted || lifetime.current !== invocation || !active()) return null; let resolved = catalog.items.find((entry) => entry.id === capabilityId); // Custom catalog entries may not declare authentication. Discover it from // the live catalog's endpoint, never from recommendation/host copy. if (resolved?.kind === "mcp" && !resolved.enabled && !resolved.authKind) { const endpoint = resolved.mcpUrl ?? resolved.endpointUrl; if (endpoint) { const inspected = await client.inspectMcpAuthentication(workspaceId, endpoint); if (invocation.signal.aborted || lifetime.current !== invocation || !active()) return null; if (inspected.kind === "oauth2") resolved = { ...resolved, authKind: "oauth2" }; else if (inspected.kind === "unknown") throw new Error( inspected.message ?? t("sessionMcpCapabilityCard.couldNotDetermineHowTo"), ); } } if (!resolved || resolved.kind !== "mcp" || resolved.authKind !== "oauth2") throw new Error(t("sessionMcpCapabilityCard.thisRecommendationIsNotAn")); setItem(resolved); if (resolved.connectionRef) setOwnership(resolved.connectionRef.subjectScope === "subject" ? "personal" : "workspace"); else if ( !item && (personalOnly || resolved.metadata?.defaultConnectionOwnership === "personal") ) setOwnership("personal"); let accountReady = false; if (resolved.enabled && resolved.connectionRef) { const connections = resolved.connectionRef.subjectScope === "subject" ? await client.listOwnConnectionAccounts(workspaceId) : await client.listConnections(workspaceId); if (invocation.signal.aborted || lifetime.current !== invocation || !active()) return null; const matches = matchingActiveMcpConnections(resolved, connections); accountReady = matches.length === 1; } if (invocation.signal.aborted || lifetime.current !== invocation || !active()) return null; const selected = session?.toolPolicy.mode === "workspace_default" ? session.effectiveToolPolicy?.selectedIds : session?.tools.filter((tool) => tool.kind === "mcp").map((tool) => tool.id); setConnected(accountReady); setComplete( accountReady && !!selected && sessionCapabilityTools(resolved).every((tool) => selected.includes(tool.id)), ); return resolved; } useEffect(() => { const abort = new AbortController(); lifetime.current = abort; // Mount reads are scoped to this effect, not to the user-action lock. // Strict Mode may cancel the first mount while its promise is still in // flight; that must not suppress the replacement mount's initialization. if (dialogOnly) setBusy(true); void (async () => { await load(); if (abort.signal.aborted || !dialogOnly) return; await recoverSavedAttempt(); })() .catch((failure) => { if (!abort.signal.aborted) setError( failure instanceof Error ? failure.message : t("sessionMcpCapabilityCard.couldnTLoadConnectionDetails"), ); }) .finally(() => { if (!abort.signal.aborted && dialogOnly) setBusy(false); }); return () => { abort.abort(); }; // ScopedCard remounts for client/workspace/session/capability changes. // These reads initialize that scope; rerunning on action closures would // cancel initialization whenever its own state updates render the card. // eslint-disable-next-line react-hooks/exhaustive-deps }, [client, workspaceId, sessionId, capabilityId, dialogOnly, t]); useEffect(() => { const path = item?.logoAssetPath; setLogo(null); if (!path?.startsWith("catalog-assets/")) return; const abort = new AbortController(); let objectUrl: string | null = null; // Embeds can require authentication even for the API's public catalog // asset route. Use the client's transport instead of an unauthenticated img. void client .downloadCatalogAsset(path, { signal: abort.signal }) .then((blob) => { if (abort.signal.aborted || !blob.type.startsWith("image/") || blob.size > 2_000_000) return; objectUrl = URL.createObjectURL(blob); setLogo(objectUrl); }) .catch(() => { /* Keep the provider's initials if its passive mark is unavailable. */ }); return () => { abort.abort(); if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [client, item?.logoAssetPath]); async function run(action: (active: () => boolean) => Promise) { if (operation.current || !current()) return; const pending = new AbortController(); operation.current = pending; const active = () => current() && operation.current === pending && !pending.signal.aborted; setBusy(true); setError(null); setNotice(null); setRetryFresh(false); try { await action(active); } catch (failure) { if (active()) { if (failure instanceof ConnectPopupClosedError) setNotice(failure.message); else setError( failure instanceof Error ? failure.message : t("sessionMcpCapabilityCard.connectionSetupCouldNotFinish"), ); } } finally { if (operation.current === pending) { operation.current = null; if (current()) setBusy(false); } } } function remember(attemptId: string) { // An opaque recovery hint only. The backend rechecks the authenticated // owner on recover; this value never grants access or proves completion. try { sessionStorage.setItem(storageKey, attemptId); } catch { /* Storage may be disabled. */ } } async function reconcile(attempt: ConnectAttempt, active: () => boolean = current) { if (!active()) return; if (attempt.state !== "complete" || !attempt.account || !attempt.credentialsCommitted) return; const resolved = await load(active); if (!resolved || !active()) return; const connection = (await client.listConnections(workspaceId)).find( (entry) => entry.id === attempt.account!.id, ); if (!active()) return; if ( !connection || connection.status !== "active" || connection.kind !== "oauth2" || connection.metadata.mcpUrl !== (resolved.mcpUrl ?? resolved.endpointUrl) || (connection.subjectId !== null) !== (attempt.ownership === "personal") ) throw new Error(t("sessionMcpCapabilityCard.theAuthorizedAccountDoesNot")); const previous = resolved.connectionRef; if ( resolved.enabled && previous?.subjectScope !== "subject" && previous?.connectionId && previous.connectionId !== connection.id ) throw new Error(t("sessionMcpCapabilityCard.thisIntegrationSSelectedAccount")); await client.enableCapability(workspaceId, capabilityId, { connectionRef: attempt.ownership === "personal" ? { providerDomain: connection.providerDomain, kind: "oauth2", subjectScope: "subject" } : { providerDomain: connection.providerDomain, kind: "oauth2", subjectScope: "workspace", connectionId: connection.id, }, }); if (!active()) return; const enabled = await load(active); if (enabled && active()) { await finishConnection(enabled, active); } if (!active()) return; try { sessionStorage.removeItem(storageKey); } catch { /* Recovery hint only. */ } } async function open() { setExpanded(true); await run(async (active) => { await load(active); if (!active()) return; await recoverSavedAttempt(active); }); } async function recoverSavedAttempt(active: () => boolean = current) { let saved: string | null = null; try { saved = sessionStorage.getItem(storageKey); } catch { /* No persisted attempt. */ } if (saved) { const attempt = await controller.recover(saved); if (!active()) return; if (["failed", "expired", "cancelled"].includes(attempt.state)) setRetryFresh(true); if (attempt.state === "complete") setReconciling(true); try { await reconcile(attempt, active); } finally { if (active()) setReconciling(false); } } } async function begin(fresh = false) { await run(async (active) => { if ( fresh || (view.attempt && ["failed", "expired", "cancelled"].includes(view.attempt.state)) ) { startKey.current = crypto.randomUUID(); advanceKey.current = crypto.randomUUID(); } const reserved = reserveBrowserConnectNavigation(window); reservedPopup.current = reserved.close; try { const resolved = await load(active); if (!resolved || !active()) return; if ( resolved.connectionRef && ownership !== (resolved.connectionRef.subjectScope === "subject" ? "personal" : "workspace") ) throw new Error(t("sessionMcpCapabilityCard.theConnectionOwnershipChangedReview")); const mcpUrl = resolved.mcpUrl ?? resolved.endpointUrl; if (!mcpUrl) throw new Error(t("sessionMcpCapabilityCard.theCurrentCatalogHasNo")); // Never silently replace an already selected account. Reauthorization // stays on its exact native connection ID. let reconnectAccountId = resolved.connectionRef?.connectionId ?? undefined; if (resolved.connectionRef?.subjectScope === "subject") { const connections = (await client.listConnections(workspaceId)).filter( (entry) => entry.subjectId !== null && entry.kind === "oauth2" && entry.metadata.mcpUrl === mcpUrl, ); if (!active()) return; if (connections.length > 1) throw new Error(t("sessionMcpCapabilityCard.moreThanOnePersonalAccount")); reconnectAccountId = connections[0]?.id; } // A closed request can still have reached the server. Keep its key for // an exact replay, but never reuse it when ownership or another input // changes while the user reopens setup. const input = JSON.stringify({ ownership, returnUrl, reconnectAccountId, mcpUrl }); if (startInput.current !== null && startInput.current !== input) { startKey.current = crypto.randomUUID(); advanceKey.current = crypto.randomUUID(); } startInput.current = input; let attempt = await controller.begin({ providerId: "mcp-oauth", ownership, returnUrl, idempotencyKey: startKey.current, ...(reconnectAccountId ? { reconnectAccountId } : {}), }); if (!active()) return; remember(attempt.id); if (attempt.state === "credential_input") attempt = await controller.advance( { type: "credentials", values: { mcpUrl } }, advanceKey.current, ); if (!active()) return; if (attempt.nextAction.type === "authorize") await performAuthorization(attempt, reserved.navigation, active); else await reconcile(attempt, active); } finally { if (reservedPopup.current === reserved.close) reservedPopup.current = null; reserved.close(); } }); } async function performAuthorization( attempt: ConnectAttempt, navigation: ConnectNavigation, active: () => boolean, ) { if (!active()) return; const pending = new AbortController(); authorization.current = pending; const abortOnUnmount = () => pending.abort(lifetime.current?.signal.reason); lifetime.current!.signal.addEventListener("abort", abortOnUnmount, { once: true }); setWaiting(true); try { const result = await authorizeConnectAttempt(controller.transport, attempt, navigation, { mode: "popup", signal: pending.signal, }); if (!active() || !result) return; setWaiting(false); setReconciling(true); await reconcile(result, active); if (!active()) return; if (result.state === "cancelled") { setNotice(t("sessionMcpCapabilityCard.signInWasCancelledYou")); setRetryFresh(true); return; } if (result.state !== "complete") { if (["failed", "expired"].includes(result.state)) setRetryFresh(true); throw new Error(t("sessionMcpCapabilityCard.signInDidNotFinish")); } } finally { lifetime.current?.signal.removeEventListener("abort", abortOnUnmount); if (authorization.current === pending) authorization.current = null; if (active()) { setReconciling(false); setWaiting(false); } } } function authorize(attempt: ConnectAttempt) { // run invokes action synchronously, retaining the browser click gesture. return run((active) => performAuthorization(attempt, createBrowserConnectNavigation(window), active), ); } async function finishConnection( capability: CapabilityCatalogItem, active: () => boolean = current, ) { if (!sessionId) { // Connection management does not select session tools. await onConfigured?.(); if (!active()) return; setComplete(true); setExpanded(false); onClose?.(); return; } if (capability.connectionRef && capability.connectionRef.subjectScope !== "subject") { const selected = (await client.listConnections(workspaceId)).find( (entry) => entry.id === capability.connectionRef?.connectionId, ); if (!active()) return; if (!selected || selected.status !== "active") throw new Error(t("sessionMcpCapabilityCard.thisAccountNeedsReconnectionBefore")); } if (capability.connectionRef?.subjectScope === "subject") { const accounts = await client.listOwnConnectionAccounts(workspaceId); if (!active()) return; if (matchingActiveMcpConnections(capability, accounts).length === 0) throw new Error(t("sessionMcpCapabilityCard.yourAccountNeedsReconnectionBefore")); } if (!active()) return; await attachSessionCapability(t, client, workspaceId, sessionId, capability, active); if (!active()) return; await onConfigured?.(); if (!active()) return; setComplete(true); setExpanded(false); } async function useHere() { await run(async (active) => { if (item) await finishConnection(item, active); }); } function close() { const pending = operation.current; if (pending) { operation.current = null; pending.abort(new ConnectPopupClosedError()); authorization.current?.abort(new ConnectPopupClosedError()); authorization.current = null; reservedPopup.current?.(); setBusy(false); setWaiting(false); setReconciling(false); } setExpanded(false); onClose?.(); } return ( void open()} onClose={close} dialogOnly={dialogOnly} >
{error ?

{error}

: null} {notice ? (

{notice}

) : null} {!item ? ( <>

{busy ? t("sessionMcpCapabilityCard.loadingConnectionDetails") : t("sessionMcpCapabilityCard.connectionDetailsAreUnavailable")}

) : ( <>

{hostDescription || item.description || rationale}

{busy ? ( <>

{waiting ? t("sessionMcpCapabilityCard.finishSigningInWithName", { name: item.name }) : reconciling ? t("sessionMcpCapabilityCard.finishingYourConnection") : t("sessionMcpCapabilityCard.preparingYourConnection")}

{waiting ? ( ) : null} ) : !connected ? ( <> {!item.connectionRef && !personalOnly ? (
{ownershipCopy?.legend ?? t("sessionMcpCapabilityCard.whoCanUseThisConnection")}
) : null}

{personalOnly && ownership === "personal" ? t("sessionMcpCapabilityCard.connectsYourOwnAccountOnly") : ownershipCopy ? ownership === "workspace" ? ownershipCopy.workspace : ownershipCopy.personal : ownership === "workspace" ? sessionId ? t("sessionMcpCapabilityCard.thisConnectionWillBeAvailable") : t("sessionMcpCapabilityCard.thisConnectionWillBeAvailable2") : t("sessionMcpCapabilityCard.thisConnectionBelongsToYou")}

) : null} {connected && !busy ? ( <>

{sessionId ? t("sessionMcpCapabilityCard.yourAccountIsConnectedEnable") : t("sessionMcpCapabilityCard.yourAccountIsConnected")}

) : null} )}
); }