2216 lines · 92.0 KB · TypeScript
RawDownload
import { Duration, Effect, Match, Option, Predicate, Result, Schema } from "effect";import * as Cause from "effect/Cause";import { ContentBlockSchema, ToolAnnotationsSchema } from "@modelcontextprotocol/core";import type { InputRequiredResult } from "@modelcontextprotocol/server";import * as z from "zod/v4"; import { isToolFile, sanitizeArtifactPreviewMarkup } from "@executor-js/sdk";import type {  Artifact,  ArtifactBinding,  ArtifactSummary,  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";import { RESOURCE_MIME_TYPE } from "./mcp-apps"; // ---------------------------------------------------------------------------// Shared 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 client identity self-reported at a previous `initialize` (or in a   * modern request's `_meta`), restored for the same reason as   * {@link restoredAppsEnabled}. Feeds only the `mcp.client.*` span   * attributes on execution spans, never behavior or security decisions.   */  readonly restoredClientInfo?: McpClientInfo;  /**   * Called when `initialize` reports the client identity, so the host can   * persist it for {@link restoredClientInfo} on a later cold restore.   * Best-effort: failures are swallowed and never affect the session.   */  readonly onClientInfoChange?: (clientInfo: McpClientInfo) => Effect.Effect<void>;}; /** * Client software identity as self-reported over MCP (`clientInfo` at * `initialize`, `_meta` on modern requests). Display and telemetry vocabulary * only: the spec forbids relying on it for behavior or security. */export type McpClientInfo = {  readonly name: string;  readonly version?: string;  readonly title?: string;}; /** * 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 ExecutorMcpToolConfig<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";    }; /** Request identity normalized from an MCP SDK callback context. */export type McpRequestJoinKeys = {  readonly requestId: string | number;  readonly sessionId?: string | undefined;}; /** 2026-07-28 input-required result returned by the MCP assembly. */export type McpInputRequiredResult = InputRequiredResult; /** Result shape produced by Executor MCP handlers. */export type McpHandlerResult = McpToolResult | McpInputRequiredResult; /** Enable/disable controls returned by the MCP SDK for registered tools. */export type RegisteredMcpTool = {  readonly enable: () => void;  readonly disable: () => void;}; type McpToolConfig<Shape extends z.ZodRawShape> = {  readonly title?: string;  readonly description?: string;  readonly inputSchema: Shape;  readonly annotations?: z.infer<typeof ToolAnnotationsSchema>;  readonly _meta?: Record<string, unknown>;}; type MutableMcpToolShape<Shape extends z.ZodRawShape> = {  -readonly [Key in keyof Shape]: Shape[Key];}; type McpAppResourceConfig = {  readonly title?: string;  readonly description?: string;  readonly mimeType?: string;  readonly _meta?: Record<string, unknown>;}; type McpResourceResult = {  readonly contents: readonly (    | {        readonly uri: string;        readonly mimeType?: string;        readonly text: string;        readonly _meta?: Record<string, unknown>;      }    | {        readonly uri: string;        readonly mimeType?: string;        readonly blob: string;        readonly _meta?: Record<string, unknown>;      }  )[];}; /** Services supplied to an assembly's SDK-specific native elicitation bridge. */export type NativeExecutionServices<  E extends Cause.YieldableError,  RequestContext extends McpRequestJoinKeys,> = {  readonly engine: ExecutionEngine<E>;  readonly code: string;  readonly requestContext: RequestContext;  readonly source: "execute" | "execute_action";  readonly debugLog: (event: string, data: Record<string, unknown>) => void;  readonly complete: (result: Parameters<typeof formatExecuteResult>[0]) => McpToolResult;  readonly resume: (    executionId: string,    response: ResumeResponse,  ) => Effect.Effect<ExecutionResult | null, E>;  readonly executionPaused: (execution: PausedExecution) => Effect.Effect<void>;}; /** * Minimal SDK-specific surface needed by the shared Executor tool assembly. * Both SDK versions are adapted to this interface at their composition roots. */export type ExecutorMcpAssembly<Server, RequestContext extends McpRequestJoinKeys> = {  readonly server: Server;  readonly initialAppsEnabled: boolean;  readonly getClientCapabilities: () => unknown | null;  /** The live `initialize`-reported client identity, when the SDK has one. */  readonly getClientInfo: () => McpClientInfo | null;  readonly getElicitationSupport: () => { readonly form: boolean; readonly url: boolean };  readonly getUiCapability: () => { readonly mimeTypes?: readonly string[] } | undefined;  readonly onInitialized: (callback: () => void) => void;  readonly registerTool: <Shape extends z.ZodRawShape>(    name: string,    config: McpToolConfig<Shape>,    callback: (      args: z.output<z.ZodObject<MutableMcpToolShape<Shape>>>,      requestContext: RequestContext,    ) => Promise<McpHandlerResult>,  ) => RegisteredMcpTool;  readonly registerAppTool: <Shape extends z.ZodRawShape>(    name: string,    config: McpToolConfig<Shape> & { readonly _meta: Record<string, unknown> },    callback: (      args: z.output<z.ZodObject<MutableMcpToolShape<Shape>>>,      requestContext: RequestContext,    ) => Promise<McpHandlerResult>,  ) => RegisteredMcpTool;  readonly registerAppResource: (    name: string,    uri: string,    config: McpAppResourceConfig,    callback: () => McpResourceResult | Promise<McpResourceResult>,  ) => void;  readonly executeNative: <E extends Cause.YieldableError>(    services: NativeExecutionServices<E, RequestContext>,  ) => Effect.Effect<McpHandlerResult, E>;}; // ---------------------------------------------------------------------------// Shared elicitation helpers// --------------------------------------------------------------------------- const readDebugDefault = (): boolean => {  if (typeof process === "undefined" || !process.env) return false;  const value = process.env.EXECUTOR_MCP_DEBUG;  return value === "1" || value === "true";}; export 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 pausedInteractionKind = (request: ElicitationRequest): ElicitationRequest["_tag"] =>  elicitationRequestTag(request); // ---------------------------------------------------------------------------// MCP result formatting// --------------------------------------------------------------------------- export type McpToolResult = {  content: ContentBlock[];  structuredContent?: Record<string, unknown>;  isError?: boolean;}; type ContentBlock = z.infer<typeof ContentBlockSchema>; 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";  }  return "resource";}; const bytesFromBase64 = (base64: string): Uint8Array => {  const binary = atob(base64);  const bytes = new Uint8Array(binary.length);  for (let index = 0; index < binary.length; index += 1) {    bytes[index] = binary.charCodeAt(index);  }  return bytes;}; const decodeTextFile = (file: ToolFileValue): string => {  const text = new TextDecoder("utf-8", { fatal: false }).decode(bytesFromBase64(file.data));  if (text.length <= TEXT_FILE_CONTENT_MAX_CHARS) return text;  return `${text.slice(0, TEXT_FILE_CONTENT_MAX_CHARS)}\n\n[truncated ${    text.length - TEXT_FILE_CONTENT_MAX_CHARS  } characters]`;}; const toolFileContent = (file: ToolFileValue): ContentBlock[] => {  const kind = toolFileKind(file);  if (kind === "image") {    return [{ type: "image", data: file.data, mimeType: file.mimeType }];  }  if (kind === "audio") {    return [{ type: "audio", data: file.data, mimeType: file.mimeType }];  }  if (kind === "text") {    return [{ type: "text", text: decodeTextFile(file) }];  }  return [    {      type: "resource",      resource: {        uri: fileResourceUri(file),        mimeType: file.mimeType,

Lines 1–500 of 2216.