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, 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 { 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; /** 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";}; 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.`, 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];}; 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 () => {Lines 1–500 of 1621. 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, AuthTemplateSlug, ConnectionName, definePlugin, IntegrationAlreadyExistsError, IntegrationSlug, mergeAuthTemplates, OAuthClientSlug, 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 { 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; /** 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"; }; 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.`, 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]; }; 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 () => {