2921 lines · 121.1 KB · TypeScript
RawDownload
import { reattachDefs } from "@executor-js/sdk/host-internal";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 {  CurrentOrgWriteAccess,  ToolAddress,  IntegrationSlug,  ConnectionName,  parseToolAddress,  isToolFile,  isToolResult,  makeOrgWriteAccessState,  sanitizeArtifactPreviewMarkup,  type OrgWriteAccess,} from "@executor-js/sdk";import type {  Artifact,  Connection,  Integration,  ArtifactBinding,  ArtifactSummary,  ElicitationResponse,  ElicitationResponseMeta,  ElicitationHandler,  ElicitationContext,  ElicitationRequest,  SaveArtifactInput,  ToolFileValue,  Executor,  ToolSchemaView,} from "@executor-js/sdk";import type * as Tracer from "effect/Tracer";import {  createExecutionEngine,  searchTools,  formatExecuteResult,  formatPausedExecution,  formatTtlDuration,  findSkill,  parseIntegrationInventory,  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";import { MCP_ORG_WRITE_ACCESS_HEADER } from "./seams";import {  passthroughCallCode,  passthroughInstructions,  SEARCH_INVOKE_SKILL,} from "./passthrough-tools";import type { McpToolMode } from "./browser-approval"; // ---------------------------------------------------------------------------// 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;  /**   * Per-connection opt-IN for the per-integration search tools. Defaults to   * false. A client that connects with `?search_tools=true` gets one   * `search_<integration>` tool per connected integration (the same inventory   * the `execute` description lists). The tools exist to carry the namespaces   * into the model's context as tool names; each call routes through the same   * execution flow as `tools.search({ namespace })` inside `execute`, so the   * results match what code-side search returns. Codemode only: passthrough   * ignores it (it has its own search tool, and codemode search   * results point at an `execute` tool passthrough does not serve).   */  readonly searchToolsEnabled?: boolean;  /**   * The tool surface this connection serves. `codemode` (the default) is the   * `execute` tool plus `skills`/`resume` and the artifact surface.   * `passthrough` (`?mode=passthrough`) serves search and invoke for the   * visible catalog, with no execute, skills, resume, or artifact tools.   * Invoke is marked destructive for client approval. Requires `tools`.   */  readonly mode?: McpToolMode;  /**   * The scoped executor's tool catalog, for passthrough mode. Structurally   * satisfied by `executor.tools`. Hosts that never serve passthrough may   * leave it unset; a passthrough session without it fails at build time   * rather than silently serving an empty surface.   */  readonly tools?: McpToolsPort;  /**   * 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 the search/invoke account inventory   * and binding artifact 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;  /** Scoped integration metadata for the search/invoke account inventory. */  readonly integrations?: McpIntegrationsPort;  /**   * 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 and discovery need: 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 &      Pick<Connection, "identityLabel" | "description" | "lastHealth">)[],    unknown  >;}; /** Catalog metadata visible to this caller; no tool schemas or credentials. */export type McpIntegrationsPort = {  readonly list: () => Effect.Effect<    readonly Pick<Integration, "slug" | "name" | "description">[],    unknown  >;}; /** The same list and schema APIs used by codemode discovery. */export type McpToolsPort = Pick<Executor["tools"], "list" | "schema">; /** A passthrough session was requested but the host gave the factory no *  catalog to serve. A configuration defect, not a runtime condition. */export class McpPassthroughUnavailableError extends Data.TaggedError(  "McpPassthroughUnavailableError",)<{ readonly reason: string }> {} 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<BrowserApprovalDecision | null>;  readonly waitForResponse?: (executionId: string) => Effect.Effect<BrowserApprovalDecision | null>;}; /** Browser response paired with authorization derived from the deciding user. */export interface BrowserApprovalDecision {  readonly response: ResumeResponse;  readonly orgWriteAccess: OrgWriteAccess;} 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); // The request's terms travel as `_meta`, the way they arrived: a native// client that renders "Allow Computer Use to use Finder?" needs to see that// accepting can be remembered, and which scopes it may answer with.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,      ...(req.meta === undefined ? {} : { _meta: req.meta }),    })),    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,      ...(req.meta === undefined ? {} : { _meta: req.meta }),    })),    Match.exhaustive,  ); /** The client's answer to the terms: the `persist` scope it chose, read from *  the result's `_meta` — and nothing else, so an answer states no more than *  `ElicitationResponseMeta` names. */const answeredTerms = (meta: unknown): ElicitationResponseMeta | undefined => {  const persist = isRecord(meta) ? meta["persist"] : undefined;  return typeof persist === "string" ? { persist } : undefined;}; 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,    ); 

Lines 1–500 of 2921.