import { useReducer, useCallback, useEffect, useRef, useState } from "react";import { useAtomSet } from "@effect-atom/atom-react"; import { useScope } from "@executor/react/api/scope-context";import { Button } from "@executor/react/components/button";import { Input } from "@executor/react/components/input";import { Label } from "@executor/react/components/label";import { Badge } from "@executor/react/components/badge";import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group";import { Spinner } from "@executor/react/components/spinner";import { Textarea } from "@executor/react/components/textarea";import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth";import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets";import { probeMcpEndpoint, addMcpSource, startMcpOAuth } from "./atoms";import { mcpPresets, type McpPreset } from "../sdk/presets"; // ---------------------------------------------------------------------------// Preset lookup// --------------------------------------------------------------------------- function findPreset(id: string | undefined): McpPreset | undefined { if (!id) return undefined; return mcpPresets.find((p) => p.id === id);} // ---------------------------------------------------------------------------// State machine (remote flow)// --------------------------------------------------------------------------- type OAuthTokens = { accessTokenSecretId: string; refreshTokenSecretId: string | null; tokenType: string; expiresAt: number | null; scope: string | null;}; type ProbeResult = { connected: boolean; requiresOAuth: boolean; name: string; namespace: string; toolCount: number | null; serverName: string | null;}; type RemoteAuthMode = "none" | "header" | "oauth2"; type PlainHeader = { name: string; value: string;}; type State = | { step: "url"; url: string } | { step: "probing"; url: string } | { step: "probed"; url: string; probe: ProbeResult } | { step: "oauth-starting"; url: string; probe: ProbeResult } | { step: "oauth-waiting"; url: string; probe: ProbeResult; sessionId: string } | { step: "oauth-done"; url: string; probe: ProbeResult; tokens: OAuthTokens } | { step: "adding"; url: string; probe: ProbeResult; tokens: OAuthTokens | null } | { step: "error"; url: string; probe: ProbeResult | null; tokens: OAuthTokens | null; error: string; }; type Action = | { type: "set-url"; url: string } | { type: "probe-start" } | { type: "probe-ok"; probe: ProbeResult } | { type: "probe-fail"; error: string } | { type: "oauth-start" } | { type: "oauth-waiting"; sessionId: string } | { type: "oauth-ok"; tokens: OAuthTokens } | { type: "oauth-fail"; error: string } | { type: "oauth-cancelled" } | { type: "add-start" } | { type: "add-fail"; error: string } | { type: "retry" }; const init: State = { step: "url", url: "" }; function reducer(state: State, action: Action): State { switch (action.type) { case "set-url": return { step: "url", url: action.url }; case "probe-start": return { step: "probing", url: state.url }; case "probe-ok": return { step: "probed", url: state.url, probe: action.probe }; case "probe-fail": return { step: "error", url: state.url, probe: null, tokens: null, error: action.error }; case "oauth-start": if (state.step !== "probed" && state.step !== "error") return state; return { step: "oauth-starting", url: state.url, probe: state.step === "probed" ? state.probe : state.probe!, }; case "oauth-waiting": if (state.step !== "oauth-starting") return state; return { step: "oauth-waiting", url: state.url, probe: state.probe, sessionId: action.sessionId, }; case "oauth-ok": if (state.step !== "oauth-waiting") return state; return { step: "oauth-done", url: state.url, probe: state.probe, tokens: action.tokens }; case "oauth-fail": if (state.step !== "oauth-starting" && state.step !== "oauth-waiting") return state; return { step: "error", url: state.url, probe: state.probe, tokens: null, error: action.error, }; case "oauth-cancelled": if (state.step !== "oauth-waiting") return state; return { step: "probed", url: state.url, probe: state.probe }; case "add-start": { const tokens = state.step === "oauth-done" ? state.tokens : state.step === "probed" ? null : null; const probe = "probe" in state ? state.probe : null; if (!probe) return state; return { step: "adding", url: state.url, probe, tokens }; } case "add-fail": if (state.step !== "adding") return state; return { step: "error", url: state.url, probe: state.probe, tokens: state.tokens, error: action.error, }; case "retry": { if (state.step !== "error") return state; return state.probe ? state.tokens ? { step: "oauth-done", url: state.url, probe: state.probe, tokens: state.tokens } : { step: "probed", url: state.url, probe: state.probe } : { step: "url", url: state.url }; } default: return state; }} // ---------------------------------------------------------------------------// OAuth popup// --------------------------------------------------------------------------- type OAuthPopupResult = | ({ type: "executor:oauth-result"; ok: true; sessionId: string } & OAuthTokens) | { type: "executor:oauth-result"; ok: false; sessionId: null; error: string }; const OAUTH_RESULT_CHANNEL = "executor:mcp-oauth-result"; const isOAuthPopupResult = (value: unknown): value is OAuthPopupResult => typeof value === "object" && value !== null && (value as { type?: unknown }).type === "executor:oauth-result"; function openOAuthPopup( url: string, onResult: (data: OAuthPopupResult) => void, onOpenFailed?: () => void,): () => void { const w = 600, h = 700; const left = window.screenX + (window.outerWidth - w) / 2; const top = window.screenY + (window.outerHeight - h) / 2; let settled = false; const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(OAUTH_RESULT_CHANNEL) : null; const settle = () => { if (settled) return; settled = true; window.removeEventListener("message", onMsg); channel?.close(); }; const handleResult = (data: unknown) => { if (!isOAuthPopupResult(data) || settled) return; settle(); onResult(data); }; const onMsg = (e: MessageEvent) => { if (e.origin === window.location.origin) handleResult(e.data); }; window.addEventListener("message", onMsg); if (channel) channel.onmessage = (e) => handleResult(e.data); const popup = window.open( url, "mcp-oauth", `width=${w},height=${h},left=${left},top=${top},popup=1`, ); if (!popup && !settled) { settle(); queueMicrotask(() => onOpenFailed?.()); } return settle;} // ---------------------------------------------------------------------------// Component// --------------------------------------------------------------------------- export default function AddMcpSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string; initialPreset?: string;}) { const preset = findPreset(props.initialPreset); const isStdioPreset = preset?.transport === "stdio"; const [transport, setTransport] = useState<"remote" | "stdio">( isStdioPreset ? "stdio" : "remote", ); // --- Stdio state --- const [stdioCommand, setStdioCommand] = useState(isStdioPreset ? preset.command : ""); const [stdioArgs, setStdioArgs] = useState( isStdioPreset && preset.args ? preset.args.join(" ") : "", ); const [stdioEnv, setStdioEnv] = useState(""); const [stdioName, setStdioName] = useState(isStdioPreset ? preset.name : ""); const [stdioAdding, setStdioAdding] = useState(false); const [stdioError, setStdioError] = useState<string | null>(null); // --- Remote state --- const remoteUrl = !isStdioPreset && preset?.transport === undefined && preset?.url ? preset.url : (props.initialUrl ?? ""); const [state, dispatch] = useReducer( reducer, remoteUrl ? { step: "url" as const, url: remoteUrl } : init, ); const scopeId = useScope(); const doProbe = useAtomSet(probeMcpEndpoint, { mode: "promise" }); const doAdd = useAtomSet(addMcpSource, { mode: "promise" }); const doStartOAuth = useAtomSet(startMcpOAuth, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const [remoteAuthMode, setRemoteAuthMode] = useState<RemoteAuthMode>("none"); const [remoteHeaderAuth, setRemoteHeaderAuth] = useState<{ name: string; prefix?: string; presetKey?: string; secretId: string | null; }>({ name: "Authorization", prefix: "Bearer ", presetKey: "bearer", secretId: null, }); const [remoteHeaders, setRemoteHeaders] = useState<PlainHeader[]>([]); const probe = "probe" in state ? state.probe : null; const tokens = "tokens" in state ? state.tokens : null; const isIdle = state.step === "url"; const isProbing = state.step === "probing"; const isAdding = state.step === "adding"; const isOAuthBusy = state.step === "oauth-starting" || state.step === "oauth-waiting"; const canUseNone = probe?.requiresOAuth !== true; const headerAuthComplete = Boolean(remoteHeaderAuth.name.trim() && remoteHeaderAuth.secretId); const remoteHeadersComplete = remoteHeaders.every( (header) => header.name.trim() && header.value.trim(), ); const authReady = remoteAuthMode === "none" ? canUseNone : remoteAuthMode === "header" ? headerAuthComplete : tokens !== null; const canAdd = Boolean(probe) && authReady && remoteHeadersComplete && !isAdding && !isOAuthBusy; const error = state.step === "error" ? state.error : null; // ---- Remote actions ---- const handleProbe = useCallback(async () => { dispatch({ type: "probe-start" }); try { const result = await doProbe({ path: { scopeId }, payload: { endpoint: state.url.trim() }, }); setRemoteAuthMode(result.requiresOAuth ? "oauth2" : "none"); dispatch({ type: "probe-ok", probe: result }); } catch (e) { dispatch({ type: "probe-fail", error: e instanceof Error ? e.message : "Failed to connect" }); } }, [state.url, scopeId, doProbe]); const autoProbed = useRef(false); useEffect(() => { if (transport === "remote" && remoteUrl && !autoProbed.current) { autoProbed.current = true; handleProbe(); } }, []); // eslint-disable-line react-hooks/exhaustive-deps const oauthCleanup = useRef<(() => void) | null>(null); const handleOAuth = useCallback(async () => { oauthCleanup.current?.(); oauthCleanup.current = null; dispatch({ type: "oauth-start" }); try { const redirectUrl = `${window.location.origin}/api/mcp/oauth/callback`; const result = await doStartOAuth({ path: { scopeId }, payload: { endpoint: state.url.trim(), redirectUrl }, }); dispatch({ type: "oauth-waiting", sessionId: result.sessionId }); oauthCleanup.current = openOAuthPopup( result.authorizationUrl, (data) => { oauthCleanup.current = null; if (data.ok) { dispatch({ type: "oauth-ok", tokens: { accessTokenSecretId: data.accessTokenSecretId, refreshTokenSecretId: data.refreshTokenSecretId, tokenType: data.tokenType, expiresAt: data.expiresAt, scope: data.scope, }, }); } else { dispatch({ type: "oauth-fail", error: data.error }); } }, () => { oauthCleanup.current = null; dispatch({ type: "oauth-fail", error: "OAuth popup was blocked" }); }, ); } catch (e) { dispatch({ type: "oauth-fail", error: e instanceof Error ? e.message : "Failed to start OAuth", }); } }, [state.url, scopeId, doStartOAuth]); const handleCancelOAuth = useCallback(() => { oauthCleanup.current?.(); oauthCleanup.current = null; dispatch({ type: "oauth-cancelled" }); }, []); const handleAddRemote = useCallback(async () => { if (!probe) return; dispatch({ type: "add-start" }); try { const auth = remoteAuthMode === "header" ? { kind: "header" as const, headerName: remoteHeaderAuth.name.trim(), secretId: remoteHeaderAuth.secretId!, ...(remoteHeaderAuth.prefix ? { prefix: remoteHeaderAuth.prefix } : {}), } : remoteAuthMode === "oauth2" && tokens ? { kind: "oauth2" as const, accessTokenSecretId: tokens.accessTokenSecretId, refreshTokenSecretId: tokens.refreshTokenSecretId, tokenType: tokens.tokenType, expiresAt: tokens.expiresAt, scope: tokens.scope, } : { kind: "none" as const }; const headers = Object.fromEntries( remoteHeaders .map((header) => [header.name.trim(), header.value.trim()] as const) .filter(([name, value]) => name && value), ); await doAdd({ path: { scopeId }, payload: { transport: "remote" as const, name: probe.serverName ?? probe.name, endpoint: state.url.trim(), auth, ...(Object.keys(headers).length > 0 ? { headers } : {}), }, }); props.onComplete(); } catch (e) { dispatch({ type: "add-fail", error: e instanceof Error ? e.message : "Failed to add source", }); } }, [ probe, remoteAuthMode, remoteHeaderAuth, remoteHeaders, tokens, state.url, doAdd, props, scopeId, ]); // ---- Stdio actions ---- const parseStdioArgs = (raw: string): string[] => { if (!raw.trim()) return []; const args: string[] = []; const regex = /[^\s"]+|"([^"]*)"/g; let match; while ((match = regex.exec(raw)) !== null) { args.push(match[1] ?? match[0]); } return args; }; const parseStdioEnv = (raw: string): Record<string, string> | undefined => { if (!raw.trim()) return undefined; const env: Record<string, string> = {}; for (const line of raw.split("\n")) { const eq = line.indexOf("="); if (eq > 0) { env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim(); } } return Object.keys(env).length > 0 ? env : undefined; }; const handleAddStdio = useCallback(async () => { const cmd = stdioCommand.trim(); if (!cmd) return; setStdioAdding(true); setStdioError(null); try { await doAdd({ path: { scopeId }, payload: { transport: "stdio" as const, name: stdioName.trim() || cmd, command: cmd, args: parseStdioArgs(stdioArgs), env: parseStdioEnv(stdioEnv), }, }); props.onComplete(); } catch (e) { setStdioError(e instanceof Error ? e.message : "Failed to add source"); setStdioAdding(false); } }, [stdioCommand, stdioArgs, stdioEnv, stdioName, doAdd, scopeId, props]); // ---- Render ---- return ( <div className="space-y-6"> <div> <h1 className="text-xl font-semibold text-foreground">Add MCP Source</h1> <p className="mt-1 text-[13px] text-muted-foreground"> Connect to an MCP server to discover and use its tools. </p> </div> {/* Transport toggle */} <div className="flex gap-1 rounded-lg border border-border bg-muted/30 p-1"> <Button variant="ghost" type="button" onClick={() => setTransport("remote")}Lines 1–500 of 989. Load more
import { useReducer, useCallback, useEffect, useRef, useState } from "react"; import { useAtomSet } from "@effect-atom/atom-react"; import { useScope } from "@executor/react/api/scope-context"; import { Button } from "@executor/react/components/button"; import { Input } from "@executor/react/components/input"; import { Label } from "@executor/react/components/label"; import { Badge } from "@executor/react/components/badge"; import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group"; import { Spinner } from "@executor/react/components/spinner"; import { Textarea } from "@executor/react/components/textarea"; import { SecretHeaderAuthRow } from "@executor/react/plugins/secret-header-auth"; import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets"; import { probeMcpEndpoint, addMcpSource, startMcpOAuth } from "./atoms"; import { mcpPresets, type McpPreset } from "../sdk/presets"; // --------------------------------------------------------------------------- // Preset lookup // --------------------------------------------------------------------------- function findPreset(id: string | undefined): McpPreset | undefined { if (!id) return undefined; return mcpPresets.find((p) => p.id === id); } // --------------------------------------------------------------------------- // State machine (remote flow) // --------------------------------------------------------------------------- type OAuthTokens = { accessTokenSecretId: string; refreshTokenSecretId: string | null; tokenType: string; expiresAt: number | null; scope: string | null; }; type ProbeResult = { connected: boolean; requiresOAuth: boolean; name: string; namespace: string; toolCount: number | null; serverName: string | null; }; type RemoteAuthMode = "none" | "header" | "oauth2"; type PlainHeader = { name: string; value: string; }; type State = | { step: "url"; url: string } | { step: "probing"; url: string } | { step: "probed"; url: string; probe: ProbeResult } | { step: "oauth-starting"; url: string; probe: ProbeResult } | { step: "oauth-waiting"; url: string; probe: ProbeResult; sessionId: string } | { step: "oauth-done"; url: string; probe: ProbeResult; tokens: OAuthTokens } | { step: "adding"; url: string; probe: ProbeResult; tokens: OAuthTokens | null } | { step: "error"; url: string; probe: ProbeResult | null; tokens: OAuthTokens | null; error: string; }; type Action = | { type: "set-url"; url: string } | { type: "probe-start" } | { type: "probe-ok"; probe: ProbeResult } | { type: "probe-fail"; error: string } | { type: "oauth-start" } | { type: "oauth-waiting"; sessionId: string } | { type: "oauth-ok"; tokens: OAuthTokens } | { type: "oauth-fail"; error: string } | { type: "oauth-cancelled" } | { type: "add-start" } | { type: "add-fail"; error: string } | { type: "retry" }; const init: State = { step: "url", url: "" }; function reducer(state: State, action: Action): State { switch (action.type) { case "set-url": return { step: "url", url: action.url }; case "probe-start": return { step: "probing", url: state.url }; case "probe-ok": return { step: "probed", url: state.url, probe: action.probe }; case "probe-fail": return { step: "error", url: state.url, probe: null, tokens: null, error: action.error }; case "oauth-start": if (state.step !== "probed" && state.step !== "error") return state; return { step: "oauth-starting", url: state.url, probe: state.step === "probed" ? state.probe : state.probe!, }; case "oauth-waiting": if (state.step !== "oauth-starting") return state; return { step: "oauth-waiting", url: state.url, probe: state.probe, sessionId: action.sessionId, }; case "oauth-ok": if (state.step !== "oauth-waiting") return state; return { step: "oauth-done", url: state.url, probe: state.probe, tokens: action.tokens }; case "oauth-fail": if (state.step !== "oauth-starting" && state.step !== "oauth-waiting") return state; return { step: "error", url: state.url, probe: state.probe, tokens: null, error: action.error, }; case "oauth-cancelled": if (state.step !== "oauth-waiting") return state; return { step: "probed", url: state.url, probe: state.probe }; case "add-start": { const tokens = state.step === "oauth-done" ? state.tokens : state.step === "probed" ? null : null; const probe = "probe" in state ? state.probe : null; if (!probe) return state; return { step: "adding", url: state.url, probe, tokens }; } case "add-fail": if (state.step !== "adding") return state; return { step: "error", url: state.url, probe: state.probe, tokens: state.tokens, error: action.error, }; case "retry": { if (state.step !== "error") return state; return state.probe ? state.tokens ? { step: "oauth-done", url: state.url, probe: state.probe, tokens: state.tokens } : { step: "probed", url: state.url, probe: state.probe } : { step: "url", url: state.url }; } default: return state; } } // --------------------------------------------------------------------------- // OAuth popup // --------------------------------------------------------------------------- type OAuthPopupResult = | ({ type: "executor:oauth-result"; ok: true; sessionId: string } & OAuthTokens) | { type: "executor:oauth-result"; ok: false; sessionId: null; error: string }; const OAUTH_RESULT_CHANNEL = "executor:mcp-oauth-result"; const isOAuthPopupResult = (value: unknown): value is OAuthPopupResult => typeof value === "object" && value !== null && (value as { type?: unknown }).type === "executor:oauth-result"; function openOAuthPopup( url: string, onResult: (data: OAuthPopupResult) => void, onOpenFailed?: () => void, ): () => void { const w = 600, h = 700; const left = window.screenX + (window.outerWidth - w) / 2; const top = window.screenY + (window.outerHeight - h) / 2; let settled = false; const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(OAUTH_RESULT_CHANNEL) : null; const settle = () => { if (settled) return; settled = true; window.removeEventListener("message", onMsg); channel?.close(); }; const handleResult = (data: unknown) => { if (!isOAuthPopupResult(data) || settled) return; settle(); onResult(data); }; const onMsg = (e: MessageEvent) => { if (e.origin === window.location.origin) handleResult(e.data); }; window.addEventListener("message", onMsg); if (channel) channel.onmessage = (e) => handleResult(e.data); const popup = window.open( url, "mcp-oauth", `width=${w},height=${h},left=${left},top=${top},popup=1`, ); if (!popup && !settled) { settle(); queueMicrotask(() => onOpenFailed?.()); } return settle; } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export default function AddMcpSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string; initialPreset?: string; }) { const preset = findPreset(props.initialPreset); const isStdioPreset = preset?.transport === "stdio"; const [transport, setTransport] = useState<"remote" | "stdio">( isStdioPreset ? "stdio" : "remote", ); // --- Stdio state --- const [stdioCommand, setStdioCommand] = useState(isStdioPreset ? preset.command : ""); const [stdioArgs, setStdioArgs] = useState( isStdioPreset && preset.args ? preset.args.join(" ") : "", ); const [stdioEnv, setStdioEnv] = useState(""); const [stdioName, setStdioName] = useState(isStdioPreset ? preset.name : ""); const [stdioAdding, setStdioAdding] = useState(false); const [stdioError, setStdioError] = useState<string | null>(null); // --- Remote state --- const remoteUrl = !isStdioPreset && preset?.transport === undefined && preset?.url ? preset.url : (props.initialUrl ?? ""); const [state, dispatch] = useReducer( reducer, remoteUrl ? { step: "url" as const, url: remoteUrl } : init, ); const scopeId = useScope(); const doProbe = useAtomSet(probeMcpEndpoint, { mode: "promise" }); const doAdd = useAtomSet(addMcpSource, { mode: "promise" }); const doStartOAuth = useAtomSet(startMcpOAuth, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const [remoteAuthMode, setRemoteAuthMode] = useState<RemoteAuthMode>("none"); const [remoteHeaderAuth, setRemoteHeaderAuth] = useState<{ name: string; prefix?: string; presetKey?: string; secretId: string | null; }>({ name: "Authorization", prefix: "Bearer ", presetKey: "bearer", secretId: null, }); const [remoteHeaders, setRemoteHeaders] = useState<PlainHeader[]>([]); const probe = "probe" in state ? state.probe : null; const tokens = "tokens" in state ? state.tokens : null; const isIdle = state.step === "url"; const isProbing = state.step === "probing"; const isAdding = state.step === "adding"; const isOAuthBusy = state.step === "oauth-starting" || state.step === "oauth-waiting"; const canUseNone = probe?.requiresOAuth !== true; const headerAuthComplete = Boolean(remoteHeaderAuth.name.trim() && remoteHeaderAuth.secretId); const remoteHeadersComplete = remoteHeaders.every( (header) => header.name.trim() && header.value.trim(), ); const authReady = remoteAuthMode === "none" ? canUseNone : remoteAuthMode === "header" ? headerAuthComplete : tokens !== null; const canAdd = Boolean(probe) && authReady && remoteHeadersComplete && !isAdding && !isOAuthBusy; const error = state.step === "error" ? state.error : null; // ---- Remote actions ---- const handleProbe = useCallback(async () => { dispatch({ type: "probe-start" }); try { const result = await doProbe({ path: { scopeId }, payload: { endpoint: state.url.trim() }, }); setRemoteAuthMode(result.requiresOAuth ? "oauth2" : "none"); dispatch({ type: "probe-ok", probe: result }); } catch (e) { dispatch({ type: "probe-fail", error: e instanceof Error ? e.message : "Failed to connect" }); } }, [state.url, scopeId, doProbe]); const autoProbed = useRef(false); useEffect(() => { if (transport === "remote" && remoteUrl && !autoProbed.current) { autoProbed.current = true; handleProbe(); } }, []); // eslint-disable-line react-hooks/exhaustive-deps const oauthCleanup = useRef<(() => void) | null>(null); const handleOAuth = useCallback(async () => { oauthCleanup.current?.(); oauthCleanup.current = null; dispatch({ type: "oauth-start" }); try { const redirectUrl = `${window.location.origin}/api/mcp/oauth/callback`; const result = await doStartOAuth({ path: { scopeId }, payload: { endpoint: state.url.trim(), redirectUrl }, }); dispatch({ type: "oauth-waiting", sessionId: result.sessionId }); oauthCleanup.current = openOAuthPopup( result.authorizationUrl, (data) => { oauthCleanup.current = null; if (data.ok) { dispatch({ type: "oauth-ok", tokens: { accessTokenSecretId: data.accessTokenSecretId, refreshTokenSecretId: data.refreshTokenSecretId, tokenType: data.tokenType, expiresAt: data.expiresAt, scope: data.scope, }, }); } else { dispatch({ type: "oauth-fail", error: data.error }); } }, () => { oauthCleanup.current = null; dispatch({ type: "oauth-fail", error: "OAuth popup was blocked" }); }, ); } catch (e) { dispatch({ type: "oauth-fail", error: e instanceof Error ? e.message : "Failed to start OAuth", }); } }, [state.url, scopeId, doStartOAuth]); const handleCancelOAuth = useCallback(() => { oauthCleanup.current?.(); oauthCleanup.current = null; dispatch({ type: "oauth-cancelled" }); }, []); const handleAddRemote = useCallback(async () => { if (!probe) return; dispatch({ type: "add-start" }); try { const auth = remoteAuthMode === "header" ? { kind: "header" as const, headerName: remoteHeaderAuth.name.trim(), secretId: remoteHeaderAuth.secretId!, ...(remoteHeaderAuth.prefix ? { prefix: remoteHeaderAuth.prefix } : {}), } : remoteAuthMode === "oauth2" && tokens ? { kind: "oauth2" as const, accessTokenSecretId: tokens.accessTokenSecretId, refreshTokenSecretId: tokens.refreshTokenSecretId, tokenType: tokens.tokenType, expiresAt: tokens.expiresAt, scope: tokens.scope, } : { kind: "none" as const }; const headers = Object.fromEntries( remoteHeaders .map((header) => [header.name.trim(), header.value.trim()] as const) .filter(([name, value]) => name && value), ); await doAdd({ path: { scopeId }, payload: { transport: "remote" as const, name: probe.serverName ?? probe.name, endpoint: state.url.trim(), auth, ...(Object.keys(headers).length > 0 ? { headers } : {}), }, }); props.onComplete(); } catch (e) { dispatch({ type: "add-fail", error: e instanceof Error ? e.message : "Failed to add source", }); } }, [ probe, remoteAuthMode, remoteHeaderAuth, remoteHeaders, tokens, state.url, doAdd, props, scopeId, ]); // ---- Stdio actions ---- const parseStdioArgs = (raw: string): string[] => { if (!raw.trim()) return []; const args: string[] = []; const regex = /[^\s"]+|"([^"]*)"/g; let match; while ((match = regex.exec(raw)) !== null) { args.push(match[1] ?? match[0]); } return args; }; const parseStdioEnv = (raw: string): Record<string, string> | undefined => { if (!raw.trim()) return undefined; const env: Record<string, string> = {}; for (const line of raw.split("\n")) { const eq = line.indexOf("="); if (eq > 0) { env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim(); } } return Object.keys(env).length > 0 ? env : undefined; }; const handleAddStdio = useCallback(async () => { const cmd = stdioCommand.trim(); if (!cmd) return; setStdioAdding(true); setStdioError(null); try { await doAdd({ path: { scopeId }, payload: { transport: "stdio" as const, name: stdioName.trim() || cmd, command: cmd, args: parseStdioArgs(stdioArgs), env: parseStdioEnv(stdioEnv), }, }); props.onComplete(); } catch (e) { setStdioError(e instanceof Error ? e.message : "Failed to add source"); setStdioAdding(false); } }, [stdioCommand, stdioArgs, stdioEnv, stdioName, doAdd, scopeId, props]); // ---- Render ---- return ( <div className="space-y-6"> <div> <h1 className="text-xl font-semibold text-foreground">Add MCP Source</h1> <p className="mt-1 text-[13px] text-muted-foreground"> Connect to an MCP server to discover and use its tools. </p> </div> {/* Transport toggle */} <div className="flex gap-1 rounded-lg border border-border bg-muted/30 p-1"> <Button variant="ghost" type="button" onClick={() => setTransport("remote")}