1535 lines · 61.8 KB · TypeScript
RawDownload
import { Effect, Layer, Option, Result, Schema } 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 {  authToolFailure,  AuthTemplateSlug,  ConnectionName,  definePlugin,  IntegrationAlreadyExistsError,  IntegrationSlug,  mergeAuthTemplates,  OAuthClientSlug,  tool,  ToolResult,  type AuthMethodDescriptor,  type Integration,  type IntegrationConfig,  type IntegrationRecord,  type OAuthClientSummary,  type Owner,  type PluginCtx,  type StaticToolSchema,  type StorageFailure,  type ToolAnnotations,  type ToolDef,  ToolName,} from "@executor-js/sdk"; import {  TOKEN_VARIABLE,  describeApiKeyAuthMethod,  describeNoneAuthMethod,  renderAuthPlacements,  requiredPlacementVariables,} from "@executor-js/sdk/http-auth"; import { createMcpConnector, type ConnectorInput, type McpConnector } from "./connection";import { discoverTools } from "./discover";import {  McpConnectionError,  McpOAuthReauthorizationRequired,  McpToolDiscoveryError,} from "./errors";import { invokeMcpTool, isUnknownToolMessage } from "./invoke";import { deriveMcpNamespace, type McpToolManifestEntry } from "./manifest";import { mcpPresets } from "./presets";import { probeMcpEndpointShape, type McpShapeProbeResult } from "./probe-shape";import {  McpAuthMethodInput,  McpAuthShorthand,  McpRemoteTransport,  type McpAuthMethod,  type McpToolAnnotations,  expandMcpAuthMethodInputs,  mcpAuthMethodFromShorthand,  normalizeMcpAuthMethods,  parseMcpIntegrationConfig,  type McpIntegrationConfig as McpIntegrationConfigType,  type McpStdioEnvMethod,  type McpStdioIntegrationConfig,} from "./types"; const MCP_PLUGIN_ID = "mcp" as const; const legacyOAuthClientSlugCandidate = (value: string): string | null => {  const slug = value    .trim()    .toLowerCase()    .replace(/[^a-z0-9]+/g, "-")    .replace(/^-+|-+$/g, "");  return slug.length > 0 ? slug : null;}; const legacyOAuthClientSlugCandidates = (  slug: string,  integration: (Integration & { readonly config: IntegrationConfig }) | null,): ReadonlySet<string> => {  const candidates = new Set<string>();  const fromSlug = legacyOAuthClientSlugCandidate(slug);  if (fromSlug) candidates.add(fromSlug);  const fromDescription =    integration == null ? null : legacyOAuthClientSlugCandidate(integration.description);  if (fromDescription) candidates.add(fromDescription);  return candidates;}; const oauthClientKey = (owner: Owner, slug: OAuthClientSlug): string => `${owner}:${String(slug)}`; const legacyMcpClientMatches = (  client: OAuthClientSummary,  candidates: ReadonlySet<string>,  config: McpIntegrationConfigType | null,): boolean => {  if (!candidates.has(String(client.slug))) return false;  if (    !config ||    config.transport !== "remote" ||    !config.authenticationTemplate.some((method: McpAuthMethod) => method.kind === "oauth2")  ) {    return false;  }  return client.grant === "authorization_code" && (client.resource ?? null) === config.endpoint;}; // ---------------------------------------------------------------------------// Tool annotations carry an `mcp` envelope alongside the executor's policy// hints. The executor persists `ToolDef.annotations` verbatim into the tool// row's JSON column, so the real MCP tool name + upstream annotations survive// to `invokeTool` / `resolveAnnotations` with no plugin-side store (resolveTools// has no ctx to write one anyway). The envelope is opaque to core.// --------------------------------------------------------------------------- interface McpToolStamp {  readonly toolName: string;  readonly upstream?: McpToolAnnotations;} type StampedAnnotations = ToolAnnotations & { readonly mcp: McpToolStamp }; const McpStampSchema = Schema.Struct({  toolName: Schema.String,  upstream: Schema.optional(    Schema.Struct({      title: Schema.optional(Schema.String),      readOnlyHint: Schema.optional(Schema.Boolean),      destructiveHint: Schema.optional(Schema.Boolean),      idempotentHint: Schema.optional(Schema.Boolean),      openWorldHint: Schema.optional(Schema.Boolean),    }),  ),});const AnnotationsWithStamp = Schema.Struct({ mcp: McpStampSchema });const decodeStamp = Schema.decodeUnknownOption(AnnotationsWithStamp); const readStamp = (annotations: unknown): McpToolStamp | null =>  Option.match(decodeStamp(annotations), {    onNone: () => null,    onSome: (decoded) => decoded.mcp,  }); // ---------------------------------------------------------------------------// Extension input shapes — `addServer` registers an MCP integration. A// connection (the credential) is then created against it via// `executor.connections.create` / `oauth.start`.// --------------------------------------------------------------------------- const McpRemoteServerInputSchema = Schema.Struct({  transport: Schema.optional(Schema.Literal("remote")),  name: Schema.String,  /** Agent-visible catalog description. Defaults to the display name. */  description: Schema.optional(Schema.String),  endpoint: Schema.String,  remoteTransport: Schema.optional(McpRemoteTransport),  headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),  queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),  slug: Schema.optional(Schema.String),  /** Declared auth methods a connection can be applied through. */  authenticationTemplate: Schema.optional(Schema.Array(McpAuthMethodInput)),  /** Single-method shorthand (legacy callers). Ignored when   *  `authenticationTemplate` is present. Defaults to none. */  auth: Schema.optional(McpAuthShorthand),}); const McpStdioServerInputSchema = Schema.Struct({  transport: Schema.Literal("stdio"),  name: Schema.String,  description: Schema.optional(Schema.String),  command: Schema.String,  args: Schema.optional(Schema.Array(Schema.String)),  /** DECLARE the secret env vars this server needs, by NAME. Their values are   *  supplied as the connection's secret credentials, not here — so the UI   *  defines what env vars exist and the connect step provides the secrets. */  envVars: Schema.optional(Schema.Array(Schema.String)),  /** Provide secret env values directly (programmatic / agent one-shot): the   *  add then auto-creates the connection holding them. The UI uses `envVars`   *  instead and leaves the values to the connect step. */  env: Schema.optional(Schema.Record(Schema.String, Schema.String)),  cwd: Schema.optional(Schema.String),  slug: Schema.optional(Schema.String),}); const McpAddServerInputSchema = Schema.Union([  McpRemoteServerInputSchema,  McpStdioServerInputSchema,]); const McpAddServerOutputSchema = Schema.Struct({  slug: Schema.String,}); /** Input for the custom-method-create flow. `merge` (default) appends onto the *  integration's existing `authenticationTemplate`; `replace` swaps the whole *  declared set. Mirrors the OpenAPI/GraphQL `configureAuth` inputs. */export const McpConfigureAuthInputSchema = Schema.Struct({  authenticationTemplate: Schema.Array(McpAuthMethodInput),  mode: Schema.optional(Schema.Literals(["merge", "replace"])),});export type McpConfigureAuthInput = typeof McpConfigureAuthInputSchema.Type; const McpProbeEndpointInputSchema = Schema.Struct({  endpoint: Schema.String,  headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),  queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),}); const McpProbeEndpointOutputSchema = Schema.Struct({  connected: Schema.Boolean,  requiresAuthentication: Schema.Boolean,  requiresOAuth: Schema.Boolean,  supportsDynamicRegistration: Schema.Boolean,  name: Schema.String,  slug: Schema.String,  toolCount: Schema.NullOr(Schema.Number),  serverName: Schema.NullOr(Schema.String),  /** The server's `instructions` from initialize — prefill for the add form's   *  description. Only available when the probe connected unauthenticated. */  instructions: Schema.NullOr(Schema.String),}); // ---------------------------------------------------------------------------// Extension input/output shapes — `addServer` registers an MCP integration. A// connection (the credential) is then created against it via// `executor.connections.create` / `oauth.start`. Types are inferred from the// schemas above so the wire shape and the TS surface can't drift.// --------------------------------------------------------------------------- export type McpRemoteServerInput = typeof McpRemoteServerInputSchema.Type;export type McpStdioServerInput = typeof McpStdioServerInputSchema.Type;export type McpServerInput = typeof McpAddServerInputSchema.Type;export type McpProbeResult = typeof McpProbeEndpointOutputSchema.Type;export type McpProbeEndpointInput = typeof McpProbeEndpointInputSchema.Type; const McpGetServerInputSchema = Schema.Struct({  slug: Schema.String,}); const McpGetServerOutputSchema = Schema.Struct({  integration: Schema.NullOr(Schema.Unknown),}); const schemaToStaticToolSchema = <A, I>(schema: Schema.Decoder<A, I>): StaticToolSchema<A, I> =>  Schema.toStandardSchemaV1(Schema.toStandardJSONSchemaV1(schema) as never) as StaticToolSchema<    A,    I  >; const McpAddServerInputStandardSchema = schemaToStaticToolSchema(McpAddServerInputSchema);const McpAddServerOutputStandardSchema = schemaToStaticToolSchema(McpAddServerOutputSchema);const McpProbeEndpointInputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointInputSchema);const McpProbeEndpointOutputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointOutputSchema);const McpGetServerInputStandardSchema = schemaToStaticToolSchema(McpGetServerInputSchema);const McpGetServerOutputStandardSchema = schemaToStaticToolSchema(McpGetServerOutputSchema); const mcpToolFailure = (code: string, message: string, details?: unknown) =>  ToolResult.fail({    code,    message,    ...(details === undefined ? {} : { details }),  }); const mcpInvocationAuthFailure = (input: {  readonly status: 401 | 403;  readonly integration: string;  readonly connection: string;}) =>  authToolFailure({    code: "connection_rejected",    message:      input.status === 403        ? `MCP server rejected connection "${input.connection}" with HTTP 403. The credential may lack access or required scope; re-authenticate or update the connection before retrying this tool.`        : `MCP server rejected connection "${input.connection}" with HTTP 401. Re-authenticate or update the connection before retrying this tool.`,    source: { id: input.integration },    credential: { kind: "upstream", label: input.connection },    status: input.status,    upstream: { status: input.status },  }); const mcpInvocationOAuthReauthFailure = (input: {  readonly integration: string;  readonly connection: string;}) =>  authToolFailure({    code: "oauth_reauth_required",    message: `OAuth connection "${input.connection}" requires reauthorization before retrying this MCP tool.`,    source: { id: input.integration },    credential: { kind: "oauth", label: input.connection },  }); // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- const slugFrom = (slug: string): IntegrationSlug => IntegrationSlug.make(slug); const normalizeSlug = (input: McpServerInput): string =>  input.slug ??  deriveMcpNamespace({    name: input.name,    endpoint: input.transport === "stdio" ? undefined : input.endpoint,    command: input.transport === "stdio" ? input.command : undefined,  }); /** Slug for a stdio server's secret-env auth method (one per integration). */const STDIO_ENV_TEMPLATE = "env"; /** The secret env var NAMES a stdio add declares: the explicit `envVars` *  declaration plus the keys of any one-shot `env` values, de-duplicated and *  order-preserving. */const stdioEnvVarNames = (input: McpStdioServerInput): readonly string[] => {  const names = new Set<string>(input.envVars ?? []);  for (const key of Object.keys(input.env ?? {})) names.add(key);  return [...names];}; const toIntegrationConfig = (input: McpServerInput): McpIntegrationConfigType => {  if (input.transport === "stdio") {    // The config only DECLARES the secret env vars by NAME (a `stdio_env`    // method); their values are credentials and live on the connection, never    // in this blob. Names come from the explicit `envVars` declaration and/or    // the keys of any one-shot `env` values.    const vars = stdioEnvVarNames(input);    return {      transport: "stdio",      command: input.command,      args: input.args ? [...input.args] : undefined,      cwd: input.cwd,      authenticationTemplate:        vars.length > 0          ? [{ slug: STDIO_ENV_TEMPLATE, kind: "stdio_env", vars }]          : [{ slug: "none", kind: "none" }],    };  }  return {    transport: "remote",    endpoint: input.endpoint,    remoteTransport: input.remoteTransport ?? "auto",    queryParams: input.queryParams,    headers: input.headers,    authenticationTemplate: input.authenticationTemplate      ? normalizeMcpAuthMethods(input.authenticationTemplate)      : [mcpAuthMethodFromShorthand(input.auth ?? { kind: "none" })],  };}; 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"],  };}; /** Build the executor-facing ToolDef for one discovered MCP tool, stamping the *  real MCP tool name + upstream annotations into the persisted annotations so *  they survive to invokeTool with no plugin-side store. */const toToolDef = (entry: McpToolManifestEntry): ToolDef => {  const destructive = entry.annotations?.destructiveHint === true;  const stamp: McpToolStamp = {    toolName: entry.toolName,    ...(entry.annotations ? { upstream: entry.annotations } : {}),  };  const annotations: StampedAnnotations = {    requiresApproval: destructive,    ...(destructive ? { approvalDescription: entry.annotations?.title ?? entry.toolName } : {}),    mcp: stamp,  };  return {    name: ToolName.make(entry.toolId),    description: entry.description ?? `MCP tool: ${entry.toolName}`,    inputSchema: entry.inputSchema,    outputSchema: mcpCallToolResultOutputSchema(entry.outputSchema),    annotations: annotations as ToolAnnotations,  };}; 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";}; // The server no longer advertises this tool — the persisted catalog drifted.// Answered after marking the connection stale so the next tools read re-lists;// the message tells the caller to re-list instead of retrying blind.const unknownToolFailure = (  toolName: string,  credential: { readonly integration: unknown; readonly connection: unknown },) =>  ToolResult.fail({    code: "mcp_tool_unknown",    message: `The MCP server no longer provides tool "${toolName}". Its tool catalog changed; list tools again for the current set.`,    details: {      integration: String(credential.integration),      connection: String(credential.connection),    },  }); /** 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. */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 hard-stop probe outcome into a message a user can act on. *  Auth-required shapes are routed to the auth editor upstream, so they never *  reach here; the only outcomes are an unreachable endpoint or a non-MCP one. *  Exported for tests. */export const userFacingProbeMessage = (  shape: Extract<McpShapeProbeResult, { readonly kind: "unreachable" | "not-mcp" }>,): string =>  shape.kind === "unreachable"    ? "Couldn't reach this URL. Check the address, your network, and that the server is running."    : "This URL doesn't appear to host an MCP server. Double-check the address, including the path."; // ---------------------------------------------------------------------------// MCP-SDK OAuth provider adapter — wraps a pre-resolved access token so the// transport sends it as a Bearer header. Refresh is core's responsibility// (the connection row carries the OAuth grant); this adapter never initiates// a new flow and fails loudly if the SDK tries to.// --------------------------------------------------------------------------- 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 -- boundary: MCP SDK OAuthClientProvider callback can only signal reauthorization by throwing    throw new McpOAuthReauthorizationRequired({      message: "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");  },  saveDiscoveryState: () => undefined,  discoveryState: () => undefined,}); // ---------------------------------------------------------------------------// Connector input — render the integration config + the connection's resolved// value through the auth method the connection references (by template slug)// into a live `ConnectorInput`.// --------------------------------------------------------------------------- /** The auth method a connection binds: its `template` slug when it matches a *  declared method. Otherwise fall back to the sole declared method — single- *  method integrations historically accepted any template slug (rendering was *  config-driven), so existing connections keep working. Ambiguity across *  several methods renders no auth rather than guessing. */const selectAuthMethod = (

Lines 1–500 of 1535.