1872 lines · 67.0 KB · TypeScript
RawDownload
import {  Duration,  Effect,  Exit,  Layer,  Option,  Predicate,  Result,  Scope,  ScopedCache,} from "effect";import type { HttpClient } from "effect/unstable/http"; import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; import { McpGroup } from "../api/group";import { McpExtensionService, McpHandlers } from "../api/handlers"; import {  ConfiguredCredentialBinding,  ConnectionId,  type CredentialBindingRef,  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/core"; // ---------------------------------------------------------------------------// 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 =>  new McpToolBinding({    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.";  }  switch (shape.category) {    case "auth-required":      return "This server requires authentication. Add credentials (Authorization header, query parameter, or API key) below and retry.";    case "wrong-shape":      return "This URL doesn't appear to host an MCP server. Double-check the address, including the path.";  }}; 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 =>  new McpSourceBindingRef({    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] = new ConfiguredCredentialBinding({      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");  },  saveCodeVerifier: () => undefined,  codeVerifier: () => {    // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: MCP SDK OAuthClientProvider callback requires a thrown verifier failure    throw new Error("No active PKCE verifier");  },

Lines 1–500 of 1872.