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, key: string, ): string | null => { const value = search[key]; return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; }; const presetTransport = ( search: Record, ): McpConnectInput["transport"] => { const value = presetString(search, "presetTransport"); return value === "auto" || value === "streamable-http" || value === "sse" || value === "stdio" ? value : null; }; const presetJsonStringMap = ( search: Record, key: string, ): Record | null => { const value = presetString(search, key); return value ? parseJsonStringMap(key, value) : null; }; const presetJsonStringArray = ( search: Record, key: string, ): Array | null => { const value = presetString(search, key); return value ? parseJsonStringArray(key, value) : null; }; const mcpInputFromSearch = ( search: Record, ): 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 | null | undefined, ): string => !value || Object.keys(value).length === 0 ? "" : JSON.stringify(value, null, 2); const stringifyStringArray = ( value: ReadonlyArray | 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 => 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 = `

Redirecting to sign in…

`; } 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; }) { const installation = useLocalInstallation(); const client = getMcpHttpClient(); const startOAuth = useAtomSet( client.mutation("mcp", "startOAuth"), { mode: "promise" }, ); const submitMutation = useExecutorMutation(props.onSubmit); const [name, setName] = useState(props.initialValue.name); const [endpoint, setEndpoint] = useState(props.initialValue.endpoint ?? ""); const [transportFields, setTransportFields] = useState( transportFieldsFromInput(props.initialValue), ); const [authKind, setAuthKind] = useState( props.initialValue.auth.kind, ); const [oauthAuth, setOauthAuth] = useState< Extract | 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(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 (
setName(event.target.value)} />
{isStdio ? (
setTransportFields({ ...transportFields, command: event.target.value, })} className="font-mono text-xs" />