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…</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. Load more
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…</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">