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, 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 } 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 McpRemoteIntegrationConfig, 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)), 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, }); const toIntegrationConfig = (input: McpServerInput): McpIntegrationConfigType => { if (input.transport === "stdio") { return { transport: "stdio", command: input.command, args: input.args ? [...input.args] : undefined, env: input.env, cwd: input.cwd, }; } 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";}; /** 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 = ( config: McpRemoteIntegrationConfig, templateSlug: string | null,): McpAuthMethod | undefined => { const methods = config.authenticationTemplate; if (templateSlug !== null) { const match = methods.find((method: McpAuthMethod) => method.slug === templateSlug); if (match) return match; } return methods.length === 1 ? methods[0] : undefined;}; const buildConnectorInput = ( config: McpIntegrationConfigType, values: Record<string, string | null>, templateSlug: string | null, allowStdio: boolean, httpClientLayer?: Layer.Layer<HttpClient.HttpClient>,): Effect.Effect<ConnectorInput, McpConnectionError> => { if (config.transport === "stdio") { if (!allowStdio) { return Effect.fail( new McpConnectionError({ transport: "stdio", message: "MCP stdio transport is disabled. Enable it by passing `dangerouslyAllowStdioMCP: true` to mcpPlugin() — only safe for trusted local contexts.", }), ); } return Effect.succeed({ transport: "stdio" as const, command: config.command, args: config.args, env: config.env, cwd: config.cwd, } satisfies McpStdioIntegrationConfig); } // Credential placements render OVER the integration's static headers / // query params — a same-named static entry is overwritten. const headers: Record<string, string> = { ...(config.headers ?? {}) }; const queryParams: Record<string, string> = { ...(config.queryParams ?? {}) }; let authProvider: OAuthClientProvider | undefined; const auth = selectAuthMethod(config, templateSlug); if (auth?.kind === "apikey") {Lines 1–500 of 1278. Load more
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, 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 } 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 McpRemoteIntegrationConfig, 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)), 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, }); const toIntegrationConfig = (input: McpServerInput): McpIntegrationConfigType => { if (input.transport === "stdio") { return { transport: "stdio", command: input.command, args: input.args ? [...input.args] : undefined, env: input.env, cwd: input.cwd, }; } 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"; }; /** 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 = ( config: McpRemoteIntegrationConfig, templateSlug: string | null, ): McpAuthMethod | undefined => { const methods = config.authenticationTemplate; if (templateSlug !== null) { const match = methods.find((method: McpAuthMethod) => method.slug === templateSlug); if (match) return match; } return methods.length === 1 ? methods[0] : undefined; }; const buildConnectorInput = ( config: McpIntegrationConfigType, values: Record<string, string | null>, templateSlug: string | null, allowStdio: boolean, httpClientLayer?: Layer.Layer<HttpClient.HttpClient>, ): Effect.Effect<ConnectorInput, McpConnectionError> => { if (config.transport === "stdio") { if (!allowStdio) { return Effect.fail( new McpConnectionError({ transport: "stdio", message: "MCP stdio transport is disabled. Enable it by passing `dangerouslyAllowStdioMCP: true` to mcpPlugin() — only safe for trusted local contexts.", }), ); } return Effect.succeed({ transport: "stdio" as const, command: config.command, args: config.args, env: config.env, cwd: config.cwd, } satisfies McpStdioIntegrationConfig); } // Credential placements render OVER the integration's static headers / // query params — a same-named static entry is overwritten. const headers: Record<string, string> = { ...(config.headers ?? {}) }; const queryParams: Record<string, string> = { ...(config.queryParams ?? {}) }; let authProvider: OAuthClientProvider | undefined; const auth = selectAuthMethod(config, templateSlug); if (auth?.kind === "apikey") {