2358 lines · 87.9 KB · TypeScript
RawDownload
import {  Duration,  Effect,  Exit,  Layer,  Match,  Option,  Predicate,  Result,  Scope,  Schema,  ScopedCache,} from "effect";import type { HttpClient } from "effect/unstable/http"; import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";import * as z from "zod/v4"; import {  type CredentialBindingRef,  type CredentialBindingValue,  ScopeId,  SourceDetectionResult,  ToolResult,  authToolFailure,  defaultSourceInstallScopeId,  definePlugin,  tool,  resolveSecretBackedMap as resolveSharedSecretBackedMap,  type PluginCtx,  type StaticToolSchema,  type StorageFailure,  StorageError,  type ToolAnnotations,} from "@executor-js/sdk/core";import {  compileHttpNamedCredentialMap,  OAuth2SourceConfig,  httpCredentialInputToBindingValue,  type HttpConfiguredValueInput,} from "@executor-js/sdk/http-source"; import {  makeMcpStore,  mcpSchema,  type McpBindingStore,  type McpStoredSource,} from "./binding-store";import { createMcpConnector, type ConnectorInput, type McpConnection } from "./connection";import { discoverTools } from "./discover";import {  McpAuthRequiredError,  McpConnectionError,  McpInvocationError,  McpToolDiscoveryError,} from "./errors";import { invokeMcpTool } from "./invoke";import { deriveMcpNamespace, type McpToolManifest, type McpToolManifestEntry } from "./manifest";import { mcpPresets } from "./presets";import { probeMcpEndpointShape, type McpShapeProbeResult } from "./probe-shape";import {  MCP_OAUTH_CLIENT_ID_SLOT,  MCP_OAUTH_CLIENT_SECRET_SLOT,  MCP_OAUTH_CONNECTION_SLOT,  McpConnectionAuthInput,  McpConfiguredValueInput,  McpCredentialInput,  McpRemoteTransport,  McpToolBinding,  mcpHeaderSlot,  mcpQueryParamSlot,  type McpConnectionAuth,  type McpConfiguredValueInput as McpConfiguredValueInputType,  SecretBackedValue,  type McpStoredSourceData,  type ConfiguredMcpCredentialValue,} from "./types"; import {  type ConfigFileSink,  type ConfigHeaderValue,  type McpAuthConfig,  type McpRemoteSourceConfig as McpRemoteConfigEntry,  type McpStdioSourceConfig as McpStdioConfigEntry,  type SourceConfig,  headerToConfigValue,} 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, McpConfiguredValueInputType>;  readonly headers?: Record<string, McpConfiguredValueInputType>;  readonly namespace?: string;  readonly oauth2?: OAuth2SourceConfig;  readonly credentials?: McpInitialCredentialsInput;} 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;type McpConfigFileRemoteSourceConfig = Omit<  McpRemoteSourceConfig,  "headers" | "queryParams" | "oauth2"> & {  readonly headers?: Record<string, McpCredentialInput | McpConfiguredValueInputType>;  readonly queryParams?: Record<string, McpCredentialInput | McpConfiguredValueInputType>;  readonly auth?: McpConnectionAuthInput;};type McpConfigFileSourceConfig = McpConfigFileRemoteSourceConfig | 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;} const McpConfigureSourcePayloadSchema = Schema.Struct({  name: Schema.optional(Schema.String),  endpoint: Schema.optional(Schema.String),  headers: Schema.optional(Schema.Record(Schema.String, McpCredentialInput)),  queryParams: Schema.optional(Schema.Record(Schema.String, McpCredentialInput)),  auth: Schema.optional(McpConnectionAuthInput),});const McpConfigureSourceInputSchema = Schema.Struct({  scope: Schema.String,  ...McpConfigureSourcePayloadSchema.fields,});export type McpConfigureSourceInput = typeof McpConfigureSourceInputSchema.Type; const McpInitialCredentialsInputSchema = Schema.Struct({  scope: Schema.String,  headers: Schema.optional(Schema.Record(Schema.String, McpCredentialInput)),  queryParams: Schema.optional(Schema.Record(Schema.String, McpCredentialInput)),  auth: Schema.optional(McpConnectionAuthInput),});type McpInitialCredentialsInput = typeof McpInitialCredentialsInputSchema.Type; const McpRemoteAddSourceInputSchema = Schema.Struct({  transport: Schema.Literal("remote"),  name: Schema.String,  endpoint: Schema.String,  remoteTransport: Schema.optional(McpRemoteTransport),  queryParams: Schema.optional(Schema.Record(Schema.String, McpConfiguredValueInput)),  headers: Schema.optional(Schema.Record(Schema.String, McpConfiguredValueInput)),  namespace: Schema.optional(Schema.String),  oauth2: Schema.optional(OAuth2SourceConfig),  credentials: Schema.optional(McpInitialCredentialsInputSchema),}); const McpStdioAddSourceInputSchema = Schema.Struct({  transport: Schema.Literal("stdio"),  name: Schema.String,  command: Schema.String,  args: Schema.optional(Schema.Array(Schema.String)),  env: Schema.optional(Schema.Record(Schema.String, Schema.String)),  cwd: Schema.optional(Schema.String),  namespace: Schema.optional(Schema.String),}); const McpAddSourceInputSchema = Schema.Union([  McpRemoteAddSourceInputSchema,  McpStdioAddSourceInputSchema,]); const McpAddSourceOutputSchema = Schema.Struct({  namespace: Schema.String,  source: Schema.Struct({    id: Schema.String,    scope: Schema.String,  }),  toolCount: Schema.Number,  discovery: Schema.optional(    Schema.Struct({      status: Schema.Literals(["ok", "failed"]),      message: Schema.optional(Schema.String),      stage: Schema.optional(Schema.String),    }),  ),}); const McpProbeEndpointInputSchema = Schema.Struct({  endpoint: Schema.String,  headers: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)),  queryParams: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)),}); const McpProbeEndpointOutputSchema = Schema.Struct({  connected: Schema.Boolean,  requiresOAuth: Schema.Boolean,  supportsDynamicRegistration: Schema.Boolean,  name: Schema.String,  namespace: Schema.String,  toolCount: Schema.NullOr(Schema.Number),  serverName: Schema.NullOr(Schema.String),}); const McpGetSourceInputSchema = Schema.Struct({  namespace: Schema.String,  scope: Schema.String,}); const McpGetSourceOutputSchema = Schema.Struct({  source: Schema.NullOr(Schema.Unknown),}); const McpStaticConfigureSourceInputSchema = Schema.Struct({  source: Schema.Struct({    id: Schema.String,    scope: Schema.String,  }),  scope: Schema.String,  ...McpConfigureSourcePayloadSchema.fields,}); const McpStaticConfigureSourceOutputSchema = Schema.Struct({  configured: Schema.Boolean,}); const schemaToStaticToolSchema = <A, I>(schema: Schema.Decoder<A, I>): StaticToolSchema<A, I> =>  Schema.toStandardSchemaV1(Schema.toStandardJSONSchemaV1(schema) as never) as StaticToolSchema<    A,    I  >; const mcpToolFailure = (code: string, message: string, details?: unknown) =>  ToolResult.fail({    code,    message,    ...(details === undefined ? {} : { details }),  }); const mcpAuthToolFailure = (failure: McpAuthRequiredError) =>  authToolFailure({    code: failure.code,    message: failure.message,    source: { id: failure.sourceId, scope: failure.sourceScope },    credential: {      kind: failure.credentialKind,      ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}),      ...(failure.slotKey ? { slotKey: failure.slotKey } : {}),      ...(failure.secretId ? { secretId: failure.secretId } : {}),      ...(failure.connectionId ? { connectionId: failure.connectionId } : {}),    },    ...(failure.status !== undefined ? { status: failure.status } : {}),    ...(failure.details !== undefined      ? {          upstream: {            ...(failure.status !== undefined ? { status: failure.status } : {}),            details: failure.details,          },        }      : {}),    recovery: { configureSourceTool: "executor.mcp.configureSource" },  }); const McpAddSourceInputStandardSchema = schemaToStaticToolSchema(McpAddSourceInputSchema);const McpAddSourceOutputStandardSchema = schemaToStaticToolSchema(McpAddSourceOutputSchema);const McpProbeEndpointInputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointInputSchema);const McpProbeEndpointOutputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointOutputSchema);const McpGetSourceInputStandardSchema = schemaToStaticToolSchema(McpGetSourceInputSchema);const McpGetSourceOutputStandardSchema = schemaToStaticToolSchema(McpGetSourceOutputSchema);const McpStaticConfigureSourceInputStandardSchema = schemaToStaticToolSchema(  McpStaticConfigureSourceInputSchema,);const McpStaticConfigureSourceOutputStandardSchema = schemaToStaticToolSchema(  McpStaticConfigureSourceOutputSchema,); export type McpProbeEndpointInput = typeof McpProbeEndpointInputSchema.Type; const resolveStaticScopeInput = (  ctx: { readonly scopes: readonly { readonly id: ScopeId; readonly name: string }[] },  value: string,): string =>  String(    ctx.scopes.find((scope) => scope.name === value || String(scope.id) === value)?.id ?? value,  ); // ---------------------------------------------------------------------------// 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,  }); type JsonSchemaObject = Record<string, unknown> & {  readonly properties?: Record<string, unknown>;}; const McpCallToolResultJsonSchema = z.toJSONSchema(CallToolResultSchema) as JsonSchemaObject; const mcpCallToolResultOutputSchema = (structuredContentSchema?: unknown): JsonSchemaObject => {  const defaultStructuredContentSchema =    McpCallToolResultJsonSchema.properties?.structuredContent ?? {};   return {    ...McpCallToolResultJsonSchema,    properties: {      ...McpCallToolResultJsonSchema.properties,      structuredContent:        structuredContentSchema === undefined          ? defaultStructuredContentSchema          : structuredContentSchema,      isError: { const: false },    },    required:      structuredContentSchema === undefined ? ["content"] : ["content", "structuredContent"],  };}; 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";const McpTextContent = Schema.Struct({ type: Schema.Literal("text"), text: Schema.String });const McpToolCallEnvelope = Schema.Struct({  isError: Schema.optional(Schema.Boolean),  content: Schema.optional(Schema.Array(Schema.Unknown)),}); const decodeMcpTextContent = Schema.decodeUnknownOption(McpTextContent);const decodeMcpToolCallEnvelope = Schema.decodeUnknownOption(McpToolCallEnvelope); const extractMcpErrorMessage = (content: unknown): string => {  if (Array.isArray(content)) {    for (const item of content) {      const decoded = Option.getOrUndefined(decodeMcpTextContent(item));      if (decoded !== undefined && decoded.text.length > 0) return decoded.text;    }  }  return "MCP tool returned an error";}; /** 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 resolveMcpSourceBinding = (  ctx: PluginCtx<McpBindingStore>,  sourceId: string,  sourceScope: string,  slot: string,): Effect.Effect<CredentialBindingRef | 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 ?? 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,

Lines 1–500 of 2358.