1099 lines · 38.7 KB · TypeScript
RawDownload
import { useReducer, useCallback, useEffect, useRef, useState, type ReactNode } 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 {  CardStack,  CardStackContent,  CardStackEntry,  CardStackEntryActions,  CardStackEntryContent,  CardStackEntryDescription,  CardStackEntryField,  CardStackEntryMedia,  CardStackEntryTitle,} from "@executor/react/components/card-stack";import { FieldError, FieldLabel } from "@executor/react/components/field";import { FilterTabs } from "@executor/react/components/filter-tabs";import { FloatActions } from "@executor/react/components/float-actions";import { Input } from "@executor/react/components/input";import { Label } from "@executor/react/components/label";import { Badge } from "@executor/react/components/badge";import { Skeleton } from "@executor/react/components/skeleton";import { SourceFavicon } from "@executor/react/components/source-favicon";import { IOSSpinner, Spinner } from "@executor/react/components/spinner";import { Textarea } from "@executor/react/components/textarea";import { HeadersList } from "@executor/react/plugins/headers-list";import { type HeaderState } from "@executor/react/plugins/secret-header-auth";import {  displayNameFromUrl,  slugifyNamespace,  SourceIdentityFields,  useSourceIdentity,} from "@executor/react/plugins/source-identity";import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets"; type RemoteAuthMode = "none" | "header" | "oauth2";import { sourceWriteKeys } from "@executor/react/api/reactivity-keys";import { usePendingSources } from "@executor/react/api/optimistic";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);} // Stable SDK connection id for an MCP source's OAuth sign-in. The same// id is passed into `startOAuth` AND stored on the source's auth// config, so at invoke time the per-user scope (which owns the user's// own secrets) resolves via shadowing over the source-level pointer.// Keyed by namespace so reconnecting mints a new connection that// shadows the previous one at the same scope.const mcpOAuthConnectionId = (namespaceSlug: string): string =>  `mcp-oauth2-${namespaceSlug || "default"}`; // ---------------------------------------------------------------------------// State machine (remote flow)// --------------------------------------------------------------------------- type OAuthTokens = {  /** Id of the SDK Connection minted by the exchange. The UI stores it   *  on the source's auth config as `{kind: "oauth2", connectionId}`. */  connectionId: string;  tokenType: string;  expiresAt: number | null;  scope: string | null;  /** Source-level OAuth state captured during the flow. Persisted on   *  the source's auth config so refreshes + future user OAuth flows   *  re-use the same DCR client + skip discovery. */  clientInformation: Record<string, unknown> | null;  authorizationServerUrl: string | null;  resourceMetadataUrl: string | null;}; type ProbeResult = {  connected: boolean;  requiresOAuth: 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 }  | { 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;  /** 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 doProbe = useAtomSet(probeMcpEndpoint, { mode: "promise" });  const doAdd = useAtomSet(addMcpSource, { mode: "promise" });  const doStartOAuth = useAtomSet(startMcpOAuth, { mode: "promise" });  const { beginAdd } = usePendingSources();  const secretList = useSecretPickerSecrets();   const [remoteAuthMode, setRemoteAuthMode] = useState<RemoteAuthMode>("none");  const [remoteAuthHeaders, setRemoteAuthHeaders] = useState<HeaderState[]>([    {      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 remoteIdentity = useSourceIdentity({    fallbackName:      probe?.serverName ?? probe?.name ?? displayNameFromUrl(state.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 remoteAuthHeader = remoteAuthHeaders[0];  const headerAuthComplete = Boolean(remoteAuthHeader?.name.trim() && remoteAuthHeader?.secretId);  const remoteHeadersComplete = remoteHeaders.every(    (header) => header.name.trim() && header.value.trim(),  );  // OAuth is "ready to save" even without tokens — the source is stored  // with a stable connectionId pointer, and each user completes their  // own sign-in via McpSignInButton on the source detail page (per-user  // scope shadowing means each user's tokens land at their own scope).  const authReady =    remoteAuthMode === "none"      ? canUseNone      : remoteAuthMode === "header"        ? headerAuthComplete        : true;  const canAdd = Boolean(probe) && authReady && remoteHeadersComplete && !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" });    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]);   // 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 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 namespaceSlug =        slugifyNamespace(remoteIdentity.namespace) ||        slugifyNamespace(probe?.namespace ?? "") ||        "mcp";      const connectionId = mcpOAuthConnectionId(namespaceSlug);      const result = await doStartOAuth({        path: { scopeId },        payload: {          endpoint: state.url.trim(),          redirectUrl,          connectionId,        },      });      dispatch({ type: "oauth-waiting", sessionId: result.sessionId });      oauthCleanup.current = openOAuthPopup(        result.authorizationUrl,        (data) => {          oauthCleanup.current = null;          if (data.ok) {            dispatch({              type: "oauth-ok",              tokens: {                connectionId: data.connectionId,                tokenType: data.tokenType,                expiresAt: data.expiresAt,                scope: data.scope,                clientInformation: data.clientInformation ?? null,                authorizationServerUrl: data.authorizationServerUrl ?? null,                resourceMetadataUrl: data.resourceMetadataUrl ?? null,              },            });          } 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, remoteIdentity.namespace, probe?.namespace]);   const handleCancelOAuth = useCallback(() => {    oauthCleanup.current?.();    oauthCleanup.current = null;    dispatch({ type: "oauth-cancelled" });  }, []);   const handleAddRemote = useCallback(async () => {    if (!probe) return;    dispatch({ type: "add-start" });    const headerAuth = remoteAuthHeaders[0];    // For oauth2 sources saved without completing the flow, use the

Lines 1–500 of 1099.