import { Data, Duration, Effect, Match, Option, Predicate, Result, Schema } from "effect";import * as Cause from "effect/Cause";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { ContentBlockSchema, type ClientCapabilities, type ContentBlock,} from "@modelcontextprotocol/sdk/types.js";import { getUiCapability, registerAppResource, registerAppTool, RESOURCE_MIME_TYPE,} from "@modelcontextprotocol/ext-apps/server";import type { jsonSchemaValidator, JsonSchemaType, JsonSchemaValidator,} from "@modelcontextprotocol/sdk/validation/types.js";import { Validator } from "@cfworker/json-schema";import * as z from "zod/v4"; import { isToolFile, sanitizeArtifactPreviewMarkup } from "@executor-js/sdk";import type { Artifact, ArtifactBinding, ArtifactSummary, ElicitationResponse, ElicitationHandler, ElicitationContext, ElicitationRequest, SaveArtifactInput, ToolFileValue,} from "@executor-js/sdk";import type * as Tracer from "effect/Tracer";import { createExecutionEngine, formatExecuteResult, formatPausedExecution, formatTtlDuration, findSkill, renderSkillsIndex, skillCatalogFor, EXECUTE_SKILL, INTEGRATION_INVENTORY_HEADER, type Skill, type ExecutionEngine, type ExecutionEngineConfig, type ResumeResponse, type ExecutionResult, type PausedExecution, type PausedExecutionDeadline,} from "@executor-js/execution";import { MCP_APPS_SHELL_RESOURCE_URI, applyArtifactEdits, smokeRenderRejection, validateArtifactCode, type ArtifactEdit, type ArtifactSmokeRenderResult,} from "./create-artifact";import { TOOL_CALL_CONTRACT_MESSAGE } from "./tool-call-code";import { resolveArtifactAction } from "./artifact-action";import { extractArtifactRoles, resolveArtifactBindings, type BindableConnection,} from "./artifact-bindings"; // ---------------------------------------------------------------------------// Workers-compatible JSON Schema validator (replaces Ajv which uses new Function())// --------------------------------------------------------------------------- class CfWorkerJsonSchemaValidator implements jsonSchemaValidator { getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> { const validator = new Validator(schema as Record<string, unknown>, "2020-12", false); return (input: unknown) => { const result = validator.validate(input); if (result.valid) { return { valid: true, data: input as T, errorMessage: undefined }; } const errorMessage = result.errors.map((e) => `${e.instanceLocation}: ${e.error}`).join("; "); return { valid: false, data: undefined, errorMessage }; }; }} // ---------------------------------------------------------------------------// Config// --------------------------------------------------------------------------- type SharedMcpServerConfig = { /** * Pre-built `execute` tool description. When provided, the factory skips * its internal `engine.getDescription` yield. Useful when the caller * wants to compute the description inside its own Effect tracer context * so sub-spans (`executor.integrations.list`, `executor.tools.list`) nest as * children of the caller's root span. */ readonly description?: string; /** * Parent span override for engine calls. The factory captures the * caller's context at construction time, but `Effect.runPromiseWith` * starts a fresh fiber per SDK callback — so the `currentSpan` * FiberRef resets to root unless explicitly anchored. * * Accepts either a fixed span (per-request McpServer instances) or a * getter (session-scoped instances that need to anchor each callback * under whichever request triggered it; see the Cloud DO). */ readonly parentSpan?: Tracer.AnySpan | (() => Tracer.AnySpan | undefined); /** * Enable verbose MCP capability / elicitation debug logging. */ readonly debug?: boolean; /** * Controls how elicitation is handled for this MCP connection. The default * is model-managed resume, where paused executions expose interaction * metadata and the model can call `resume` with the user's response. */ readonly elicitationMode?: | { readonly mode: "browser"; readonly approvalUrl: (executionId: string) => string; } | { readonly mode: "model"; } | { readonly mode: "native"; }; readonly browserApprovalStore?: BrowserApprovalStore; /** * Host-owned lifecycle for paused executions. The MCP server reports pause * boundaries; the host decides whether that means a keepAlive lease, browser * wait, durable record, or no-op. */ readonly pausedExecutionHooks?: PausedExecutionHooks; /** * Host-provided approval lease duration. When present, paused payloads carry * an absolute deadline and hooks receive the same deadline. */ readonly pausedExecutionLeaseMs?: number; /** * Optional host-owned model resume fallback. Used by Cloudflare session * Durable Objects to route a resume miss to the session that owns the pause. */ readonly resumeFallback?: ( executionId: string, response: ResumeResponse, ) => Effect.Effect<ResumeFallbackOutcome | null, unknown>; /** * Loads the MCP-Apps shell HTML served as the `ui://executor/shell.html` * resource. Injected rather than imported: the shell carries React, Recharts * and Tailwind, and this package also runs on Workers. Hosts that can serve * it pass `loadMcpAppsShellHtml` from `@executor-js/mcp-apps-shell`; hosts * that leave it unset simply don't register the resource or the ui tools. */ readonly loadAppShellHtml?: () => Promise<string>; /** * Per-connection artifacts opt-out. Defaults to true. A client that connects * with `?artifacts=false` gets NO artifact surface at all: none of the five * artifact tools, no `ui://` shell resource, and no artifact entries in the * `skills` inventory — the same shape a host without `loadAppShellHtml` * serves. `execute`, `skills` and `resume` are untouched. */ readonly artifactsEnabled?: boolean; /** * Renders an artifact once, server-side, before it is saved — so a component * that throws on its first render is refused at create time with the real * error instead of saving cleanly and dying on the user's page. * * Injected for the same reason `loadAppShellHtml` is: it needs React, * react-dom/server and the whole component barrel, and this package must not * drag any of that into the graph of a host that only ever calls `execute`. * Hosts that can afford it pass `smokeRenderArtifact` from * `@executor-js/mcp-apps-shell`, which loads it behind a dynamic import. * * Unset means no smoke check: creates are validated statically and saved, as * they were before. That is also what happens when the check itself fails — * see the fail-open path in `createArtifact`. */ readonly smokeRenderArtifact?: (code: string) => Promise<ArtifactSmokeRenderResult>; /** * The scoped executor's artifact operations, so `create-artifact` can persist what * it renders and `list-artifacts` / `show-artifact` can read it back. Only * the three operations the MCP surface needs, so hosts don't have to hand the * whole `Executor` across this boundary. */ readonly artifacts?: McpArtifactsPort; /** * The caller's saved connections, for binding an artifact's integration roles * at create time. Structurally satisfied by `executor.connections`; hosts pass * the same scoped executor they pass `artifacts`. * * Absent means `create-artifact` cannot bind, so it refuses code that calls an * integration rather than saving an artifact that could never run. */ readonly connections?: McpConnectionsPort; /** * Builds the web-app deep link for a saved artifact. Clients that can't * render MCP Apps get this URL instead of an inline widget. Absent (stdio has * no origin at all) means `create-artifact` still persists and reports the id, but * has no URL to offer. */ readonly artifactUrl?: (artifactId: string) => string; /** * Notified when an agent-facing artifact tool completes a user-meaningful * operation: `create-artifact` (created, or updated when it overwrote an * existing id) and `show-artifact` (viewed). Internal artifact reads — * binding resolution inside `execute-action` — deliberately do not notify. * Best-effort observation: failures are swallowed and cannot affect the tool * result. Hosts recording product analytics supply it; core stays agnostic. */ readonly onArtifactUsage?: (action: "created" | "viewed" | "updated") => Effect.Effect<void>; /** * Whether the client this session belongs to can render MCP Apps, as * negotiated at a previous `initialize`. * * Capabilities normally arrive from the client at `initialize` and live only * in the server instance. A session whose host evicted and cold-restored it * (deploy, idle) is rebuilt mid-conversation with no `initialize` to replay, * so without this the rebuilt server assumes no apps support and silently * downgrades every artifact to a deep link. Hosts that persist the * negotiated value pass it back here; the next `initialize`, if one comes, * overwrites it. */ readonly restoredAppsEnabled?: boolean; /** * Called when `initialize` negotiates the client's MCP-Apps support, so the * host can persist it for {@link restoredAppsEnabled} on a later cold * restore. Best-effort: failures are swallowed and never affect the session. */ readonly onAppsEnabledChange?: (appsEnabled: boolean) => Effect.Effect<void>;}; /** * The narrow artifact surface the MCP tools need. Structurally satisfied by * `Executor["artifacts"]`, so hosts holding a scoped executor can pass * `executor.artifacts` directly. */export type McpArtifactsPort = { readonly list: () => Effect.Effect<readonly ArtifactSummary[], unknown>; readonly get: (id: string) => Effect.Effect<Artifact, unknown>; readonly save: (input: SaveArtifactInput) => Effect.Effect<Artifact, unknown>;}; /** * The connection surface binding needs: list what this caller can reach. The * scoped executor has already narrowed it, so an inferred binding can never * name a connection the caller couldn't call themselves. */export type McpConnectionsPort = { readonly list: () => Effect.Effect<readonly BindableConnection[], unknown>;}; export type ExecutorMcpServerConfig<E extends Cause.YieldableError = Cause.YieldableError> = | (ExecutionEngineConfig<E> & SharedMcpServerConfig) | ({ readonly engine: ExecutionEngine<E> } & SharedMcpServerConfig) | (ExecutionEngineConfig<E> & SharedMcpServerConfig & { readonly stateless: true }) | ({ readonly engine: ExecutionEngine<E>; readonly stateless: true } & SharedMcpServerConfig); export type BrowserApprovalStore = { readonly takeResponse: (executionId: string) => Effect.Effect<ResumeResponse | null>; readonly waitForResponse?: (executionId: string) => Effect.Effect<ResumeResponse | null>;}; export const PAUSED_APPROVAL_TIMEOUT_MS = 4 * 60 * 1000;const BROWSER_APPROVAL_WAIT_TIMEOUT_MS = PAUSED_APPROVAL_TIMEOUT_MS + 1000; export type PausedExecutionHooks = { readonly onExecutionPaused?: ( executionId: string, deadline: PausedExecutionDeadline | undefined, ) => Effect.Effect<void>; readonly onResumeStarted?: (executionId: string) => Effect.Effect<void>; readonly onResumeSettled?: (executionId: string) => Effect.Effect<void>;}; export type ResumeUnavailableStatus = | "execution_not_found" | "execution_expired" | "execution_forbidden" | "execution_already_settled"; export type ResumeFallbackOutcome = | { readonly status: "result"; readonly result: McpToolResult; } | { readonly status: Exclude<ResumeUnavailableStatus, "execution_not_found">; readonly ttlMs?: number; } | { readonly status: "execution_not_found"; }; // ---------------------------------------------------------------------------// Elicitation bridge// --------------------------------------------------------------------------- const getElicitationSupport = (server: McpServer): { form: boolean; url: boolean } => { const capabilities = server.server.getClientCapabilities(); if (capabilities === undefined || !capabilities.elicitation) return { form: false, url: false }; const elicitation = capabilities.elicitation as Record<string, unknown>; return { form: Boolean(elicitation.form), url: Boolean(elicitation.url) };}; const readDebugDefault = (): boolean => { if (typeof process === "undefined" || !process.env) return false; const value = process.env.EXECUTOR_MCP_DEBUG; return value === "1" || value === "true";}; const capabilitySnapshot = (server: McpServer) => ({ clientCapabilities: server.server.getClientCapabilities() ?? null, elicitationSupport: getElicitationSupport(server),}); class McpNativeElicitationTransportError extends Data.TaggedError( "McpNativeElicitationTransportError",)<{ readonly cause: unknown;}> {} type ElicitInputParams = | { mode?: "form"; message: string; requestedSchema: { readonly [key: string]: unknown }; } | { mode: "url"; message: string; url: string; elicitationId: string }; const elicitationRequestTag = (request: ElicitationRequest): ElicitationRequest["_tag"] => Match.value(request).pipe( Match.tag("UrlElicitation", () => "UrlElicitation" as const), Match.tag("FormElicitation", () => "FormElicitation" as const), Match.exhaustive, ); const requestedSchemaIsNonEmpty = (request: ElicitationRequest): boolean => Match.value(request).pipe( Match.tag("FormElicitation", (req) => Object.keys(req.requestedSchema).length > 0), Match.tag("UrlElicitation", () => false), Match.exhaustive, ); const elicitationRequestUrl = (request: ElicitationRequest): string | undefined => Match.value(request).pipe( Match.tag("UrlElicitation", (req): string | undefined => req.url), Match.tag("FormElicitation", (): string | undefined => undefined), Match.exhaustive, ); const pausedInteractionKind = (request: ElicitationRequest): ElicitationRequest["_tag"] => elicitationRequestTag(request); const elicitationRequestToParams: (request: ElicitationRequest) => ElicitInputParams = Match.type<ElicitationRequest>().pipe( Match.tag("UrlElicitation", (req) => ({ mode: "url" as const, message: req.message, url: req.url, elicitationId: req.elicitationId, })), Match.tag("FormElicitation", (req) => ({ message: req.message, // The MCP SDK validates requestedSchema as a JSON Schema with // `type: "object"` and `properties`. For approval-only elicitations // where no fields are needed, provide a minimal valid schema. requestedSchema: Object.keys(req.requestedSchema).length === 0 ? { type: "object" as const, properties: {} } : req.requestedSchema, })), Match.exhaustive, ); const makeMcpElicitationHandler = ( server: McpServer, relatedRequestId: string | number, debugLog?: (event: string, data: Record<string, unknown>) => void, ): ElicitationHandler => (ctx: ElicitationContext): Effect.Effect<typeof ElicitationResponse.Type> => { const { url: supportsUrl } = getElicitationSupport(server); // If client doesn't support url mode, fall back to a form asking the user // to visit the URL manually and confirm when done. const params = Match.value(ctx.request).pipe( Match.tag( "UrlElicitation", (req): ElicitInputParams => !supportsUrl ? { message: `${req.message}\n\nPlease visit this URL:\n${req.url}\n\nClick accept once you have completed the flow.`, requestedSchema: { type: "object" as const, properties: {} }, } : elicitationRequestToParams(req), ), Match.tag("FormElicitation", (req): ElicitInputParams => elicitationRequestToParams(req)), Match.exhaustive, ); return Effect.promise(async (): Promise<typeof ElicitationResponse.Type> => { const requestTag = elicitationRequestTag(ctx.request); debugLog?.("elicitation.request", { requestTag, supportsUrl, message: ctx.request.message, hasRequestedSchema: requestedSchemaIsNonEmpty(ctx.request), url: elicitationRequestUrl(ctx.request), clientCapabilities: server.server.getClientCapabilities() ?? null, }); const response = await server.server.elicitInput( params as Parameters<typeof server.server.elicitInput>[0], { relatedRequestId }, ); debugLog?.("elicitation.response", { requestTag, action: response.action, hasContent: typeof response.content === "object" && response.content !== null && Object.keys(response.content).length > 0, }); return { action: response.action as typeof ElicitationResponse.Type.action, content: response.content, }; }).pipe( Effect.tapDefect((defect) => Effect.sync(() => { debugLog?.("elicitation.error", { requestTag: elicitationRequestTag(ctx.request), error: formatBoundaryError(defect), clientCapabilities: server.server.getClientCapabilities() ?? null, }); }), ), Effect.catchDefect((cause) => // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: ElicitationHandler has no error channel, so retain a classified defect for the MCP result boundary. Effect.die(new McpNativeElicitationTransportError({ cause })), ), ); }; const formatBoundaryError = (err: unknown): { name?: string; message: string; stack?: string } => { // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: SDK Promise rejection supplies unknown JS errors for logging only if (err instanceof Error) return { name: err.name, message: err.message, stack: err.stack }; // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: fallback log formatting for unknown SDK Promise rejection values return { message: String(err) };}; // ---------------------------------------------------------------------------// MCP result formatting// --------------------------------------------------------------------------- export type McpToolResult = { content: ContentBlock[]; structuredContent?: Record<string, unknown>; isError?: boolean;}; type FormattedExecuteInput = Parameters<typeof formatExecuteResult>[0];type ExecuteOutputItem = NonNullable<FormattedExecuteInput["output"]>[number]; const TEXT_FILE_CONTENT_MAX_CHARS = 64_000; const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value); const toolFileName = (file: ToolFileValue): string => file.name ?? "tool-output"; const fileResourceUri = (file: ToolFileValue): string => `executor-file:///${encodeURIComponent(toolFileName(file))}`; const normalizedMimeType = (file: ToolFileValue): string => file.mimeType.split(";")[0]?.trim().toLowerCase() ?? ""; const toolFileKind = (file: ToolFileValue): "image" | "audio" | "text" | "resource" => { const mimeType = normalizedMimeType(file); if (mimeType.startsWith("image/")) return "image"; if (mimeType.startsWith("audio/")) return "audio"; if ( mimeType.startsWith("text/") || mimeType === "application/json" || mimeType.endsWith("+json") || mimeType === "application/xml" || mimeType.endsWith("+xml") || mimeType === "application/javascript" || mimeType === "application/x-javascript" || mimeType === "application/yaml" || mimeType === "application/x-yaml" ) { return "text";Lines 1–500 of 2259. Load more
import { Data, Duration, Effect, Match, Option, Predicate, Result, Schema } from "effect"; import * as Cause from "effect/Cause"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { ContentBlockSchema, type ClientCapabilities, type ContentBlock, } from "@modelcontextprotocol/sdk/types.js"; import { getUiCapability, registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; import type { jsonSchemaValidator, JsonSchemaType, JsonSchemaValidator, } from "@modelcontextprotocol/sdk/validation/types.js"; import { Validator } from "@cfworker/json-schema"; import * as z from "zod/v4"; import { isToolFile, sanitizeArtifactPreviewMarkup } from "@executor-js/sdk"; import type { Artifact, ArtifactBinding, ArtifactSummary, ElicitationResponse, ElicitationHandler, ElicitationContext, ElicitationRequest, SaveArtifactInput, ToolFileValue, } from "@executor-js/sdk"; import type * as Tracer from "effect/Tracer"; import { createExecutionEngine, formatExecuteResult, formatPausedExecution, formatTtlDuration, findSkill, renderSkillsIndex, skillCatalogFor, EXECUTE_SKILL, INTEGRATION_INVENTORY_HEADER, type Skill, type ExecutionEngine, type ExecutionEngineConfig, type ResumeResponse, type ExecutionResult, type PausedExecution, type PausedExecutionDeadline, } from "@executor-js/execution"; import { MCP_APPS_SHELL_RESOURCE_URI, applyArtifactEdits, smokeRenderRejection, validateArtifactCode, type ArtifactEdit, type ArtifactSmokeRenderResult, } from "./create-artifact"; import { TOOL_CALL_CONTRACT_MESSAGE } from "./tool-call-code"; import { resolveArtifactAction } from "./artifact-action"; import { extractArtifactRoles, resolveArtifactBindings, type BindableConnection, } from "./artifact-bindings"; // --------------------------------------------------------------------------- // Workers-compatible JSON Schema validator (replaces Ajv which uses new Function()) // --------------------------------------------------------------------------- class CfWorkerJsonSchemaValidator implements jsonSchemaValidator { getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> { const validator = new Validator(schema as Record<string, unknown>, "2020-12", false); return (input: unknown) => { const result = validator.validate(input); if (result.valid) { return { valid: true, data: input as T, errorMessage: undefined }; } const errorMessage = result.errors.map((e) => `${e.instanceLocation}: ${e.error}`).join("; "); return { valid: false, data: undefined, errorMessage }; }; } } // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- type SharedMcpServerConfig = { /** * Pre-built `execute` tool description. When provided, the factory skips * its internal `engine.getDescription` yield. Useful when the caller * wants to compute the description inside its own Effect tracer context * so sub-spans (`executor.integrations.list`, `executor.tools.list`) nest as * children of the caller's root span. */ readonly description?: string; /** * Parent span override for engine calls. The factory captures the * caller's context at construction time, but `Effect.runPromiseWith` * starts a fresh fiber per SDK callback — so the `currentSpan` * FiberRef resets to root unless explicitly anchored. * * Accepts either a fixed span (per-request McpServer instances) or a * getter (session-scoped instances that need to anchor each callback * under whichever request triggered it; see the Cloud DO). */ readonly parentSpan?: Tracer.AnySpan | (() => Tracer.AnySpan | undefined); /** * Enable verbose MCP capability / elicitation debug logging. */ readonly debug?: boolean; /** * Controls how elicitation is handled for this MCP connection. The default * is model-managed resume, where paused executions expose interaction * metadata and the model can call `resume` with the user's response. */ readonly elicitationMode?: | { readonly mode: "browser"; readonly approvalUrl: (executionId: string) => string; } | { readonly mode: "model"; } | { readonly mode: "native"; }; readonly browserApprovalStore?: BrowserApprovalStore; /** * Host-owned lifecycle for paused executions. The MCP server reports pause * boundaries; the host decides whether that means a keepAlive lease, browser * wait, durable record, or no-op. */ readonly pausedExecutionHooks?: PausedExecutionHooks; /** * Host-provided approval lease duration. When present, paused payloads carry * an absolute deadline and hooks receive the same deadline. */ readonly pausedExecutionLeaseMs?: number; /** * Optional host-owned model resume fallback. Used by Cloudflare session * Durable Objects to route a resume miss to the session that owns the pause. */ readonly resumeFallback?: ( executionId: string, response: ResumeResponse, ) => Effect.Effect<ResumeFallbackOutcome | null, unknown>; /** * Loads the MCP-Apps shell HTML served as the `ui://executor/shell.html` * resource. Injected rather than imported: the shell carries React, Recharts * and Tailwind, and this package also runs on Workers. Hosts that can serve * it pass `loadMcpAppsShellHtml` from `@executor-js/mcp-apps-shell`; hosts * that leave it unset simply don't register the resource or the ui tools. */ readonly loadAppShellHtml?: () => Promise<string>; /** * Per-connection artifacts opt-out. Defaults to true. A client that connects * with `?artifacts=false` gets NO artifact surface at all: none of the five * artifact tools, no `ui://` shell resource, and no artifact entries in the * `skills` inventory — the same shape a host without `loadAppShellHtml` * serves. `execute`, `skills` and `resume` are untouched. */ readonly artifactsEnabled?: boolean; /** * Renders an artifact once, server-side, before it is saved — so a component * that throws on its first render is refused at create time with the real * error instead of saving cleanly and dying on the user's page. * * Injected for the same reason `loadAppShellHtml` is: it needs React, * react-dom/server and the whole component barrel, and this package must not * drag any of that into the graph of a host that only ever calls `execute`. * Hosts that can afford it pass `smokeRenderArtifact` from * `@executor-js/mcp-apps-shell`, which loads it behind a dynamic import. * * Unset means no smoke check: creates are validated statically and saved, as * they were before. That is also what happens when the check itself fails — * see the fail-open path in `createArtifact`. */ readonly smokeRenderArtifact?: (code: string) => Promise<ArtifactSmokeRenderResult>; /** * The scoped executor's artifact operations, so `create-artifact` can persist what * it renders and `list-artifacts` / `show-artifact` can read it back. Only * the three operations the MCP surface needs, so hosts don't have to hand the * whole `Executor` across this boundary. */ readonly artifacts?: McpArtifactsPort; /** * The caller's saved connections, for binding an artifact's integration roles * at create time. Structurally satisfied by `executor.connections`; hosts pass * the same scoped executor they pass `artifacts`. * * Absent means `create-artifact` cannot bind, so it refuses code that calls an * integration rather than saving an artifact that could never run. */ readonly connections?: McpConnectionsPort; /** * Builds the web-app deep link for a saved artifact. Clients that can't * render MCP Apps get this URL instead of an inline widget. Absent (stdio has * no origin at all) means `create-artifact` still persists and reports the id, but * has no URL to offer. */ readonly artifactUrl?: (artifactId: string) => string; /** * Notified when an agent-facing artifact tool completes a user-meaningful * operation: `create-artifact` (created, or updated when it overwrote an * existing id) and `show-artifact` (viewed). Internal artifact reads — * binding resolution inside `execute-action` — deliberately do not notify. * Best-effort observation: failures are swallowed and cannot affect the tool * result. Hosts recording product analytics supply it; core stays agnostic. */ readonly onArtifactUsage?: (action: "created" | "viewed" | "updated") => Effect.Effect<void>; /** * Whether the client this session belongs to can render MCP Apps, as * negotiated at a previous `initialize`. * * Capabilities normally arrive from the client at `initialize` and live only * in the server instance. A session whose host evicted and cold-restored it * (deploy, idle) is rebuilt mid-conversation with no `initialize` to replay, * so without this the rebuilt server assumes no apps support and silently * downgrades every artifact to a deep link. Hosts that persist the * negotiated value pass it back here; the next `initialize`, if one comes, * overwrites it. */ readonly restoredAppsEnabled?: boolean; /** * Called when `initialize` negotiates the client's MCP-Apps support, so the * host can persist it for {@link restoredAppsEnabled} on a later cold * restore. Best-effort: failures are swallowed and never affect the session. */ readonly onAppsEnabledChange?: (appsEnabled: boolean) => Effect.Effect<void>; }; /** * The narrow artifact surface the MCP tools need. Structurally satisfied by * `Executor["artifacts"]`, so hosts holding a scoped executor can pass * `executor.artifacts` directly. */ export type McpArtifactsPort = { readonly list: () => Effect.Effect<readonly ArtifactSummary[], unknown>; readonly get: (id: string) => Effect.Effect<Artifact, unknown>; readonly save: (input: SaveArtifactInput) => Effect.Effect<Artifact, unknown>; }; /** * The connection surface binding needs: list what this caller can reach. The * scoped executor has already narrowed it, so an inferred binding can never * name a connection the caller couldn't call themselves. */ export type McpConnectionsPort = { readonly list: () => Effect.Effect<readonly BindableConnection[], unknown>; }; export type ExecutorMcpServerConfig<E extends Cause.YieldableError = Cause.YieldableError> = | (ExecutionEngineConfig<E> & SharedMcpServerConfig) | ({ readonly engine: ExecutionEngine<E> } & SharedMcpServerConfig) | (ExecutionEngineConfig<E> & SharedMcpServerConfig & { readonly stateless: true }) | ({ readonly engine: ExecutionEngine<E>; readonly stateless: true } & SharedMcpServerConfig); export type BrowserApprovalStore = { readonly takeResponse: (executionId: string) => Effect.Effect<ResumeResponse | null>; readonly waitForResponse?: (executionId: string) => Effect.Effect<ResumeResponse | null>; }; export const PAUSED_APPROVAL_TIMEOUT_MS = 4 * 60 * 1000; const BROWSER_APPROVAL_WAIT_TIMEOUT_MS = PAUSED_APPROVAL_TIMEOUT_MS + 1000; export type PausedExecutionHooks = { readonly onExecutionPaused?: ( executionId: string, deadline: PausedExecutionDeadline | undefined, ) => Effect.Effect<void>; readonly onResumeStarted?: (executionId: string) => Effect.Effect<void>; readonly onResumeSettled?: (executionId: string) => Effect.Effect<void>; }; export type ResumeUnavailableStatus = | "execution_not_found" | "execution_expired" | "execution_forbidden" | "execution_already_settled"; export type ResumeFallbackOutcome = | { readonly status: "result"; readonly result: McpToolResult; } | { readonly status: Exclude<ResumeUnavailableStatus, "execution_not_found">; readonly ttlMs?: number; } | { readonly status: "execution_not_found"; }; // --------------------------------------------------------------------------- // Elicitation bridge // --------------------------------------------------------------------------- const getElicitationSupport = (server: McpServer): { form: boolean; url: boolean } => { const capabilities = server.server.getClientCapabilities(); if (capabilities === undefined || !capabilities.elicitation) return { form: false, url: false }; const elicitation = capabilities.elicitation as Record<string, unknown>; return { form: Boolean(elicitation.form), url: Boolean(elicitation.url) }; }; const readDebugDefault = (): boolean => { if (typeof process === "undefined" || !process.env) return false; const value = process.env.EXECUTOR_MCP_DEBUG; return value === "1" || value === "true"; }; const capabilitySnapshot = (server: McpServer) => ({ clientCapabilities: server.server.getClientCapabilities() ?? null, elicitationSupport: getElicitationSupport(server), }); class McpNativeElicitationTransportError extends Data.TaggedError( "McpNativeElicitationTransportError", )<{ readonly cause: unknown; }> {} type ElicitInputParams = | { mode?: "form"; message: string; requestedSchema: { readonly [key: string]: unknown }; } | { mode: "url"; message: string; url: string; elicitationId: string }; const elicitationRequestTag = (request: ElicitationRequest): ElicitationRequest["_tag"] => Match.value(request).pipe( Match.tag("UrlElicitation", () => "UrlElicitation" as const), Match.tag("FormElicitation", () => "FormElicitation" as const), Match.exhaustive, ); const requestedSchemaIsNonEmpty = (request: ElicitationRequest): boolean => Match.value(request).pipe( Match.tag("FormElicitation", (req) => Object.keys(req.requestedSchema).length > 0), Match.tag("UrlElicitation", () => false), Match.exhaustive, ); const elicitationRequestUrl = (request: ElicitationRequest): string | undefined => Match.value(request).pipe( Match.tag("UrlElicitation", (req): string | undefined => req.url), Match.tag("FormElicitation", (): string | undefined => undefined), Match.exhaustive, ); const pausedInteractionKind = (request: ElicitationRequest): ElicitationRequest["_tag"] => elicitationRequestTag(request); const elicitationRequestToParams: (request: ElicitationRequest) => ElicitInputParams = Match.type<ElicitationRequest>().pipe( Match.tag("UrlElicitation", (req) => ({ mode: "url" as const, message: req.message, url: req.url, elicitationId: req.elicitationId, })), Match.tag("FormElicitation", (req) => ({ message: req.message, // The MCP SDK validates requestedSchema as a JSON Schema with // `type: "object"` and `properties`. For approval-only elicitations // where no fields are needed, provide a minimal valid schema. requestedSchema: Object.keys(req.requestedSchema).length === 0 ? { type: "object" as const, properties: {} } : req.requestedSchema, })), Match.exhaustive, ); const makeMcpElicitationHandler = ( server: McpServer, relatedRequestId: string | number, debugLog?: (event: string, data: Record<string, unknown>) => void, ): ElicitationHandler => (ctx: ElicitationContext): Effect.Effect<typeof ElicitationResponse.Type> => { const { url: supportsUrl } = getElicitationSupport(server); // If client doesn't support url mode, fall back to a form asking the user // to visit the URL manually and confirm when done. const params = Match.value(ctx.request).pipe( Match.tag( "UrlElicitation", (req): ElicitInputParams => !supportsUrl ? { message: `${req.message}\n\nPlease visit this URL:\n${req.url}\n\nClick accept once you have completed the flow.`, requestedSchema: { type: "object" as const, properties: {} }, } : elicitationRequestToParams(req), ), Match.tag("FormElicitation", (req): ElicitInputParams => elicitationRequestToParams(req)), Match.exhaustive, ); return Effect.promise(async (): Promise<typeof ElicitationResponse.Type> => { const requestTag = elicitationRequestTag(ctx.request); debugLog?.("elicitation.request", { requestTag, supportsUrl, message: ctx.request.message, hasRequestedSchema: requestedSchemaIsNonEmpty(ctx.request), url: elicitationRequestUrl(ctx.request), clientCapabilities: server.server.getClientCapabilities() ?? null, }); const response = await server.server.elicitInput( params as Parameters<typeof server.server.elicitInput>[0], { relatedRequestId }, ); debugLog?.("elicitation.response", { requestTag, action: response.action, hasContent: typeof response.content === "object" && response.content !== null && Object.keys(response.content).length > 0, }); return { action: response.action as typeof ElicitationResponse.Type.action, content: response.content, }; }).pipe( Effect.tapDefect((defect) => Effect.sync(() => { debugLog?.("elicitation.error", { requestTag: elicitationRequestTag(ctx.request), error: formatBoundaryError(defect), clientCapabilities: server.server.getClientCapabilities() ?? null, }); }), ), Effect.catchDefect((cause) => // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: ElicitationHandler has no error channel, so retain a classified defect for the MCP result boundary. Effect.die(new McpNativeElicitationTransportError({ cause })), ), ); }; const formatBoundaryError = (err: unknown): { name?: string; message: string; stack?: string } => { // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: SDK Promise rejection supplies unknown JS errors for logging only if (err instanceof Error) return { name: err.name, message: err.message, stack: err.stack }; // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: fallback log formatting for unknown SDK Promise rejection values return { message: String(err) }; }; // --------------------------------------------------------------------------- // MCP result formatting // --------------------------------------------------------------------------- export type McpToolResult = { content: ContentBlock[]; structuredContent?: Record<string, unknown>; isError?: boolean; }; type FormattedExecuteInput = Parameters<typeof formatExecuteResult>[0]; type ExecuteOutputItem = NonNullable<FormattedExecuteInput["output"]>[number]; const TEXT_FILE_CONTENT_MAX_CHARS = 64_000; const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value); const toolFileName = (file: ToolFileValue): string => file.name ?? "tool-output"; const fileResourceUri = (file: ToolFileValue): string => `executor-file:///${encodeURIComponent(toolFileName(file))}`; const normalizedMimeType = (file: ToolFileValue): string => file.mimeType.split(";")[0]?.trim().toLowerCase() ?? ""; const toolFileKind = (file: ToolFileValue): "image" | "audio" | "text" | "resource" => { const mimeType = normalizedMimeType(file); if (mimeType.startsWith("image/")) return "image"; if (mimeType.startsWith("audio/")) return "audio"; if ( mimeType.startsWith("text/") || mimeType === "application/json" || mimeType.endsWith("+json") || mimeType === "application/xml" || mimeType.endsWith("+xml") || mimeType === "application/javascript" || mimeType === "application/x-javascript" || mimeType === "application/yaml" || mimeType === "application/x-yaml" ) { return "text";