Rhys Sullivanmcp fixesd9605557 months agoHistory
1117 lines · 32.9 KB · TypeScript
RawDownload
import { startTransition, useEffect, useMemo, useState, type ReactNode } from "react";import type { Source } from "@executor/react";import {  Result,  defineExecutorPluginHttpApiClient,  useAtomSet,  useAtomValue,  useExecutorMutation,  useLocalInstallation,  useSource,} from "@executor/react";import {  Alert,  Badge,  Button,  IconPencil,  IconSpinner,  Input,  Label,  Select,  SourceToolExplorer,  Textarea,  parseSourceToolExplorerSearch,  type SourceToolExplorerSearch,  useSourcePluginNavigation,  useSourcePluginRouteParams,  useSourcePluginSearch,} from "@executor/react/plugins"; import {  mcpHttpApiExtension,} from "@executor/plugin-mcp-http";import {  type McpConnectInput,  type McpConnectionAuth,  type McpDiscoverResult,  type McpOAuthPopupResult,  type McpStartOAuthInput,} from "@executor/plugin-mcp-shared";import {  asMcpRemoteTransportValue,  defaultMcpRemoteTransportFields,  defaultMcpStdioTransportFields,  setMcpTransportFieldsTransport,  type McpTransportFields,  type McpTransportValue,} from "./transport";import {  parseJsonStringArray,  parseJsonStringMap,} from "./json";import {  buildMcpRemoteConfigKey,  mcpDiscoveryRequiresOAuth,} from "./oauth-detection"; const OAUTH_STORAGE_PREFIX = "executor:mcp-oauth:";const OAUTH_TIMEOUT_MS = 2 * 60_000; const getMcpHttpClient = defineExecutorPluginHttpApiClient<"McpReactHttpClient">()(  "McpReactHttpClient",  [mcpHttpApiExtension] as const,); const defaultMcpInput = (): McpConnectInput => ({  name: "My MCP Source",  endpoint: "https://example.com/mcp",  transport: "auto",  queryParams: null,  headers: null,  command: null,  args: null,  env: null,  cwd: null,  auth: {    kind: "none",  },}); const presetString = (  search: Record<string, unknown>,  key: string,): string | null => {  const value = search[key];  return typeof value === "string" && value.trim().length > 0    ? value.trim()    : null;}; const presetTransport = (  search: Record<string, unknown>,): McpConnectInput["transport"] => {  const value = presetString(search, "presetTransport");  return value === "auto" ||      value === "streamable-http" ||      value === "sse" ||      value === "stdio"    ? value    : null;}; const presetJsonStringMap = (  search: Record<string, unknown>,  key: string,): Record<string, string> | null => {  const value = presetString(search, key);  return value ? parseJsonStringMap(key, value) : null;}; const presetJsonStringArray = (  search: Record<string, unknown>,  key: string,): Array<string> | null => {  const value = presetString(search, key);  return value ? parseJsonStringArray(key, value) : null;}; const mcpInputFromSearch = (  search: Record<string, unknown>,): McpConnectInput => {  const defaults = defaultMcpInput();  const command = presetString(search, "presetCommand");  const args = presetJsonStringArray(search, "presetArgs");  const env = presetJsonStringMap(search, "presetEnv");  const cwd = presetString(search, "presetCwd");  const queryParams = presetJsonStringMap(search, "presetQueryParams");  const headers = presetJsonStringMap(search, "presetHeaders");  const transport = presetTransport(search)    ?? (command ? "stdio" : defaults.transport);   return {    ...defaults,    name: presetString(search, "presetName") ?? defaults.name,    endpoint:      command        ? null        : presetString(search, "presetEndpoint") ?? defaults.endpoint,    transport,    queryParams,    headers,    command,    args,    env,    cwd,  };}; const stringifyStringMap = (  value: Record<string, string> | null | undefined,): string =>  !value || Object.keys(value).length === 0    ? ""    : JSON.stringify(value, null, 2); const stringifyStringArray = (  value: ReadonlyArray<string> | null | undefined,): string =>  !value || value.length === 0 ? "" : JSON.stringify(value, null, 2); const transportFieldsFromInput = (input: McpConnectInput): McpTransportFields =>  input.transport === "stdio" || input.command    ? defaultMcpStdioTransportFields({        command: input.command ?? "",        argsText: stringifyStringArray(input.args),        envText: stringifyStringMap(input.env),        cwd: input.cwd ?? "",      })    : {        ...defaultMcpRemoteTransportFields(          asMcpRemoteTransportValue(input.transport),        ),        queryParamsText: stringifyStringMap(input.queryParams),        headersText: stringifyStringMap(input.headers),      }; const waitForOauthPopupResult = async (  sessionId: string,): Promise<McpOAuthPopupResult> =>  new Promise((resolve, reject) => {    const storageKey = `${OAUTH_STORAGE_PREFIX}${sessionId}`;    const startedAt = Date.now();     const cleanup = () => {      window.removeEventListener("message", handleMessage);      window.clearInterval(intervalId);    };     const finish = (result: McpOAuthPopupResult) => {      cleanup();      try {        window.localStorage.removeItem(storageKey);      } catch {}      resolve(result);    };     const handleMessage = (event: MessageEvent) => {      if (event.origin !== window.location.origin) {        return;      }       const data = event.data as McpOAuthPopupResult | undefined;      if (!data || data.type !== "executor:oauth-result") {        return;      }       if (data.ok && data.sessionId !== sessionId) {        return;      }       finish(data);    };     window.addEventListener("message", handleMessage);    const intervalId = window.setInterval(() => {      if (Date.now() - startedAt > OAUTH_TIMEOUT_MS) {        cleanup();        reject(new Error("Timed out waiting for MCP OAuth to finish."));        return;      }       try {        const raw = window.localStorage.getItem(storageKey);        if (!raw) {          return;        }         finish(JSON.parse(raw) as McpOAuthPopupResult);      } catch {        // Ignore malformed local storage and continue polling.      }    }, 400);  }); const openOauthPopup = (): Window | null => {  const popup = window.open("", "executor-mcp-oauth", "width=560,height=760");  if (!popup) {    return null;  }   try {    popup.document.title = "Connecting";    popup.document.body.innerHTML = `      <main style="font-family: system-ui, -apple-system, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; color: #333;">        <div style="width: 20px; height: 20px; border: 2px solid #e5e7eb; border-top-color: #6366f1; border-radius: 50%; animation: spin 0.7s linear infinite; margin-bottom: 16px;"></div>        <p style="margin: 0; font-size: 14px; color: #888;">Redirecting to sign in&hellip;</p>        <style>          @keyframes spin { to { transform: rotate(360deg); } }          @media (prefers-color-scheme: dark) {            main { background: #09090b; color: #fafafa !important; }            p { color: #a1a1aa !important; }            div { border-color: #3f3f46 !important; }          }        </style>      </main>    `;  } catch {    // Ignore cases where the browser does not allow touching the popup document.  }   return popup;}; function McpSourceForm(props: {  initialValue: McpConnectInput;  mode: "create" | "edit";  onSubmit: (input: McpConnectInput) => Promise<void>;}) {  const installation = useLocalInstallation();  const client = getMcpHttpClient();  const discoverSource = useAtomSet(    client.mutation("mcp", "discoverSource"),    { mode: "promise" },  );  const startOAuth = useAtomSet(    client.mutation("mcp", "startOAuth"),    { mode: "promise" },  );  const submitMutation = useExecutorMutation<McpConnectInput, void>(props.onSubmit);  const [name, setName] = useState(props.initialValue.name);  const [endpoint, setEndpoint] = useState(props.initialValue.endpoint ?? "");  const [transportFields, setTransportFields] = useState<McpTransportFields>(    transportFieldsFromInput(props.initialValue),  );  const [authKind, setAuthKind] = useState<McpConnectionAuth["kind"]>(    props.initialValue.auth.kind,  );  const [oauthAuth, setOauthAuth] = useState<    Extract<McpConnectionAuth, { kind: "oauth2" }> | null  >(    props.initialValue.auth.kind === "oauth2"      ? props.initialValue.auth      : null,  );  const [oauthStatus, setOauthStatus] = useState<"idle" | "pending" | "connected">(    props.initialValue.auth.kind === "oauth2" ? "connected" : "idle",  );  const [detectedOauthRequired, setDetectedOauthRequired] = useState(    props.initialValue.auth.kind === "oauth2",  );  const [lastDiscoveryKey, setLastDiscoveryKey] = useState<string | null>(    props.initialValue.auth.kind === "oauth2"      ? buildMcpRemoteConfigKey({          transport: transportFields.transport,          endpoint: props.initialValue.endpoint ?? "",          queryParams: props.initialValue.queryParams,        })      : null,  );  const [oauthConnectionKey, setOauthConnectionKey] = useState<string | null>(    props.initialValue.auth.kind === "oauth2"      ? buildMcpRemoteConfigKey({          transport: transportFields.transport,          endpoint: props.initialValue.endpoint ?? "",          queryParams: props.initialValue.queryParams,        })      : null,  );  const [error, setError] = useState<string | null>(null);   const isStdio = transportFields.transport === "stdio";  const remoteQueryParamsText =    transportFields.transport === "stdio"      ? ""      : transportFields.queryParamsText;  const parsedRemoteQueryParams = (() => {    if (isStdio) {      return null;    }     try {      return parseJsonStringMap("Query params", remoteQueryParamsText);    } catch {      return null;    }  })();  const remoteConfigKey = (() => {    try {      return buildMcpRemoteConfigKey({        transport: transportFields.transport,        endpoint,        queryParams: parsedRemoteQueryParams,      });    } catch {      return null;    }  })();   const runOauth = async (): Promise<Extract<McpConnectionAuth, { kind: "oauth2" }>> => {    if (installation.status !== "ready") {      throw new Error("Workspace is still loading.");    }    if (isStdio) {      throw new Error("MCP OAuth is only available for remote MCP transports.");    }     const popup = openOauthPopup();    if (!popup) {      throw new Error("Failed opening MCP OAuth popup. Allow popups for this site and try again.");    }     const payload: McpStartOAuthInput = {      endpoint: endpoint.trim(),      queryParams: parseJsonStringMap("Query params", transportFields.queryParamsText),      redirectUrl: new URL(        "/v1/plugins/mcp/oauth/callback",        window.location.origin,      ).toString(),    };     let started;    try {      started = await startOAuth({        path: {          workspaceId: installation.data.scopeId,        },        payload,      });    } catch (cause) {      popup.close();      throw cause;    }     popup.location.replace(started.authorizationUrl);    popup.focus();     const result = await waitForOauthPopupResult(started.sessionId);    if (!result.ok) {      throw new Error(result.error);    }     setOauthAuth(result.auth);    setOauthStatus("connected");    setOauthConnectionKey(remoteConfigKey);    return result.auth;  };   const runDiscovery = async (input: {    mode: "auto" | "manual";  }): Promise<McpDiscoverResult> => {    if (installation.status !== "ready") {      throw new Error("Workspace is still loading.");    }    if (isStdio || remoteConfigKey === null) {      setDetectedOauthRequired(false);      setLastDiscoveryKey(null);      return null;    }     const payload = {      endpoint: endpoint.trim(),      queryParams:        parseJsonStringMap("Query params", remoteQueryParamsText),    };     try {      const discovered = await discoverSource({        path: {          workspaceId: installation.data.scopeId,        },        payload,      });      const oauthRequired = mcpDiscoveryRequiresOAuth(discovered);      setDetectedOauthRequired(oauthRequired);      setLastDiscoveryKey(remoteConfigKey);      if (oauthRequired) {        setAuthKind((current) => current === "oauth2" ? current : "oauth2");      }      return discovered;    } catch (cause) {      setDetectedOauthRequired(false);      setLastDiscoveryKey(remoteConfigKey);      if (input.mode === "manual") {        throw cause;      }      return null;    }  };   useEffect(() => {    if (isStdio || remoteConfigKey === null) {      setDetectedOauthRequired(false);      setLastDiscoveryKey(null);      return;    }     if (remoteConfigKey === lastDiscoveryKey) {      return;    }     const timeout = window.setTimeout(() => {      void runDiscovery({ mode: "auto" });    }, 450);     return () => {      window.clearTimeout(timeout);    };  }, [    endpoint,    installation.status,    isStdio,    lastDiscoveryKey,    remoteConfigKey,    remoteQueryParamsText,  ]);   useEffect(() => {    if (oauthConnectionKey === null || remoteConfigKey === oauthConnectionKey) {      return;    }     setOauthAuth(null);    setOauthStatus("idle");    setOauthConnectionKey(null);  }, [oauthConnectionKey, remoteConfigKey]);   const buildInput = (    connectedOauthAuth: Extract<McpConnectionAuth, { kind: "oauth2" }> | null = oauthAuth,    authOverride?: McpConnectionAuth,  ): McpConnectInput => ({    name: name.trim(),    endpoint: isStdio ? null : (endpoint.trim() || null),    transport:      transportFields.transport === ""        ? null        : transportFields.transport,    queryParams: isStdio      ? null      : parseJsonStringMap("Query params", transportFields.queryParamsText),    headers: isStdio      ? null      : parseJsonStringMap("Headers", transportFields.headersText),    command: isStdio ? transportFields.command.trim() || null : null,    args: isStdio      ? parseJsonStringArray("Args", transportFields.argsText)      : null,    env: isStdio      ? parseJsonStringMap("Environment", transportFields.envText)      : null,    cwd: isStdio ? transportFields.cwd.trim() || null : null,    auth:

Lines 1–500 of 1117.