494 lines · 18.8 KB · TypeScript
RawDownload
import { Effect, Match, Option, Schema } from "effect";import * as Cause from "effect/Cause";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import type {  jsonSchemaValidator,  JsonSchemaType,  JsonSchemaValidator,} from "@modelcontextprotocol/sdk/validation/types.js";import { Validator } from "@cfworker/json-schema";import { z } from "zod/v4"; import type {  ElicitationResponse,  ElicitationHandler,  ElicitationContext,  ElicitationRequest,} from "@executor-js/sdk";import type * as Tracer from "effect/Tracer";import {  createExecutionEngine,  formatExecuteResult,  formatPausedExecution,  type ExecutionEngine,  type ExecutionEngineConfig,} from "@executor-js/execution"; // ---------------------------------------------------------------------------// 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.sources.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;}; 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); // ---------------------------------------------------------------------------// 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 supportsManagedElicitation = (server: McpServer): boolean =>  getElicitationSupport(server).form; const capabilitySnapshot = (server: McpServer) => ({  clientCapabilities: server.server.getClientCapabilities() ?? null,  elicitationSupport: getElicitationSupport(server),  managedElicitation: supportsManagedElicitation(server),}); 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.orElse(() => false),  ); const elicitationRequestUrl = (request: ElicitationRequest): string | undefined =>  Match.value(request).pipe(    Match.tag("UrlElicitation", (req) => req.url),    Match.orElse(() => undefined),  ); 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,    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) =>        !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.orElse((req) => elicitationRequestToParams(req)),    );     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,      });       // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: MCP SDK elicitInput is a Promise API; failures become a cancel response      try {        const response = await server.server.elicitInput(          params as Parameters<typeof server.server.elicitInput>[0],        );         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,        };      } catch (err) {        const error = formatBoundaryError(err);        debugLog?.("elicitation.error", {          requestTag,          error,          clientCapabilities: server.server.getClientCapabilities() ?? null,        });        console.error(          "[executor] elicitInput failed - falling back to cancel.",          JSON.stringify({            error,            requestTag,            ...capabilitySnapshot(server),          }),        );        return { action: "cancel" as const } as ElicitationResponse;      }    });  }; 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// --------------------------------------------------------------------------- type McpToolResult = {  content: Array<{ type: "text"; text: string }>;  structuredContent?: Record<string, unknown>;  isError?: boolean;}; const toMcpResult = (formatted: ReturnType<typeof formatExecuteResult>): McpToolResult => ({  content: [{ type: "text", text: formatted.text }],  structuredContent: formatted.structured,  isError: formatted.isError || undefined,}); const toMcpPausedResult = (formatted: ReturnType<typeof formatPausedExecution>): McpToolResult => ({  content: [{ type: "text", text: formatted.text }],  structuredContent: formatted.structured,}); const formatFailureMessage = (value: unknown): string | null => {  if (typeof value === "object" && value !== null && "message" in value) {    const message = (value as { readonly message?: unknown }).message;    if (typeof message === "string" && message.length > 0) return message;  }  if (typeof value === "string" && value.length > 0) return value;  return null;}; const toMcpFailureResult = (cause: Cause.Cause<unknown>): McpToolResult => {  const failure = cause.reasons.find(Cause.isFailReason);  const text = failure    ? (formatFailureMessage(failure.error) ?? "Tool execution failed")    : "Tool execution failed";  return {    content: [{ type: "text", text: `Error: ${text}` }],    structuredContent: { status: "error", error: text },    isError: true,  };}; const JsonObjectFromString = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown));const decodeJsonObjectString = Schema.decodeUnknownOption(JsonObjectFromString); const parseJsonContent = (raw: string): Record<string, unknown> | undefined => {  if (raw === "{}") return undefined;  const parsed = decodeJsonObjectString(raw);  return Option.isSome(parsed) ? parsed.value : undefined;}; // ---------------------------------------------------------------------------// Server factory// --------------------------------------------------------------------------- export const createExecutorMcpServer = <E extends Cause.YieldableError>(  config: ExecutorMcpServerConfig<E>,): Effect.Effect<McpServer> =>  Effect.gen(function* () {    const engine = "engine" in config ? config.engine : createExecutionEngine(config);    const description =      config.description ??      (yield* engine.getDescription.pipe(Effect.withSpan("mcp.host.get_description")));     // Captured at construction time. SDK callbacks fire later (often    // deferred past the outer Effect's await), so we use the runtime to    // re-enter Effect-land at each callback edge.    const context = yield* Effect.context<never>();    const debugEnabled = config.debug ?? readDebugDefault();    const debugLog = (event: string, data: Record<string, unknown>) => {      if (!debugEnabled) return;      // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: debug logging must tolerate non-serializable SDK capability snapshots      try {        console.error(`[executor:mcp] ${event} ${JSON.stringify(data)}`);      } catch {        console.error(`[executor:mcp] ${event}`, data);      }    };     const resolveParentSpan = (): Tracer.AnySpan | undefined => {      const ps = config.parentSpan;      return typeof ps === "function" ? ps() : ps;    };    const anchor = <A, EffE>(effect: Effect.Effect<A, EffE>): Effect.Effect<A, EffE> => {      const parent = resolveParentSpan();      return parent ? Effect.withParentSpan(effect, parent) : effect;    };    const runToolEffect = <EffE>(effect: Effect.Effect<McpToolResult, EffE>) =>      Effect.runPromiseWith(context)(        anchor(effect).pipe(          Effect.catchCause((cause) => Effect.succeed(toMcpFailureResult(cause))),        ),      );     const server = yield* Effect.sync(      () =>        new McpServer(          { name: "executor", version: "1.0.0" },          {            capabilities: { tools: {} },            jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),          },        ),    ).pipe(Effect.withSpan("mcp.host.create_server"));     const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>      Effect.gen(function* () {        debugLog("execute.call", {          managedElicitation: supportsManagedElicitation(server),          elicitationSupport: getElicitationSupport(server),          clientCapabilities: server.server.getClientCapabilities() ?? null,          codeLength: code.length,        });        if (supportsManagedElicitation(server)) {          const result = yield* engine.execute(code, {            onElicitation: makeMcpElicitationHandler(server, debugLog),          });          return toMcpResult(formatExecuteResult(result));        }        const outcome = yield* engine.executeWithPause(code);        debugLog("execute.paused_flow_result", {          status: outcome.status,          executionId: outcome.status === "paused" ? outcome.execution.id : undefined,          interactionKind:            outcome.status === "paused"              ? pausedInteractionKind(outcome.execution.elicitationContext.request)              : undefined,        });        return outcome.status === "completed"          ? toMcpResult(formatExecuteResult(outcome.result))          : toMcpPausedResult(formatPausedExecution(outcome.execution));      }).pipe(        Effect.withSpan("mcp.host.tool.execute", {          attributes: {            "mcp.tool.name": "execute",            "mcp.execute.code_length": code.length,          },        }),      );     const resumeExecution = (      executionId: string,      action: "accept" | "decline" | "cancel",      content: Record<string, unknown> | undefined,    ): Effect.Effect<McpToolResult, E> =>      Effect.gen(function* () {        debugLog("resume.call", {          executionId,          action,          hasContent: content !== undefined,          clientCapabilities: server.server.getClientCapabilities() ?? null,        });        const outcome = yield* engine.resume(executionId, { action, content });        if (!outcome) {          debugLog("resume.missing_execution", { executionId });          return {            content: [{ type: "text" as const, text: `No paused execution: ${executionId}` }],            isError: true,          } satisfies McpToolResult;        }        debugLog("resume.result", {          executionId,          status: outcome.status,          nextExecutionId: outcome.status === "paused" ? outcome.execution.id : undefined,          interactionKind:            outcome.status === "paused"              ? pausedInteractionKind(outcome.execution.elicitationContext.request)              : undefined,        });        return outcome.status === "completed"          ? toMcpResult(formatExecuteResult(outcome.result))          : toMcpPausedResult(formatPausedExecution(outcome.execution));      }).pipe(        Effect.withSpan("mcp.host.tool.resume", {          attributes: {            "mcp.tool.name": "resume",            "mcp.execute.resume.action": action,            "mcp.execute.execution_id": executionId,          },        }),      );     // --- tools ---     const executeTool = yield* Effect.sync(() =>      server.registerTool(        "execute",        {          description,          inputSchema: { code: z.string().trim().min(1) },        },        ({ code }) => runToolEffect(executeCode(code)),      ),    ).pipe(      Effect.withSpan("mcp.host.register_tool", {        attributes: { "mcp.tool.name": "execute" },      }),    );     const resumeTool = yield* Effect.sync(() =>      server.registerTool(        "resume",        {          description: [            "Resume a paused execution using the executionId returned by execute.",            "Never call this without user approval unless they explicitly state otherwise.",          ].join("\n"),          inputSchema: {            executionId: z.string().describe("The execution ID from the paused result"),            action: z              .enum(["accept", "decline", "cancel"])              .describe("How to respond to the interaction"),            content: z              .string()              .describe("Optional JSON-encoded response content for form elicitations")              .default("{}"),          },        },        ({ executionId, action, content: rawContent }) =>          runToolEffect(resumeExecution(executionId, action, parseJsonContent(rawContent))),      ),    ).pipe(      Effect.withSpan("mcp.host.register_tool", {        attributes: { "mcp.tool.name": "resume" },      }),    );     // --- capability-based tool visibility ---     const syncToolAvailability = () => {      executeTool.enable();      if (supportsManagedElicitation(server)) {        resumeTool.disable();      } else {        resumeTool.enable();      }      console.error(        "[executor] MCP capability snapshot",        JSON.stringify({          ...capabilitySnapshot(server),          resumeEnabled: !supportsManagedElicitation(server),        }),      );      debugLog("tool.visibility", {        clientCapabilities: server.server.getClientCapabilities() ?? null,        elicitationSupport: getElicitationSupport(server),        managedElicitation: supportsManagedElicitation(server),        resumeEnabled: !supportsManagedElicitation(server),      });    };     yield* Effect.sync(() => {      syncToolAvailability();      server.server.oninitialized = syncToolAvailability;    }).pipe(Effect.withSpan("mcp.host.sync_tool_availability"));     return server;  }).pipe(Effect.withSpan("mcp.host.create_executor_server"));