Rhys Sullivanrefresh mcp3f52c607 months agoHistory
941 lines · 27.7 KB · TypeScript
RawDownload
import { startTransition, 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,  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 McpOAuthPopupResult,  type McpStartOAuthInput,} from "@executor/plugin-mcp-shared";import {  asMcpRemoteTransportValue,  defaultMcpRemoteTransportFields,  defaultMcpStdioTransportFields,  setMcpTransportFieldsTransport,  type McpTransportFields,  type McpTransportValue,} from "./transport";import {  parseJsonStringArray,  parseJsonStringMap,} from "./json"; 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 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 [error, setError] = useState<string | null>(null);   const isStdio = transportFields.transport === "stdio";   const runOauth = async () => {    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");  };   const buildInput = (): 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:      authKind === "oauth2"        ? oauthAuth ?? (() => {            throw new Error("Complete MCP OAuth before saving.");          })()        : { kind: "none" },  });   return (    <div className="space-y-8">      <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]">        <div className="grid gap-2">          <Label>Name</Label>          <Input            value={name}            onChange={(event) => setName(event.target.value)}          />        </div>         <div className="grid gap-2">          <Label>Transport</Label>          <Select            value={transportFields.transport}            onChange={(event) => {              const nextTransport = event.target.value as McpTransportValue;              setTransportFields((current) =>                setMcpTransportFieldsTransport(current, nextTransport)              );              if (nextTransport === "stdio") {                setAuthKind("none");                setOauthAuth(null);                setOauthStatus("idle");              }            }}          >            <option value="">Auto (remote)</option>            <option value="auto">Auto</option>            <option value="streamable-http">Streamable HTTP</option>            <option value="sse">SSE</option>            <option value="stdio">stdio</option>          </Select>        </div>         {isStdio ? (          <div className="grid gap-4 md:grid-cols-2">            <div className="grid gap-2 md:col-span-2">              <Label>Command</Label>              <Input                value={transportFields.command}                onChange={(event) =>                  setTransportFields({                    ...transportFields,                    command: event.target.value,                  })}                className="font-mono text-xs"              />            </div>             <div className="grid gap-2">              <Label>Args</Label>              <Textarea                value={transportFields.argsText}                onChange={(event) =>                  setTransportFields({                    ...transportFields,                    argsText: event.target.value,                  })}                rows={3}                placeholder='["server.js","--port","8787"]'              />            </div>             <div className="grid gap-2">              <Label>Environment</Label>              <Textarea                value={transportFields.envText}                onChange={(event) =>                  setTransportFields({                    ...transportFields,                    envText: event.target.value,                  })}                rows={3}                placeholder='{"NODE_ENV":"production"}'              />            </div>             <div className="grid gap-2 md:col-span-2">              <Label>Working Directory</Label>              <Input                value={transportFields.cwd}                onChange={(event) =>                  setTransportFields({                    ...transportFields,                    cwd: event.target.value,                  })}                className="font-mono text-xs"              />            </div>          </div>        ) : (          <div className="grid gap-4 md:grid-cols-2">            <div className="grid gap-2 md:col-span-2">              <Label>Endpoint</Label>              <Input                value={endpoint}                onChange={(event) => setEndpoint(event.target.value)}                className="font-mono text-xs"              />            </div>             <div className="grid gap-2">              <Label>Query Params</Label>              <Textarea                value={transportFields.queryParamsText}                onChange={(event) =>                  setTransportFields({                    ...transportFields,                    queryParamsText: event.target.value,                  })}                rows={3}                placeholder='{"transport":"streamable-http"}'              />            </div>             <div className="grid gap-2">              <Label>Headers</Label>              <Textarea                value={transportFields.headersText}                onChange={(event) =>                  setTransportFields({                    ...transportFields,                    headersText: event.target.value,                  })}                rows={3}                placeholder='{"x-api-key":"..."}'              />            </div>          </div>        )}         {!isStdio && (          <div className="grid gap-2">

Lines 1–500 of 941.