import { useEffect, useRef, useState } from "react"; import type { AuthNeededItem } from "../../../timeline"; import { useAppContext } from "../../app-context"; import { apiErrorFacts, userErrorText } from "../../../ui/api-error"; import { hasWorkspacePermission } from "../../lib/permissions"; import { Button } from "../../../ui/button"; import { Input } from "../../../ui/input"; import { Label } from "../../../ui/label"; import { SessionCapabilityFrame } from "../../../components/session-capability-frame"; import { PreparedMcpSetupCard } from "./prepared-mcp-setup-card"; type Props = { item: AuthNeededItem; workspaceId: string; sessionId: string; onConfigured?: (() => Promise) | undefined; onRegistered: (id: string, restoreFocus: boolean) => void; }; /** A proposed URL is untrusted display data until the human submits this form. * Creation uses the ordinary human-only Capabilities endpoint; the existing * capability card then handles authentication, enabling, and session selection. */ export function SessionCustomMcpCard(props: Props) { const context = useAppContext(); if (props.item.setupRequest?.mcpSetup) return ( ); return ; } function LegacyCustomMcpCard({ item, workspaceId, onRegistered }: Props) { const context = useAppContext(); const proposal = item.setupRequest!; const [expanded, setExpanded] = useState(false); const [name, setName] = useState(proposal.name); const [url, setUrl] = useState(proposal.endpointUrl); const [description, setDescription] = useState(""); const [busy, setBusy] = useState(false); const [checking, setChecking] = useState(true); const [error, setError] = useState(null); const expandedRef = useRef(expanded); expandedRef.current = expanded; useEffect(() => { let active = true; // Read-only lookup lets anyone return to the existing connection card after // an admin adds the server; only a human with manage permission may create it. void context.client .listCapabilities(workspaceId) .then((catalog) => { if (!active) return; const existing = catalog.items.find( (entry) => entry.kind === "mcp" && entry.endpointUrl === proposal.endpointUrl && !entry.stale, ); if (existing) onRegistered(existing.id, expandedRef.current); }) .catch(() => { // Submission reads the catalog again and reports any uncertainty. }) .finally(() => { if (active) setChecking(false); }); return () => { active = false; }; }, [context.client, workspaceId, proposal.endpointUrl, onRegistered]); const canManage = hasWorkspacePermission( context.accessContext, workspaceId, "capabilities:manage", ); const validUrl = (() => { try { const parsed = new URL(url.trim()); return ( parsed.protocol === "https:" && Boolean(parsed.hostname) && !parsed.username && !parsed.password && !parsed.hash && !parsed.search ); } catch { return false; } })(); async function submit(event: React.FormEvent) { event.preventDefault(); if (busy || checking || !canManage || !name.trim() || !validUrl) return; setBusy(true); setError(null); try { // A replayed card, or an uncertain creation response, must not write a // second catalog row when the endpoint is already present. const existing = (await context.client.listCapabilities(workspaceId)).items.find( (entry) => entry.kind === "mcp" && entry.endpointUrl === url.trim() && !entry.stale, ); if (existing) { onRegistered(existing.id, true); return; } const created = await context.client.createCapability(workspaceId, { kind: "mcp", source: "manual", name: name.trim(), endpointUrl: url.trim(), ...(description.trim() ? { description: description.trim() } : {}), }); onRegistered(created.id, true); } catch (failure) { // Read back before offering a retry: the write may have committed even // if the response was lost. Never silently repeat an uncertain POST. try { const matching = (await context.client.listCapabilities(workspaceId)).items.find( (entry) => entry.kind === "mcp" && entry.endpointUrl === url.trim() && !entry.stale, ); if (matching) { onRegistered(matching.id, true); return; } } catch { // Preserve the uncertainty in the visible error below. } // A refusal or a rejected field is final, so say what to change. Anything // else (a lost response, a server failure) may have committed. const status = apiErrorFacts(failure).status; setError( status !== undefined && status < 500 ? `Couldn't add this server. ${userErrorText(failure)}` : "Couldn't confirm this server was added. Check Connections before trying again; the request may have succeeded.", ); } finally { setBusy(false); } } return ( setExpanded(true)} onClose={() => setExpanded(false)} busy={busy} >
void submit(event)}>

This server was suggested by the agent. Check that you trust its address before adding it to this workspace.

setName(event.target.value)} autoFocus />
setUrl(event.target.value)} aria-describedby={`mcp-url-help-${item.id}`} />

HTTPS only. No query parameters or secrets in the URL; add credentials in the next step. Tools may receive data you choose to share.

setDescription(event.target.value)} placeholder="What this connection is for" />
{!canManage ? (

A workspace admin needs to add this server. You can share its URL with them.

) : null} {error ? (

{error}

) : null}
); }