import { Effect, Layer, Option, Result, Schema } from "effect";import type { HttpClient } from "effect/unstable/http"; import type { OAuthClientProvider } from "@modelcontextprotocol/client"; import { callToolResultJsonSchema } from "./call-tool-result-schema.gen"; import { authToolFailure, AuthTemplateSlug, ConnectionName, definePlugin, endpointTelemetryAttributes, IntegrationAlreadyExistsError, IntegrationSlug, mergeAuthTemplates, OAuthClientSlug, sha256Hex, tool, ToolResult, type AuthMethodDescriptor, classifyHttpStatus, type HealthCheckResult, 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/core"; import { TOKEN_VARIABLE, describeApiKeyAuthMethod, describeNoneAuthMethod, renderAuthPlacements, requiredPlacementVariables,} from "@executor-js/sdk/http-auth"; import type { CodexPluginEntry } from "./codex-plugins";import { createMcpConnector, type ConnectorInput, type McpConnector } from "./connection";import { createMcpConnectionPool } from "./connection-pool";import { discoverToolsFromInput } from "./discover";import { McpConnectionError, type McpConnectionFailureKind, 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 { recoverSlackConnectFile } from "./slack-connect-file";import { McpAuthMethodInput, McpAuthShorthand, McpRemoteTransport, type McpAuthMethod, type McpToolAnnotations, McpToolMeta, expandMcpAuthMethodInputs, mcpAuthMethodFromShorthand, normalizeMcpAuthMethods, McpStdioVersionNegotiation, parseMcpIntegrationConfig, type McpIntegrationConfig as McpIntegrationConfigType, type McpStdioEnvMethod, type McpStdioIntegrationConfig,} from "./types"; const MCP_PLUGIN_ID = "mcp" as const; /** Classify a failed liveness probe. The structural `httpStatus` carried on the * connect error is the primary signal (401/403 = auth wall = expired); message * substrings are only a fallback for causes with no status (OAuth * re-authorization, servers whose auth rejection isn't an HTTP status). * Anything else (server down, wrong transport) is degraded, not a credential * problem. */const mcpLivenessFailureStatus = (failure: { readonly message: string; readonly httpStatus?: number;}): "expired" | "degraded" => { if (failure.httpStatus !== undefined) { // A failed connect can't be healthy; the shared classifier decides // expired vs degraded so "which statuses mean expired" lives in one place. const classified = classifyHttpStatus(failure.httpStatus); return classified === "expired" ? "expired" : "degraded"; } const lower = failure.message.toLowerCase(); const authWalled = lower.includes("oauth re-authorization") || lower.includes("unauthorized") || lower.includes("forbidden"); return authWalled ? "expired" : "degraded";}; /** Enumerable failure MECHANISM for a failed liveness probe, beside the status * classifier above: a hit deadline is `probe_timeout` (a slow-but-alive * server, not a dead one), an HTTP verdict is `upstream_status`, and anything * else (transport, protocol, config) is `probe_failed`. Structural fields * only — never the message. */const mcpLivenessFailureReason = (failure: { readonly httpStatus?: number; readonly timedOut?: boolean; readonly failureKind?: McpConnectionFailureKind;}): "probe_timeout" | "upstream_status" | "probe_failed" => { if (failure.timedOut === true || failure.failureKind === "timeout") return "probe_timeout"; if (failure.httpStatus !== undefined) return "upstream_status"; return "probe_failed";}; 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, the upstream annotations, and// the tool's reserved `_meta` map 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; /** The tool's reserved MCP `_meta` map, opaque to core and to the model. */ readonly _meta?: McpToolMeta;} 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), }), ), _meta: Schema.optional(McpToolMeta),});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, /** Optional catalog family used to group related integrations. */ family: Schema.optional(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), /** Pin protocol negotiation. `legacy` is for servers that echo the proposed * 2026-07-28 revision and then violate its response contract; the probe * reports when it had to fall back, and the add flow passes that through. */ versionNegotiation: Schema.optional(McpStdioVersionNegotiation),}); const McpStdioServerInputSchema = Schema.Struct({ transport: Schema.Literal("stdio"), name: Schema.String, /** Optional catalog family used to group related integrations. */ family: Schema.optional(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)), /** Non-secret environment the server needs, stored on the integration and * injected verbatim at spawn. * * Separate from `env` because that channel makes every variable a * CREDENTIAL: it is declared as a `stdio_env` method and the user is asked * to type its value on a masked form. That is right for an API key and * wrong for a machine-derived path — a Codex plugin's `CODEX_HOME` is * already known to the scanner, is not a secret, and must never become a * field a person has to fill in. Nothing here is a credential, so it does * not appear in `authenticationTemplate`. */ staticEnv: Schema.optional(Schema.Record(Schema.String, Schema.String)), cwd: Schema.optional(Schema.String), /** Protocol negotiation at connect: `auto` probes `server/discover` (spec * 2026-07-28) for modern-only servers. Defaults to the legacy `initialize` * handshake — the right call for spawn-per-call servers, where the auto * probe costs an extra child process per connect. */ versionNegotiation: Schema.optional(McpStdioVersionNegotiation), /** Opt out of process reuse — spawn a fresh child for every tool call (see * `McpStdioIntegrationConfig.spawnPerCall`). */ spawnPerCall: Schema.optional(Schema.Boolean), /** Reach the server through the Codex app-server bridge: the command spawns * `codex app-server` and `server` names the MCP server inside Codex whose * tools this integration exposes. Set by the Codex plugin add flow. */ appServer: Schema.optional( Schema.Struct({ server: Schema.String, surface: Schema.optional(Schema.Literals(["sky", "browser"])), modulePath: Schema.optional(Schema.String), presetId: 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), /** Present when discovery succeeded: which protocol negotiation worked. * `legacy` means the server echoed the modern revision and then broke its * contract — the add should pin `versionNegotiation: "legacy"`. */ versionNegotiation: Schema.optional(McpStdioVersionNegotiation),}); // ---------------------------------------------------------------------------// 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; readonly insufficientScope?: boolean;}) => { // A 403 that named a scope shortfall is unfixable by re-running the same // grant, so it carries its own code (and recovery without the oauth.start // hint) instead of the re-authenticate loop. if (input.status === 403 && input.insufficientScope === true) { return authToolFailure({ code: "oauth_scope_insufficient", message: `MCP server rejected connection "${input.connection}" with HTTP 403: the connection's OAuth grant does not cover the scope this tool requires. Re-authenticating with the same grant will return the same error; reconnect with broader access.`, integration: { id: input.integration }, credential: { kind: "oauth", label: input.connection }, status: input.status, upstream: { status: input.status }, }); } return 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.`, integration: { 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.`, integration: { 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];}; /** Exported for tests: the credential/non-credential split is a security * boundary (a value in `env` becomes something the user is asked to type), * and asserting it through the whole add flow would not show it. */export 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); const staticEnv = input.staticEnv; return { transport: "stdio", family: input.family?.trim() || undefined, command: input.command, args: input.args ? [...input.args] : undefined, env: staticEnv !== undefined && Object.keys(staticEnv).length > 0 ? staticEnv : undefined, cwd: input.cwd, versionNegotiation: input.versionNegotiation, spawnPerCall: input.spawnPerCall, appServer: input.appServer, authenticationTemplate: vars.length > 0 ? [{ slug: STDIO_ENV_TEMPLATE, kind: "stdio_env", vars }] : [{ slug: "none", kind: "none" }], }; } return { transport: "remote", family: input.family?.trim() || undefined, endpoint: input.endpoint, remoteTransport: input.remoteTransport ?? "auto", queryParams: input.queryParams, headers: input.headers, authenticationTemplate: input.authenticationTemplate ? normalizeMcpAuthMethods(input.authenticationTemplate) : [mcpAuthMethodFromShorthand(input.auth ?? { kind: "none" })], versionNegotiation: input.versionNegotiation, };}; type JsonSchemaObject = Record<string, unknown> & { readonly properties?: Record<string, unknown>;}; // Baked at generation time rather than derived from @modelcontextprotocol/core// at module scope — importing core costs ~8.6MB of heap per Cloudflare isolate// (see client-module.ts), and this schema is the only thing the plugin needs// from it outside a live connection.const McpCallToolResultJsonSchema: JsonSchemaObject = callToolResultJsonSchema; 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, the upstream annotations, and the tool's `_meta` mapLines 1–500 of 2088. Load more
import { Effect, Layer, Option, Result, Schema } from "effect"; import type { HttpClient } from "effect/unstable/http"; import type { OAuthClientProvider } from "@modelcontextprotocol/client"; import { callToolResultJsonSchema } from "./call-tool-result-schema.gen"; import { authToolFailure, AuthTemplateSlug, ConnectionName, definePlugin, endpointTelemetryAttributes, IntegrationAlreadyExistsError, IntegrationSlug, mergeAuthTemplates, OAuthClientSlug, sha256Hex, tool, ToolResult, type AuthMethodDescriptor, classifyHttpStatus, type HealthCheckResult, 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/core"; import { TOKEN_VARIABLE, describeApiKeyAuthMethod, describeNoneAuthMethod, renderAuthPlacements, requiredPlacementVariables, } from "@executor-js/sdk/http-auth"; import type { CodexPluginEntry } from "./codex-plugins"; import { createMcpConnector, type ConnectorInput, type McpConnector } from "./connection"; import { createMcpConnectionPool } from "./connection-pool"; import { discoverToolsFromInput } from "./discover"; import { McpConnectionError, type McpConnectionFailureKind, 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 { recoverSlackConnectFile } from "./slack-connect-file"; import { McpAuthMethodInput, McpAuthShorthand, McpRemoteTransport, type McpAuthMethod, type McpToolAnnotations, McpToolMeta, expandMcpAuthMethodInputs, mcpAuthMethodFromShorthand, normalizeMcpAuthMethods, McpStdioVersionNegotiation, parseMcpIntegrationConfig, type McpIntegrationConfig as McpIntegrationConfigType, type McpStdioEnvMethod, type McpStdioIntegrationConfig, } from "./types"; const MCP_PLUGIN_ID = "mcp" as const; /** Classify a failed liveness probe. The structural `httpStatus` carried on the * connect error is the primary signal (401/403 = auth wall = expired); message * substrings are only a fallback for causes with no status (OAuth * re-authorization, servers whose auth rejection isn't an HTTP status). * Anything else (server down, wrong transport) is degraded, not a credential * problem. */ const mcpLivenessFailureStatus = (failure: { readonly message: string; readonly httpStatus?: number; }): "expired" | "degraded" => { if (failure.httpStatus !== undefined) { // A failed connect can't be healthy; the shared classifier decides // expired vs degraded so "which statuses mean expired" lives in one place. const classified = classifyHttpStatus(failure.httpStatus); return classified === "expired" ? "expired" : "degraded"; } const lower = failure.message.toLowerCase(); const authWalled = lower.includes("oauth re-authorization") || lower.includes("unauthorized") || lower.includes("forbidden"); return authWalled ? "expired" : "degraded"; }; /** Enumerable failure MECHANISM for a failed liveness probe, beside the status * classifier above: a hit deadline is `probe_timeout` (a slow-but-alive * server, not a dead one), an HTTP verdict is `upstream_status`, and anything * else (transport, protocol, config) is `probe_failed`. Structural fields * only — never the message. */ const mcpLivenessFailureReason = (failure: { readonly httpStatus?: number; readonly timedOut?: boolean; readonly failureKind?: McpConnectionFailureKind; }): "probe_timeout" | "upstream_status" | "probe_failed" => { if (failure.timedOut === true || failure.failureKind === "timeout") return "probe_timeout"; if (failure.httpStatus !== undefined) return "upstream_status"; return "probe_failed"; }; 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, the upstream annotations, and // the tool's reserved `_meta` map 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; /** The tool's reserved MCP `_meta` map, opaque to core and to the model. */ readonly _meta?: McpToolMeta; } 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), }), ), _meta: Schema.optional(McpToolMeta), }); 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, /** Optional catalog family used to group related integrations. */ family: Schema.optional(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), /** Pin protocol negotiation. `legacy` is for servers that echo the proposed * 2026-07-28 revision and then violate its response contract; the probe * reports when it had to fall back, and the add flow passes that through. */ versionNegotiation: Schema.optional(McpStdioVersionNegotiation), }); const McpStdioServerInputSchema = Schema.Struct({ transport: Schema.Literal("stdio"), name: Schema.String, /** Optional catalog family used to group related integrations. */ family: Schema.optional(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)), /** Non-secret environment the server needs, stored on the integration and * injected verbatim at spawn. * * Separate from `env` because that channel makes every variable a * CREDENTIAL: it is declared as a `stdio_env` method and the user is asked * to type its value on a masked form. That is right for an API key and * wrong for a machine-derived path — a Codex plugin's `CODEX_HOME` is * already known to the scanner, is not a secret, and must never become a * field a person has to fill in. Nothing here is a credential, so it does * not appear in `authenticationTemplate`. */ staticEnv: Schema.optional(Schema.Record(Schema.String, Schema.String)), cwd: Schema.optional(Schema.String), /** Protocol negotiation at connect: `auto` probes `server/discover` (spec * 2026-07-28) for modern-only servers. Defaults to the legacy `initialize` * handshake — the right call for spawn-per-call servers, where the auto * probe costs an extra child process per connect. */ versionNegotiation: Schema.optional(McpStdioVersionNegotiation), /** Opt out of process reuse — spawn a fresh child for every tool call (see * `McpStdioIntegrationConfig.spawnPerCall`). */ spawnPerCall: Schema.optional(Schema.Boolean), /** Reach the server through the Codex app-server bridge: the command spawns * `codex app-server` and `server` names the MCP server inside Codex whose * tools this integration exposes. Set by the Codex plugin add flow. */ appServer: Schema.optional( Schema.Struct({ server: Schema.String, surface: Schema.optional(Schema.Literals(["sky", "browser"])), modulePath: Schema.optional(Schema.String), presetId: 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), /** Present when discovery succeeded: which protocol negotiation worked. * `legacy` means the server echoed the modern revision and then broke its * contract — the add should pin `versionNegotiation: "legacy"`. */ versionNegotiation: Schema.optional(McpStdioVersionNegotiation), }); // --------------------------------------------------------------------------- // 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; readonly insufficientScope?: boolean; }) => { // A 403 that named a scope shortfall is unfixable by re-running the same // grant, so it carries its own code (and recovery without the oauth.start // hint) instead of the re-authenticate loop. if (input.status === 403 && input.insufficientScope === true) { return authToolFailure({ code: "oauth_scope_insufficient", message: `MCP server rejected connection "${input.connection}" with HTTP 403: the connection's OAuth grant does not cover the scope this tool requires. Re-authenticating with the same grant will return the same error; reconnect with broader access.`, integration: { id: input.integration }, credential: { kind: "oauth", label: input.connection }, status: input.status, upstream: { status: input.status }, }); } return 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.`, integration: { 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.`, integration: { 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]; }; /** Exported for tests: the credential/non-credential split is a security * boundary (a value in `env` becomes something the user is asked to type), * and asserting it through the whole add flow would not show it. */ export 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); const staticEnv = input.staticEnv; return { transport: "stdio", family: input.family?.trim() || undefined, command: input.command, args: input.args ? [...input.args] : undefined, env: staticEnv !== undefined && Object.keys(staticEnv).length > 0 ? staticEnv : undefined, cwd: input.cwd, versionNegotiation: input.versionNegotiation, spawnPerCall: input.spawnPerCall, appServer: input.appServer, authenticationTemplate: vars.length > 0 ? [{ slug: STDIO_ENV_TEMPLATE, kind: "stdio_env", vars }] : [{ slug: "none", kind: "none" }], }; } return { transport: "remote", family: input.family?.trim() || undefined, endpoint: input.endpoint, remoteTransport: input.remoteTransport ?? "auto", queryParams: input.queryParams, headers: input.headers, authenticationTemplate: input.authenticationTemplate ? normalizeMcpAuthMethods(input.authenticationTemplate) : [mcpAuthMethodFromShorthand(input.auth ?? { kind: "none" })], versionNegotiation: input.versionNegotiation, }; }; type JsonSchemaObject = Record<string, unknown> & { readonly properties?: Record<string, unknown>; }; // Baked at generation time rather than derived from @modelcontextprotocol/core // at module scope — importing core costs ~8.6MB of heap per Cloudflare isolate // (see client-module.ts), and this schema is the only thing the plugin needs // from it outside a live connection. const McpCallToolResultJsonSchema: JsonSchemaObject = callToolResultJsonSchema; 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, the upstream annotations, and the tool's `_meta` map