import { useState } from "react"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Exit from "effect/Exit"; import { mcpSourceAtom, mcpSourceBindingsAtom, setMcpSourceBinding, updateMcpSource, } from "./atoms"; import { connectionsAtom } from "@executor-js/react/api/atoms"; import { useScope, useScopeStack } from "@executor-js/react/api/scope-context"; import { connectionWriteKeys, sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { slugifyNamespace, useSourceIdentity } from "@executor-js/react/plugins/source-identity"; import { useCredentialTargetScope } from "@executor-js/react/plugins/credential-target-scope"; import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets"; import { HttpCredentialsEditor, serializeHttpCredentials, serializeScopedHttpCredentials, type HttpCredentialsState, } from "@executor-js/react/plugins/http-credentials"; import { effectiveCredentialBindingForScope, httpCredentialsFromConfiguredCredentialBindings, initialCredentialTargetScope, } from "@executor-js/react/plugins/credential-bindings"; import { SourceOAuthConnectionControl } from "@executor-js/react/plugins/source-oauth-connection"; import { Button } from "@executor-js/react/components/button"; import { Badge } from "@executor-js/react/components/badge"; import { ScopeId } from "@executor-js/sdk/core"; import { McpRemoteSourceFields } from "./McpRemoteSourceFields"; import { McpSourceBindingInput, type McpCredentialInput, type McpSourceBindingRef, } from "../sdk/types"; import type { McpStoredSourceSchemaType } from "../sdk/stored-source"; // --------------------------------------------------------------------------- // Remote edit form // --------------------------------------------------------------------------- function RemoteEditForm(props: { sourceId: string; initial: McpStoredSourceSchemaType & { config: { transport: "remote" } }; bindings: readonly McpSourceBindingRef[]; onSave: () => void; }) { const displayScope = useScope(); const scopeStack = useScopeStack(); const sourceScope = ScopeId.make(props.initial.scope); const { credentialTargetScope, credentialScopeOptions } = useCredentialTargetScope({ sourceScope, initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings), }); const { credentialTargetScope: oauthCredentialTargetScope, setCredentialTargetScope: setOAuthCredentialTargetScope, } = useCredentialTargetScope({ sourceScope, initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings), }); const doUpdate = useAtomSet(updateMcpSource, { mode: "promiseExit" }); const setBinding = useAtomSet(setMcpSourceBinding, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const connectionsResult = useAtomValue(connectionsAtom(displayScope)); const identity = useSourceIdentity({ fallbackName: props.initial.name, fallbackNamespace: props.initial.namespace, }); const [endpoint, setEndpoint] = useState(props.initial.config.endpoint); const [credentials, setCredentials] = useState(() => httpCredentialsFromConfiguredCredentialBindings({ headers: props.initial.config.headers, queryParams: props.initial.config.queryParams, bindings: props.bindings, }), ); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [credentialsDirty, setCredentialsDirty] = useState(false); const identityDirty = identity.name.trim() !== props.initial.name.trim(); const metadataDirty = identityDirty || endpoint.trim() !== props.initial.config.endpoint.trim(); const dirty = metadataDirty || credentialsDirty; const oauth2 = props.initial.config.auth.kind === "oauth2" ? props.initial.config.auth : null; const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : []; const scopeRanks = new Map(scopeStack.map((scope, index) => [scope.id, index] as const)); const connectionBinding = oauth2 ? effectiveCredentialBindingForScope( props.bindings, oauth2.connectionSlot, oauthCredentialTargetScope, scopeRanks, ) : null; const boundConnectionId = connectionBinding?.value.kind === "connection" ? connectionBinding.value.connectionId : null; const isConnected = boundConnectionId !== null && connections.some((connection) => connection.id === boundConnectionId); const oauthRequestCredentials = serializeHttpCredentials(credentials); const handleCredentialsChange = (next: HttpCredentialsState) => { setCredentials(next); setCredentialsDirty(true); }; const handleSave = async () => { setSaving(true); setError(null); const { headers, queryParams } = serializeScopedHttpCredentials( credentials, credentialTargetScope, ); const payload: { sourceScope: ScopeId; name?: string; endpoint?: string; headers?: Record; queryParams?: Record; credentialTargetScope?: ScopeId; } = { sourceScope, name: metadataDirty ? identity.name.trim() || undefined : undefined, endpoint: metadataDirty ? endpoint.trim() || undefined : undefined, }; if (credentialsDirty) { payload.headers = headers; payload.queryParams = queryParams as Record; payload.credentialTargetScope = credentialTargetScope; } const exit = await doUpdate({ params: { scopeId: displayScope, namespace: props.sourceId }, payload, reactivityKeys: sourceWriteKeys, }); if (Exit.isFailure(exit)) { setError("Failed to update source"); setSaving(false); return; } setCredentialsDirty(false); setSaving(false); props.onSave(); }; return (

Edit MCP Source

Update the endpoint and headers for this MCP connection.

{props.sourceId}

remote
{oauth2 && ( { await setBinding({ params: { scopeId: oauthCredentialTargetScope }, payload: new McpSourceBindingInput({ sourceId: props.sourceId, sourceScope, scope: oauthCredentialTargetScope, slot: oauth2.connectionSlot, value: { kind: "connection", connectionId }, }), reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys], }); }} reconnectingLabel="Reconnecting…" signingInLabel="Signing in…" /> )} {error && (

{error}

)}
); } // --------------------------------------------------------------------------- // Stdio read-only view // --------------------------------------------------------------------------- function StdioReadOnly(props: { sourceId: string; initial: McpStoredSourceSchemaType & { config: { transport: "stdio" } }; onSave: () => void; }) { const { command, args } = props.initial.config; return (

Edit MCP Source

Stdio MCP sources cannot be edited in the UI. Modify the executor.jsonc config file directly.

{props.sourceId}

{command} {(args ?? []).join(" ")}

stdio
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function EditMcpSource({ sourceId, onSave, }: { readonly sourceId: string; readonly onSave: () => void; }) { const scopeId = useScope(); const sourceResult = useAtomValue(mcpSourceAtom(scopeId, sourceId)) as AsyncResult.AsyncResult< McpStoredSourceSchemaType | null, unknown >; const source = AsyncResult.isSuccess(sourceResult) && sourceResult.value ? sourceResult.value : null; const sourceScope = source ? ScopeId.make(source.scope) : scopeId; const bindingsResult = useAtomValue(mcpSourceBindingsAtom(scopeId, sourceId, sourceScope)); if (!AsyncResult.isSuccess(sourceResult) || !source || !AsyncResult.isSuccess(bindingsResult)) { return (

Edit MCP Source

Loading configuration…

); } if (source.config.transport === "stdio") { return ( ); } return ( ); }