import { Duration, Effect, Exit, Layer, Match, Option, Predicate, Result, Scope, ScopedCache,} from "effect";import type { HttpClient } from "effect/unstable/http"; import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; import { ConfiguredCredentialBinding, ConnectionId, type CredentialBindingRef, type CredentialBindingValue, ScopeId, SecretId, SourceDetectionResult, definePlugin, resolveSecretBackedMap as resolveSharedSecretBackedMap, type PluginCtx, type StorageFailure, StorageError, type ToolAnnotations,} from "@executor-js/sdk/core"; import { makeMcpStore, mcpSchema, type McpBindingStore, type McpStoredSource,} from "./binding-store";import { createMcpConnector, type ConnectorInput, type McpConnection } from "./connection";import { discoverTools } from "./discover";import { McpConnectionError, McpInvocationError, McpToolDiscoveryError } from "./errors";import { invokeMcpTool } from "./invoke";import { deriveMcpNamespace, type McpToolManifest, type McpToolManifestEntry } from "./manifest";import { probeMcpEndpointShape, type McpShapeProbeResult } from "./probe-shape";import { MCP_HEADER_AUTH_SLOT, MCP_OAUTH_CLIENT_ID_SLOT, MCP_OAUTH_CLIENT_SECRET_SLOT, MCP_OAUTH_CONNECTION_SLOT, McpToolBinding, McpSourceBindingInput, McpSourceBindingRef, mcpHeaderSlot, mcpQueryParamSlot, type McpConnectionAuth, type McpConnectionAuthInput, type McpCredentialInput, type McpSourceBindingValue, type SecretBackedValue, type McpStoredSourceData, type ConfiguredMcpCredentialValue,} from "./types"; import { SECRET_REF_PREFIX, type ConfigFileSink, type McpAuthConfig, type McpRemoteSourceConfig as McpRemoteConfigEntry, type McpStdioSourceConfig as McpStdioConfigEntry, type SourceConfig,} from "@executor-js/config"; // ---------------------------------------------------------------------------// Plugin config — discriminated union on transport// --------------------------------------------------------------------------- /** * Executor scope id that owns a newly-added MCP source row. Must be one * of the executor's configured scopes. Admins adding a shared server at * org scope pin here; per-user stdio sources can pin at the inner * scope. */type McpSourceScopeField = { readonly scope: string }; export interface McpRemoteSourceConfig extends McpSourceScopeField { readonly transport: "remote"; readonly name: string; readonly endpoint: string; readonly remoteTransport?: "streamable-http" | "sse" | "auto"; readonly queryParams?: Record<string, McpCredentialInput>; readonly headers?: Record<string, McpCredentialInput>; readonly namespace?: string; readonly auth?: McpConnectionAuthInput; /** * Scope that owns any direct credentials supplied on this call. Required * whenever headers/queryParams/auth carry direct secret or connection ids. */ readonly credentialTargetScope?: string;} export interface McpStdioSourceConfig extends McpSourceScopeField { readonly transport: "stdio"; readonly name: string; readonly command: string; readonly args?: string[]; readonly env?: Record<string, string>; readonly cwd?: string; readonly namespace?: string;} export type McpSourceConfig = McpRemoteSourceConfig | McpStdioSourceConfig; // ---------------------------------------------------------------------------// Extension types// --------------------------------------------------------------------------- // OAuth start/complete/callback moved to the shared// `/scopes/:scopeId/oauth/*` surface in `@executor-js/api` — no// plugin-specific types needed here. export interface McpProbeResult { readonly connected: boolean; readonly requiresOAuth: boolean; readonly supportsDynamicRegistration: boolean; readonly name: string; readonly namespace: string; readonly toolCount: number | null; readonly serverName: string | null;} export interface McpUpdateSourceInput { readonly name?: string; readonly endpoint?: string; readonly headers?: Record<string, McpCredentialInput>; readonly queryParams?: Record<string, McpCredentialInput>; readonly credentialTargetScope?: string; readonly auth?: McpConnectionAuthInput;} export interface McpProbeEndpointInput { readonly endpoint: string; readonly headers?: Record<string, SecretBackedValue>; readonly queryParams?: Record<string, SecretBackedValue>;} // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- const toStoredSourceData = ( config: McpSourceConfig, remoteCredentials?: { readonly headers: Record<string, ConfiguredMcpCredentialValue>; readonly queryParams: Record<string, ConfiguredMcpCredentialValue>; readonly auth: McpConnectionAuth; },): McpStoredSourceData => { if (config.transport === "stdio") { return { transport: "stdio", command: config.command, args: config.args, env: config.env, cwd: config.cwd, }; } return { transport: "remote", endpoint: config.endpoint, remoteTransport: config.remoteTransport ?? "auto", queryParams: remoteCredentials?.queryParams, headers: remoteCredentials?.headers, auth: remoteCredentials?.auth ?? { kind: "none" }, };}; const normalizeNamespace = (config: McpSourceConfig): string => config.namespace ?? deriveMcpNamespace({ name: config.name, endpoint: config.transport === "remote" ? config.endpoint : undefined, command: config.transport === "stdio" ? config.command : undefined, }); const toBinding = (entry: McpToolManifestEntry): McpToolBinding => McpToolBinding.make({ toolId: entry.toolId, toolName: entry.toolName, description: entry.description, inputSchema: entry.inputSchema, outputSchema: entry.outputSchema, annotations: entry.annotations, }); const MCP_PLUGIN_ID = "mcp"; /** Match `token` as a separator-bounded run inside a URL hostname or path, * used as a low-confidence detection hint when wire-shape detection fails. * Boundary chars are everything non-alphanumeric, so `/api/mcp`, * `mcp.example.com`, `mcp-server`, and `mcp_v1` all match while * `mcphost.com` and `/mcpstore` do not. */const urlMatchesToken = (url: URL, token: string): boolean => { const re = new RegExp(`(?:^|[^a-z0-9])${token}(?:$|[^a-z0-9])`, "i"); return re.test(url.hostname) || re.test(url.pathname);}; /** Translate a non-MCP probe outcome into a message a user can act on. * The technical `reason` (`401 without Bearer WWW-Authenticate — not an * MCP auth challenge`, etc.) stays in telemetry via the probe span; the * user gets a sentence pointing at their next step. Exported for tests. */export const userFacingProbeMessage = ( shape: Extract<McpShapeProbeResult, { kind: "not-mcp" } | { kind: "unreachable" }>,): string => { if (shape.kind === "unreachable") { return "Couldn't reach this URL. Check the address, your network, and that the server is running."; } return Match.value(shape.category).pipe( Match.when( "auth-required", () => "This server requires authentication. Add credentials (Authorization header, query parameter, or API key) below and retry.", ), Match.when( "wrong-shape", () => "This URL doesn't appear to host an MCP server. Double-check the address, including the path.", ), Match.exhaustive, );}; const scopeRanks = (ctx: PluginCtx<McpBindingStore>): ReadonlyMap<string, number> => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index])); const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: string): number => ranks.get(scopeId) ?? Infinity; const coreBindingToMcpBinding = (binding: CredentialBindingRef): McpSourceBindingRef => McpSourceBindingRef.make({ sourceId: binding.sourceId, sourceScopeId: binding.sourceScopeId, scopeId: binding.scopeId, slot: binding.slotKey, value: binding.value, createdAt: binding.createdAt, updatedAt: binding.updatedAt, }); const listMcpSourceBindings = ( ctx: PluginCtx<McpBindingStore>, sourceId: string, sourceScope: string,): Effect.Effect<readonly McpSourceBindingRef[], StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return []; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: MCP_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); return bindings .filter((binding) => scopeRank(ranks, binding.scopeId) <= sourceSourceRank) .map(coreBindingToMcpBinding); }); const resolveMcpSourceBinding = ( ctx: PluginCtx<McpBindingStore>, sourceId: string, sourceScope: string, slot: string,): Effect.Effect<McpSourceBindingRef | null, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return null; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: MCP_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); const binding = bindings .filter( (candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank, ) .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0]; return binding ? coreBindingToMcpBinding(binding) : null; }); const validateMcpBindingTarget = ( ctx: PluginCtx<McpBindingStore>, input: { readonly sourceScope: string; readonly targetScope: string; readonly sourceId: string; },): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, input.sourceScope); const targetRank = scopeRank(ranks, input.targetScope); const scopeList = `[${ctx.scopes.map((s) => s.id).join(", ")}]`; if (sourceSourceRank === Infinity) { return yield* new StorageError({ message: `MCP source binding references source scope "${input.sourceScope}" ` + `which is not in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank === Infinity) { return yield* new StorageError({ message: `MCP source binding targets scope "${input.targetScope}" which is not ` + `in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank > sourceSourceRank) { return yield* new StorageError({ message: `MCP source bindings for "${input.sourceId}" cannot be written at ` + `outer scope "${input.targetScope}" because the base source lives at ` + `"${input.sourceScope}"`, cause: undefined, }); } }); const bindingTargetScope = ( targetScope: string | undefined, bindings: readonly unknown[],): Effect.Effect<string | undefined, McpConnectionError> => { if (bindings.length === 0) return Effect.succeed(undefined); if (targetScope) return Effect.succeed(targetScope); return Effect.fail( new McpConnectionError({ transport: "remote", message: "credentialTargetScope is required when adding direct MCP credentials", }), );}; const targetScopeForBinding = ( fallbackTargetScope: string | undefined, binding: { readonly targetScope?: string },): Effect.Effect<string, McpConnectionError> => { const targetScope = binding.targetScope ?? fallbackTargetScope; if (targetScope) return Effect.succeed(targetScope); return Effect.fail( new McpConnectionError({ transport: "remote", message: "credentialTargetScope is required when adding direct MCP credentials", }), );}; const canonicalizeCredentialMap = ( values: Record<string, McpCredentialInput> | undefined, slotForName: (name: string) => string,): { readonly values: Record<string, ConfiguredMcpCredentialValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: McpSourceBindingValue; readonly targetScope?: string; }>;} => { const nextValues: Record<string, ConfiguredMcpCredentialValue> = {}; const bindings: Array<{ slot: string; value: McpSourceBindingValue; targetScope?: string }> = []; for (const [name, value] of Object.entries(values ?? {})) { if (typeof value === "string") { nextValues[name] = value; continue; } if ("kind" in value) { nextValues[name] = value; continue; } const slot = slotForName(name); nextValues[name] = ConfiguredCredentialBinding.make({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, targetScope: "targetScope" in value ? value.targetScope : undefined, value: { kind: "secret", secretId: SecretId.make(value.secretId), ...("secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}), }, }); } return { values: nextValues, bindings };}; const canonicalizeAuth = ( auth: McpConnectionAuthInput | undefined,): { readonly auth: McpConnectionAuth; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: McpSourceBindingValue; readonly targetScope?: string; }>;} => { if (!auth || auth.kind === "none") return { auth: { kind: "none" }, bindings: [] }; if (auth.kind === "header") { if ("secretSlot" in auth) return { auth, bindings: [] }; return { auth: { kind: "header", headerName: auth.headerName, secretSlot: MCP_HEADER_AUTH_SLOT, prefix: auth.prefix, }, bindings: [ { slot: MCP_HEADER_AUTH_SLOT, targetScope: auth.targetScope, value: { kind: "secret", secretId: SecretId.make(auth.secretId), ...(auth.secretScopeId ? { secretScopeId: auth.secretScopeId } : {}), }, }, ], }; } if ("connectionSlot" in auth) return { auth, bindings: [] }; const bindings: Array<{ slot: string; value: McpSourceBindingValue; targetScope?: string }> = [ { slot: MCP_OAUTH_CONNECTION_SLOT, value: { kind: "connection", connectionId: ConnectionId.make(auth.connectionId), }, }, ]; if (auth.clientIdSecretId) { bindings.push({ slot: MCP_OAUTH_CLIENT_ID_SLOT, value: { kind: "secret", secretId: SecretId.make(auth.clientIdSecretId) }, }); } if (auth.clientSecretSecretId) { bindings.push({ slot: MCP_OAUTH_CLIENT_SECRET_SLOT, value: { kind: "secret", secretId: SecretId.make(auth.clientSecretSecretId) }, }); } return { auth: { kind: "oauth2", connectionSlot: MCP_OAUTH_CONNECTION_SLOT, ...(auth.clientIdSecretId ? { clientIdSlot: MCP_OAUTH_CLIENT_ID_SLOT } : {}), ...(auth.clientSecretSecretId ? { clientSecretSlot: MCP_OAUTH_CLIENT_SECRET_SLOT } : {}), }, bindings, };}; // ---------------------------------------------------------------------------// MCP-SDK OAuth provider adapter — builds the `OAuthClientProvider` the// MCP SDK's StreamableHTTP/SSE transports want, wrapping a pre-resolved// access token.//// Refresh is NOT driven through this provider — `ctx.connections.access// Token` owns that lifecycle at the core level via the canonical// "oauth2" ConnectionProvider. This adapter only injects the current// token into the transport's Authorization header and fails loudly if// the SDK ever tries to initiate a new OAuth flow (which would bypass// our refresh machinery).// ---------------------------------------------------------------------------const makeOAuthProvider = (accessToken: string): OAuthClientProvider => ({ get redirectUrl() { return "http://localhost/oauth/callback"; }, get clientMetadata() { return { redirect_uris: ["http://localhost/oauth/callback"], grant_types: ["authorization_code", "refresh_token"] as string[], response_types: ["code"] as string[], token_endpoint_auth_method: "none" as const, client_name: "Executor", }; }, clientInformation: () => undefined, saveClientInformation: () => undefined, tokens: () => ({ access_token: accessToken, token_type: "Bearer" }), saveTokens: () => undefined, redirectToAuthorization: async () => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: MCP SDK OAuthClientProvider callback can only signal reauthorization by throwing throw new Error("MCP OAuth re-authorization required");Lines 1–500 of 1993. Load more
import { Duration, Effect, Exit, Layer, Match, Option, Predicate, Result, Scope, ScopedCache, } from "effect"; import type { HttpClient } from "effect/unstable/http"; import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; import { ConfiguredCredentialBinding, ConnectionId, type CredentialBindingRef, type CredentialBindingValue, ScopeId, SecretId, SourceDetectionResult, definePlugin, resolveSecretBackedMap as resolveSharedSecretBackedMap, type PluginCtx, type StorageFailure, StorageError, type ToolAnnotations, } from "@executor-js/sdk/core"; import { makeMcpStore, mcpSchema, type McpBindingStore, type McpStoredSource, } from "./binding-store"; import { createMcpConnector, type ConnectorInput, type McpConnection } from "./connection"; import { discoverTools } from "./discover"; import { McpConnectionError, McpInvocationError, McpToolDiscoveryError } from "./errors"; import { invokeMcpTool } from "./invoke"; import { deriveMcpNamespace, type McpToolManifest, type McpToolManifestEntry } from "./manifest"; import { probeMcpEndpointShape, type McpShapeProbeResult } from "./probe-shape"; import { MCP_HEADER_AUTH_SLOT, MCP_OAUTH_CLIENT_ID_SLOT, MCP_OAUTH_CLIENT_SECRET_SLOT, MCP_OAUTH_CONNECTION_SLOT, McpToolBinding, McpSourceBindingInput, McpSourceBindingRef, mcpHeaderSlot, mcpQueryParamSlot, type McpConnectionAuth, type McpConnectionAuthInput, type McpCredentialInput, type McpSourceBindingValue, type SecretBackedValue, type McpStoredSourceData, type ConfiguredMcpCredentialValue, } from "./types"; import { SECRET_REF_PREFIX, type ConfigFileSink, type McpAuthConfig, type McpRemoteSourceConfig as McpRemoteConfigEntry, type McpStdioSourceConfig as McpStdioConfigEntry, type SourceConfig, } from "@executor-js/config"; // --------------------------------------------------------------------------- // Plugin config — discriminated union on transport // --------------------------------------------------------------------------- /** * Executor scope id that owns a newly-added MCP source row. Must be one * of the executor's configured scopes. Admins adding a shared server at * org scope pin here; per-user stdio sources can pin at the inner * scope. */ type McpSourceScopeField = { readonly scope: string }; export interface McpRemoteSourceConfig extends McpSourceScopeField { readonly transport: "remote"; readonly name: string; readonly endpoint: string; readonly remoteTransport?: "streamable-http" | "sse" | "auto"; readonly queryParams?: Record<string, McpCredentialInput>; readonly headers?: Record<string, McpCredentialInput>; readonly namespace?: string; readonly auth?: McpConnectionAuthInput; /** * Scope that owns any direct credentials supplied on this call. Required * whenever headers/queryParams/auth carry direct secret or connection ids. */ readonly credentialTargetScope?: string; } export interface McpStdioSourceConfig extends McpSourceScopeField { readonly transport: "stdio"; readonly name: string; readonly command: string; readonly args?: string[]; readonly env?: Record<string, string>; readonly cwd?: string; readonly namespace?: string; } export type McpSourceConfig = McpRemoteSourceConfig | McpStdioSourceConfig; // --------------------------------------------------------------------------- // Extension types // --------------------------------------------------------------------------- // OAuth start/complete/callback moved to the shared // `/scopes/:scopeId/oauth/*` surface in `@executor-js/api` — no // plugin-specific types needed here. export interface McpProbeResult { readonly connected: boolean; readonly requiresOAuth: boolean; readonly supportsDynamicRegistration: boolean; readonly name: string; readonly namespace: string; readonly toolCount: number | null; readonly serverName: string | null; } export interface McpUpdateSourceInput { readonly name?: string; readonly endpoint?: string; readonly headers?: Record<string, McpCredentialInput>; readonly queryParams?: Record<string, McpCredentialInput>; readonly credentialTargetScope?: string; readonly auth?: McpConnectionAuthInput; } export interface McpProbeEndpointInput { readonly endpoint: string; readonly headers?: Record<string, SecretBackedValue>; readonly queryParams?: Record<string, SecretBackedValue>; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const toStoredSourceData = ( config: McpSourceConfig, remoteCredentials?: { readonly headers: Record<string, ConfiguredMcpCredentialValue>; readonly queryParams: Record<string, ConfiguredMcpCredentialValue>; readonly auth: McpConnectionAuth; }, ): McpStoredSourceData => { if (config.transport === "stdio") { return { transport: "stdio", command: config.command, args: config.args, env: config.env, cwd: config.cwd, }; } return { transport: "remote", endpoint: config.endpoint, remoteTransport: config.remoteTransport ?? "auto", queryParams: remoteCredentials?.queryParams, headers: remoteCredentials?.headers, auth: remoteCredentials?.auth ?? { kind: "none" }, }; }; const normalizeNamespace = (config: McpSourceConfig): string => config.namespace ?? deriveMcpNamespace({ name: config.name, endpoint: config.transport === "remote" ? config.endpoint : undefined, command: config.transport === "stdio" ? config.command : undefined, }); const toBinding = (entry: McpToolManifestEntry): McpToolBinding => McpToolBinding.make({ toolId: entry.toolId, toolName: entry.toolName, description: entry.description, inputSchema: entry.inputSchema, outputSchema: entry.outputSchema, annotations: entry.annotations, }); const MCP_PLUGIN_ID = "mcp"; /** Match `token` as a separator-bounded run inside a URL hostname or path, * used as a low-confidence detection hint when wire-shape detection fails. * Boundary chars are everything non-alphanumeric, so `/api/mcp`, * `mcp.example.com`, `mcp-server`, and `mcp_v1` all match while * `mcphost.com` and `/mcpstore` do not. */ const urlMatchesToken = (url: URL, token: string): boolean => { const re = new RegExp(`(?:^|[^a-z0-9])${token}(?:$|[^a-z0-9])`, "i"); return re.test(url.hostname) || re.test(url.pathname); }; /** Translate a non-MCP probe outcome into a message a user can act on. * The technical `reason` (`401 without Bearer WWW-Authenticate — not an * MCP auth challenge`, etc.) stays in telemetry via the probe span; the * user gets a sentence pointing at their next step. Exported for tests. */ export const userFacingProbeMessage = ( shape: Extract<McpShapeProbeResult, { kind: "not-mcp" } | { kind: "unreachable" }>, ): string => { if (shape.kind === "unreachable") { return "Couldn't reach this URL. Check the address, your network, and that the server is running."; } return Match.value(shape.category).pipe( Match.when( "auth-required", () => "This server requires authentication. Add credentials (Authorization header, query parameter, or API key) below and retry.", ), Match.when( "wrong-shape", () => "This URL doesn't appear to host an MCP server. Double-check the address, including the path.", ), Match.exhaustive, ); }; const scopeRanks = (ctx: PluginCtx<McpBindingStore>): ReadonlyMap<string, number> => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index])); const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: string): number => ranks.get(scopeId) ?? Infinity; const coreBindingToMcpBinding = (binding: CredentialBindingRef): McpSourceBindingRef => McpSourceBindingRef.make({ sourceId: binding.sourceId, sourceScopeId: binding.sourceScopeId, scopeId: binding.scopeId, slot: binding.slotKey, value: binding.value, createdAt: binding.createdAt, updatedAt: binding.updatedAt, }); const listMcpSourceBindings = ( ctx: PluginCtx<McpBindingStore>, sourceId: string, sourceScope: string, ): Effect.Effect<readonly McpSourceBindingRef[], StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return []; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: MCP_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); return bindings .filter((binding) => scopeRank(ranks, binding.scopeId) <= sourceSourceRank) .map(coreBindingToMcpBinding); }); const resolveMcpSourceBinding = ( ctx: PluginCtx<McpBindingStore>, sourceId: string, sourceScope: string, slot: string, ): Effect.Effect<McpSourceBindingRef | null, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return null; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: MCP_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); const binding = bindings .filter( (candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank, ) .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0]; return binding ? coreBindingToMcpBinding(binding) : null; }); const validateMcpBindingTarget = ( ctx: PluginCtx<McpBindingStore>, input: { readonly sourceScope: string; readonly targetScope: string; readonly sourceId: string; }, ): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, input.sourceScope); const targetRank = scopeRank(ranks, input.targetScope); const scopeList = `[${ctx.scopes.map((s) => s.id).join(", ")}]`; if (sourceSourceRank === Infinity) { return yield* new StorageError({ message: `MCP source binding references source scope "${input.sourceScope}" ` + `which is not in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank === Infinity) { return yield* new StorageError({ message: `MCP source binding targets scope "${input.targetScope}" which is not ` + `in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank > sourceSourceRank) { return yield* new StorageError({ message: `MCP source bindings for "${input.sourceId}" cannot be written at ` + `outer scope "${input.targetScope}" because the base source lives at ` + `"${input.sourceScope}"`, cause: undefined, }); } }); const bindingTargetScope = ( targetScope: string | undefined, bindings: readonly unknown[], ): Effect.Effect<string | undefined, McpConnectionError> => { if (bindings.length === 0) return Effect.succeed(undefined); if (targetScope) return Effect.succeed(targetScope); return Effect.fail( new McpConnectionError({ transport: "remote", message: "credentialTargetScope is required when adding direct MCP credentials", }), ); }; const targetScopeForBinding = ( fallbackTargetScope: string | undefined, binding: { readonly targetScope?: string }, ): Effect.Effect<string, McpConnectionError> => { const targetScope = binding.targetScope ?? fallbackTargetScope; if (targetScope) return Effect.succeed(targetScope); return Effect.fail( new McpConnectionError({ transport: "remote", message: "credentialTargetScope is required when adding direct MCP credentials", }), ); }; const canonicalizeCredentialMap = ( values: Record<string, McpCredentialInput> | undefined, slotForName: (name: string) => string, ): { readonly values: Record<string, ConfiguredMcpCredentialValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: McpSourceBindingValue; readonly targetScope?: string; }>; } => { const nextValues: Record<string, ConfiguredMcpCredentialValue> = {}; const bindings: Array<{ slot: string; value: McpSourceBindingValue; targetScope?: string }> = []; for (const [name, value] of Object.entries(values ?? {})) { if (typeof value === "string") { nextValues[name] = value; continue; } if ("kind" in value) { nextValues[name] = value; continue; } const slot = slotForName(name); nextValues[name] = ConfiguredCredentialBinding.make({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, targetScope: "targetScope" in value ? value.targetScope : undefined, value: { kind: "secret", secretId: SecretId.make(value.secretId), ...("secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}), }, }); } return { values: nextValues, bindings }; }; const canonicalizeAuth = ( auth: McpConnectionAuthInput | undefined, ): { readonly auth: McpConnectionAuth; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: McpSourceBindingValue; readonly targetScope?: string; }>; } => { if (!auth || auth.kind === "none") return { auth: { kind: "none" }, bindings: [] }; if (auth.kind === "header") { if ("secretSlot" in auth) return { auth, bindings: [] }; return { auth: { kind: "header", headerName: auth.headerName, secretSlot: MCP_HEADER_AUTH_SLOT, prefix: auth.prefix, }, bindings: [ { slot: MCP_HEADER_AUTH_SLOT, targetScope: auth.targetScope, value: { kind: "secret", secretId: SecretId.make(auth.secretId), ...(auth.secretScopeId ? { secretScopeId: auth.secretScopeId } : {}), }, }, ], }; } if ("connectionSlot" in auth) return { auth, bindings: [] }; const bindings: Array<{ slot: string; value: McpSourceBindingValue; targetScope?: string }> = [ { slot: MCP_OAUTH_CONNECTION_SLOT, value: { kind: "connection", connectionId: ConnectionId.make(auth.connectionId), }, }, ]; if (auth.clientIdSecretId) { bindings.push({ slot: MCP_OAUTH_CLIENT_ID_SLOT, value: { kind: "secret", secretId: SecretId.make(auth.clientIdSecretId) }, }); } if (auth.clientSecretSecretId) { bindings.push({ slot: MCP_OAUTH_CLIENT_SECRET_SLOT, value: { kind: "secret", secretId: SecretId.make(auth.clientSecretSecretId) }, }); } return { auth: { kind: "oauth2", connectionSlot: MCP_OAUTH_CONNECTION_SLOT, ...(auth.clientIdSecretId ? { clientIdSlot: MCP_OAUTH_CLIENT_ID_SLOT } : {}), ...(auth.clientSecretSecretId ? { clientSecretSlot: MCP_OAUTH_CLIENT_SECRET_SLOT } : {}), }, bindings, }; }; // --------------------------------------------------------------------------- // MCP-SDK OAuth provider adapter — builds the `OAuthClientProvider` the // MCP SDK's StreamableHTTP/SSE transports want, wrapping a pre-resolved // access token. // // Refresh is NOT driven through this provider — `ctx.connections.access // Token` owns that lifecycle at the core level via the canonical // "oauth2" ConnectionProvider. This adapter only injects the current // token into the transport's Authorization header and fails loudly if // the SDK ever tries to initiate a new OAuth flow (which would bypass // our refresh machinery). // --------------------------------------------------------------------------- const makeOAuthProvider = (accessToken: string): OAuthClientProvider => ({ get redirectUrl() { return "http://localhost/oauth/callback"; }, get clientMetadata() { return { redirect_uris: ["http://localhost/oauth/callback"], grant_types: ["authorization_code", "refresh_token"] as string[], response_types: ["code"] as string[], token_endpoint_auth_method: "none" as const, client_name: "Executor", }; }, clientInformation: () => undefined, saveClientInformation: () => undefined, tokens: () => ({ access_token: accessToken, token_type: "Bearer" }), saveTokens: () => undefined, redirectToAuthorization: async () => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: MCP SDK OAuthClientProvider callback can only signal reauthorization by throwing throw new Error("MCP OAuth re-authorization required");