import { useReducer, useCallback, useEffect, useRef, useState, type ReactNode } from "react";import { useAtomSet } from "@effect/atom-react";import * as Exit from "effect/Exit";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; import { useScope } from "@executor-js/react/api/scope-context";import { Button } from "@executor-js/react/components/button";import { CardStack, CardStackContent, CardStackEntry, CardStackEntryField,} from "@executor-js/react/components/card-stack";import { FieldLabel } from "@executor-js/react/components/field";import { FilterTabs } from "@executor-js/react/components/filter-tabs";import { FloatActions } from "@executor-js/react/components/float-actions";import { Input } from "@executor-js/react/components/input";import { Label } from "@executor-js/react/components/label";import { Spinner } from "@executor-js/react/components/spinner";import { Textarea } from "@executor-js/react/components/textarea";import { emptyHttpCredentials, httpCredentialsValid, HttpCredentialsEditor, serializeScopedHttpCredentials, serializeHttpCredentials,} from "@executor-js/react/plugins/http-credentials";import { sourceDisplayNameFromUrl, slugifyNamespace, SourceIdentityFields, useSourceIdentity,} from "@executor-js/react/plugins/source-identity";import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";import { oauthCallbackUrl, oauthConnectionId, useOAuthPopupFlow, type OAuthCompletionPayload,} from "@executor-js/react/plugins/oauth-sign-in";import { CredentialControlField, CredentialUsageRow, useCredentialTargetScope,} from "@executor-js/react/plugins/credential-target-scope"; type RemoteAuthMode = "none" | "oauth2";import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";import { probeMcpEndpoint, addMcpSourceOptimistic } from "./atoms";import { McpRemoteSourceFields } from "./McpRemoteSourceFields";import { mcpPresets, type McpPreset } from "../sdk/presets";import { MCP_OAUTH_CONNECTION_SLOT, type McpCredentialInput } from "../sdk/types"; const ErrorMessage = Schema.Struct({ message: Schema.String });const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), { onNone: () => fallback, onSome: ({ message }) => message, }); // ---------------------------------------------------------------------------// 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 = OAuthCompletionPayload; type ProbeResult = { connected: boolean; requiresOAuth: boolean; supportsDynamicRegistration: boolean; name: string; namespace: string; toolCount: number | null; serverName: string | null;}; type PlainHeader = { name: string; value: string;}; type State = | { step: "url"; url: string } | { step: "probing"; url: string; probe: ProbeResult | null } | { 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: "oauth-reset" } | { 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, probe: "probe" in state ? state.probe : null }; 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 "oauth-reset": if ("probe" in state && state.probe) { return { step: "probed", url: state.url, probe: state.probe }; } return state; 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; }} // ---------------------------------------------------------------------------// Component// --------------------------------------------------------------------------- export default function AddMcpSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string; initialPreset?: string; /** Whether the stdio transport is enabled on the server. */ allowStdio?: boolean;}) { const allowStdio = props.allowStdio ?? false; const rawPreset = findPreset(props.initialPreset); // Drop stdio presets when stdio is disabled — the caller should have // already filtered these out, but defence-in-depth. const preset = rawPreset?.transport === "stdio" && !allowStdio ? undefined : rawPreset; const isStdioPreset = preset?.transport === "stdio"; const [transport, setTransport] = useState<"remote" | "stdio">( isStdioPreset && allowStdio ? "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 stdioIdentity = useSourceIdentity({ fallbackName: isStdioPreset ? preset.name : stdioCommand, }); 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 { credentialTargetScope: requestCredentialTargetScope } = useCredentialTargetScope(); const { credentialTargetScope: oauthCredentialTargetScope, setCredentialTargetScope: setOAuthCredentialTargetScope, credentialScopeOptions, } = useCredentialTargetScope(); const doProbe = useAtomSet(probeMcpEndpoint, { mode: "promiseExit" }); const doAdd = useAtomSet(addMcpSourceOptimistic(scopeId), { mode: "promiseExit", }); const secretList = useSecretPickerSecrets(); const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({ popupName: "mcp-oauth", popupBlockedMessage: "OAuth popup was blocked", detectPopupClosed: false, startErrorMessage: "Failed to start OAuth", }); const [remoteAuthMode, setRemoteAuthMode] = useState<RemoteAuthMode>("none"); const [remoteHeaders, setRemoteHeaders] = useState<PlainHeader[]>([]); const [remoteCredentials, setRemoteCredentials] = useState(() => emptyHttpCredentials()); const probe = "probe" in state ? state.probe : null; const tokens = "tokens" in state ? state.tokens : null; const remoteIdentity = useSourceIdentity({ fallbackName: sourceDisplayNameFromUrl(state.url, "MCP") ?? probe?.serverName ?? probe?.name ?? "", }); const isProbing = state.step === "probing"; const isAdding = state.step === "adding"; const isOAuthBusy = state.step === "oauth-starting" || state.step === "oauth-waiting" || oauth.busy; const canUseNone = probe?.requiresOAuth !== true || probe.supportsDynamicRegistration === false; const remoteHeadersComplete = remoteHeaders.every( (header) => header.name.trim() && header.value.trim(), ); const remoteCredentialsComplete = httpCredentialsValid(remoteCredentials); const authReady = remoteAuthMode === "none" ? canUseNone : tokens !== null; const canAdd = Boolean(probe) && authReady && remoteHeadersComplete && remoteCredentialsComplete && !isAdding && !isOAuthBusy; // Probe failures are shown inline on the URL field; other failures // (OAuth start, add source) render in the bottom error block. const probeError = state.step === "error" && state.probe === null ? state.error : null; const otherError = state.step === "error" && state.probe !== null ? state.error : null; // ---- Remote actions ---- const handleProbe = useCallback(async () => { dispatch({ type: "probe-start" }); const { headers, queryParams } = serializeHttpCredentials(remoteCredentials); const exit = await doProbe({ params: { scopeId }, payload: { endpoint: state.url.trim(), ...(Object.keys(headers).length > 0 ? { headers } : {}), ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), }, }); if (Exit.isFailure(exit)) { dispatch({ type: "probe-fail", error: errorMessageFromExit(exit, "Failed to connect"), }); return; } setRemoteAuthMode(exit.value.requiresOAuth ? "oauth2" : "none"); dispatch({ type: "probe-ok", probe: exit.value }); }, [state.url, scopeId, doProbe, remoteCredentials]); // Keep the latest handleProbe in a ref so the debounced effect can call it // without depending on its identity (which changes every render). const handleProbeRef = useRef(handleProbe); handleProbeRef.current = handleProbe; // Auto-probe whenever the URL changes (debounced) while we're on the // remote transport and not already probing/probed. useEffect(() => { if (transport !== "remote") return; if (state.step !== "url") return; const trimmed = state.url.trim(); if (!trimmed) return; const handle = setTimeout(() => { handleProbeRef.current(); }, 400); return () => clearTimeout(handle); }, [transport, state.step, state.url]); const handleRemoteCredentialsChange = useCallback((next: typeof remoteCredentials) => { setRemoteCredentials(next); }, []); const handleOAuth = useCallback(async () => { dispatch({ type: "oauth-start" }); const namespaceSlug = slugifyNamespace(remoteIdentity.namespace) || slugifyNamespace(probe?.namespace ?? "") || "mcp"; const { headers, queryParams } = serializeHttpCredentials(remoteCredentials); await oauth.start({ payload: { endpoint: state.url.trim(), ...(Object.keys(headers).length > 0 ? { headers } : {}), ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), redirectUrl: oauthCallbackUrl(), connectionId: oauthConnectionId({ pluginId: "mcp", namespace: namespaceSlug, }), tokenScope: oauthCredentialTargetScope, strategy: { kind: "dynamic-dcr" }, pluginId: "mcp", identityLabel: `${remoteIdentity.name.trim() || probe?.serverName || probe?.name || "MCP"} OAuth`, }, onSuccess: (result) => { dispatch({ type: "oauth-ok", tokens: { connectionId: result.connectionId, expiresAt: result.expiresAt, scope: result.scope, }, }); }, onAuthorizationStarted: (result) => dispatch({ type: "oauth-waiting", sessionId: result.sessionId }), onError: (error) => dispatch({ type: "oauth-fail", error }), }); }, [state.url, remoteIdentity, probe, remoteCredentials, oauth, oauthCredentialTargetScope]); const handleCancelOAuth = useCallback(() => { oauth.cancel(); dispatch({ type: "oauth-cancelled" }); }, [oauth]); const handleAddRemote = useCallback(async () => { if (!probe) return; dispatch({ type: "add-start" }); const auth = remoteAuthMode === "oauth2" ? tokens ? { kind: "oauth2" as const, connectionId: tokens.connectionId, } : { kind: "oauth2" as const, connectionSlot: MCP_OAUTH_CONNECTION_SLOT, } : { kind: "none" as const }; const headers = Object.fromEntries( remoteHeaders .map((header) => [header.name.trim(), header.value.trim()] as const) .filter(([name, value]) => name && value), ); const credentials = serializeScopedHttpCredentials( remoteCredentials, requestCredentialTargetScope, ); const remoteRequestHeaders: Record<string, McpCredentialInput> = { ...headers, ...credentials.headers, }; const displayName = remoteIdentity.name.trim() || probe.serverName || probe.name; const slugNamespace = slugifyNamespace(remoteIdentity.namespace); const exit = await doAdd({ params: { scopeId }, payload: { targetScope: scopeId, transport: "remote" as const, name: displayName, namespace: slugNamespace || undefined, endpoint: state.url.trim(), auth, credentialTargetScope: remoteAuthMode === "oauth2" && tokens ? oauthCredentialTargetScope : requestCredentialTargetScope, ...(Object.keys(remoteRequestHeaders).length > 0 ? { headers: remoteRequestHeaders } : {}), ...(Object.keys(credentials.queryParams).length > 0 ? { queryParams: credentials.queryParams } : {}), }, reactivityKeys: sourceWriteKeys, }); if (Exit.isFailure(exit)) { dispatch({ type: "add-fail", error: errorMessageFromExit(exit, "Failed to add source"), }); return; } props.onComplete(); }, [ probe, remoteAuthMode, remoteHeaders, remoteCredentials, remoteIdentity, tokens, state.url, doAdd, props, scopeId, requestCredentialTargetScope, oauthCredentialTargetScope, ]); // ---- Stdio actions ----Lines 1–500 of 969. Load more
import { useReducer, useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useAtomSet } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { useScope } from "@executor-js/react/api/scope-context"; import { Button } from "@executor-js/react/components/button"; import { CardStack, CardStackContent, CardStackEntry, CardStackEntryField, } from "@executor-js/react/components/card-stack"; import { FieldLabel } from "@executor-js/react/components/field"; import { FilterTabs } from "@executor-js/react/components/filter-tabs"; import { FloatActions } from "@executor-js/react/components/float-actions"; import { Input } from "@executor-js/react/components/input"; import { Label } from "@executor-js/react/components/label"; import { Spinner } from "@executor-js/react/components/spinner"; import { Textarea } from "@executor-js/react/components/textarea"; import { emptyHttpCredentials, httpCredentialsValid, HttpCredentialsEditor, serializeScopedHttpCredentials, serializeHttpCredentials, } from "@executor-js/react/plugins/http-credentials"; import { sourceDisplayNameFromUrl, slugifyNamespace, SourceIdentityFields, useSourceIdentity, } from "@executor-js/react/plugins/source-identity"; import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets"; import { oauthCallbackUrl, oauthConnectionId, useOAuthPopupFlow, type OAuthCompletionPayload, } from "@executor-js/react/plugins/oauth-sign-in"; import { CredentialControlField, CredentialUsageRow, useCredentialTargetScope, } from "@executor-js/react/plugins/credential-target-scope"; type RemoteAuthMode = "none" | "oauth2"; import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { probeMcpEndpoint, addMcpSourceOptimistic } from "./atoms"; import { McpRemoteSourceFields } from "./McpRemoteSourceFields"; import { mcpPresets, type McpPreset } from "../sdk/presets"; import { MCP_OAUTH_CONNECTION_SLOT, type McpCredentialInput } from "../sdk/types"; const ErrorMessage = Schema.Struct({ message: Schema.String }); const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), { onNone: () => fallback, onSome: ({ message }) => message, }); // --------------------------------------------------------------------------- // 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 = OAuthCompletionPayload; type ProbeResult = { connected: boolean; requiresOAuth: boolean; supportsDynamicRegistration: boolean; name: string; namespace: string; toolCount: number | null; serverName: string | null; }; type PlainHeader = { name: string; value: string; }; type State = | { step: "url"; url: string } | { step: "probing"; url: string; probe: ProbeResult | null } | { 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: "oauth-reset" } | { 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, probe: "probe" in state ? state.probe : null }; 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 "oauth-reset": if ("probe" in state && state.probe) { return { step: "probed", url: state.url, probe: state.probe }; } return state; 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; } } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export default function AddMcpSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string; initialPreset?: string; /** Whether the stdio transport is enabled on the server. */ allowStdio?: boolean; }) { const allowStdio = props.allowStdio ?? false; const rawPreset = findPreset(props.initialPreset); // Drop stdio presets when stdio is disabled — the caller should have // already filtered these out, but defence-in-depth. const preset = rawPreset?.transport === "stdio" && !allowStdio ? undefined : rawPreset; const isStdioPreset = preset?.transport === "stdio"; const [transport, setTransport] = useState<"remote" | "stdio">( isStdioPreset && allowStdio ? "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 stdioIdentity = useSourceIdentity({ fallbackName: isStdioPreset ? preset.name : stdioCommand, }); 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 { credentialTargetScope: requestCredentialTargetScope } = useCredentialTargetScope(); const { credentialTargetScope: oauthCredentialTargetScope, setCredentialTargetScope: setOAuthCredentialTargetScope, credentialScopeOptions, } = useCredentialTargetScope(); const doProbe = useAtomSet(probeMcpEndpoint, { mode: "promiseExit" }); const doAdd = useAtomSet(addMcpSourceOptimistic(scopeId), { mode: "promiseExit", }); const secretList = useSecretPickerSecrets(); const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({ popupName: "mcp-oauth", popupBlockedMessage: "OAuth popup was blocked", detectPopupClosed: false, startErrorMessage: "Failed to start OAuth", }); const [remoteAuthMode, setRemoteAuthMode] = useState<RemoteAuthMode>("none"); const [remoteHeaders, setRemoteHeaders] = useState<PlainHeader[]>([]); const [remoteCredentials, setRemoteCredentials] = useState(() => emptyHttpCredentials()); const probe = "probe" in state ? state.probe : null; const tokens = "tokens" in state ? state.tokens : null; const remoteIdentity = useSourceIdentity({ fallbackName: sourceDisplayNameFromUrl(state.url, "MCP") ?? probe?.serverName ?? probe?.name ?? "", }); const isProbing = state.step === "probing"; const isAdding = state.step === "adding"; const isOAuthBusy = state.step === "oauth-starting" || state.step === "oauth-waiting" || oauth.busy; const canUseNone = probe?.requiresOAuth !== true || probe.supportsDynamicRegistration === false; const remoteHeadersComplete = remoteHeaders.every( (header) => header.name.trim() && header.value.trim(), ); const remoteCredentialsComplete = httpCredentialsValid(remoteCredentials); const authReady = remoteAuthMode === "none" ? canUseNone : tokens !== null; const canAdd = Boolean(probe) && authReady && remoteHeadersComplete && remoteCredentialsComplete && !isAdding && !isOAuthBusy; // Probe failures are shown inline on the URL field; other failures // (OAuth start, add source) render in the bottom error block. const probeError = state.step === "error" && state.probe === null ? state.error : null; const otherError = state.step === "error" && state.probe !== null ? state.error : null; // ---- Remote actions ---- const handleProbe = useCallback(async () => { dispatch({ type: "probe-start" }); const { headers, queryParams } = serializeHttpCredentials(remoteCredentials); const exit = await doProbe({ params: { scopeId }, payload: { endpoint: state.url.trim(), ...(Object.keys(headers).length > 0 ? { headers } : {}), ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), }, }); if (Exit.isFailure(exit)) { dispatch({ type: "probe-fail", error: errorMessageFromExit(exit, "Failed to connect"), }); return; } setRemoteAuthMode(exit.value.requiresOAuth ? "oauth2" : "none"); dispatch({ type: "probe-ok", probe: exit.value }); }, [state.url, scopeId, doProbe, remoteCredentials]); // Keep the latest handleProbe in a ref so the debounced effect can call it // without depending on its identity (which changes every render). const handleProbeRef = useRef(handleProbe); handleProbeRef.current = handleProbe; // Auto-probe whenever the URL changes (debounced) while we're on the // remote transport and not already probing/probed. useEffect(() => { if (transport !== "remote") return; if (state.step !== "url") return; const trimmed = state.url.trim(); if (!trimmed) return; const handle = setTimeout(() => { handleProbeRef.current(); }, 400); return () => clearTimeout(handle); }, [transport, state.step, state.url]); const handleRemoteCredentialsChange = useCallback((next: typeof remoteCredentials) => { setRemoteCredentials(next); }, []); const handleOAuth = useCallback(async () => { dispatch({ type: "oauth-start" }); const namespaceSlug = slugifyNamespace(remoteIdentity.namespace) || slugifyNamespace(probe?.namespace ?? "") || "mcp"; const { headers, queryParams } = serializeHttpCredentials(remoteCredentials); await oauth.start({ payload: { endpoint: state.url.trim(), ...(Object.keys(headers).length > 0 ? { headers } : {}), ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), redirectUrl: oauthCallbackUrl(), connectionId: oauthConnectionId({ pluginId: "mcp", namespace: namespaceSlug, }), tokenScope: oauthCredentialTargetScope, strategy: { kind: "dynamic-dcr" }, pluginId: "mcp", identityLabel: `${remoteIdentity.name.trim() || probe?.serverName || probe?.name || "MCP"} OAuth`, }, onSuccess: (result) => { dispatch({ type: "oauth-ok", tokens: { connectionId: result.connectionId, expiresAt: result.expiresAt, scope: result.scope, }, }); }, onAuthorizationStarted: (result) => dispatch({ type: "oauth-waiting", sessionId: result.sessionId }), onError: (error) => dispatch({ type: "oauth-fail", error }), }); }, [state.url, remoteIdentity, probe, remoteCredentials, oauth, oauthCredentialTargetScope]); const handleCancelOAuth = useCallback(() => { oauth.cancel(); dispatch({ type: "oauth-cancelled" }); }, [oauth]); const handleAddRemote = useCallback(async () => { if (!probe) return; dispatch({ type: "add-start" }); const auth = remoteAuthMode === "oauth2" ? tokens ? { kind: "oauth2" as const, connectionId: tokens.connectionId, } : { kind: "oauth2" as const, connectionSlot: MCP_OAUTH_CONNECTION_SLOT, } : { kind: "none" as const }; const headers = Object.fromEntries( remoteHeaders .map((header) => [header.name.trim(), header.value.trim()] as const) .filter(([name, value]) => name && value), ); const credentials = serializeScopedHttpCredentials( remoteCredentials, requestCredentialTargetScope, ); const remoteRequestHeaders: Record<string, McpCredentialInput> = { ...headers, ...credentials.headers, }; const displayName = remoteIdentity.name.trim() || probe.serverName || probe.name; const slugNamespace = slugifyNamespace(remoteIdentity.namespace); const exit = await doAdd({ params: { scopeId }, payload: { targetScope: scopeId, transport: "remote" as const, name: displayName, namespace: slugNamespace || undefined, endpoint: state.url.trim(), auth, credentialTargetScope: remoteAuthMode === "oauth2" && tokens ? oauthCredentialTargetScope : requestCredentialTargetScope, ...(Object.keys(remoteRequestHeaders).length > 0 ? { headers: remoteRequestHeaders } : {}), ...(Object.keys(credentials.queryParams).length > 0 ? { queryParams: credentials.queryParams } : {}), }, reactivityKeys: sourceWriteKeys, }); if (Exit.isFailure(exit)) { dispatch({ type: "add-fail", error: errorMessageFromExit(exit, "Failed to add source"), }); return; } props.onComplete(); }, [ probe, remoteAuthMode, remoteHeaders, remoteCredentials, remoteIdentity, tokens, state.url, doAdd, props, scopeId, requestCredentialTargetScope, oauthCredentialTargetScope, ]); // ---- Stdio actions ----