| 661 | // --------------------------------------------------------------------------- |
| 662 | |
| 663 | export const createExecutorMcpServer = <E extends Cause.YieldableError>( |
| 664 | config: ExecutorMcpServerConfig<E>, |
| 665 | ): Effect.Effect<McpServer> => |
| 666 | Effect.gen(function* () { |
| 667 | const engine = "engine" in config ? config.engine : createExecutionEngine(config); |
| 668 | const description = |
| 669 | config.description ?? |
| 670 | (yield* engine.getDescription.pipe(Effect.withSpan("mcp.host.get_description"))); |
| 671 | // The same live integration inventory the description carries, re-used by |
| 672 | // the `skills` tool so the `execute` guide lists what is connected too. |
| 673 | const executeInventory = extractInventory(description); |
| 674 | |
| 675 | // Captured at construction time. SDK callbacks fire later (often |
| 676 | // deferred past the outer Effect's await), so we use the runtime to |
| 677 | // re-enter Effect-land at each callback edge. |
| 678 | const context = yield* Effect.context<never>(); |
| 679 | const debugEnabled = config.debug ?? readDebugDefault(); |
| 680 | const debugLog = (event: string, data: Record<string, unknown>) => { |
| 681 | if (!debugEnabled) return; |
| 682 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: debug logging must tolerate non-serializable SDK capability snapshots |
| 683 | try { |
| 684 | console.error(`[executor:mcp] ${event} ${JSON.stringify(data)}`); |
| 685 | } catch { |
| 686 | console.error(`[executor:mcp] ${event}`, data); |
| 687 | } |
| 688 | }; |
| 689 | const elicitationMode = |
| 690 | config.elicitationMode ?? |
| 691 | ({ |
| 692 | mode: "model", |
| 693 | } as const); |
| 694 | const pauseDeadline = (): PausedExecutionDeadline | undefined => { |
| 695 | const ttlMs = config.pausedExecutionLeaseMs; |
| 696 | return ttlMs === undefined || ttlMs <= 0 |
| 697 | ? undefined |
| 698 | : { ttlMs, expiresAt: new Date(Date.now() + ttlMs).toISOString() }; |
| 699 | }; |
| 700 | const onExecutionPaused = ( |
| 701 | executionId: string, |
| 702 | deadline: PausedExecutionDeadline | undefined, |
| 703 | ): Effect.Effect<void> => |
| 704 | config.pausedExecutionHooks?.onExecutionPaused?.(executionId, deadline) ?? Effect.void; |
| 705 | const onResumeStarted = (executionId: string): Effect.Effect<void> => |
| 706 | config.pausedExecutionHooks?.onResumeStarted?.(executionId) ?? Effect.void; |
| 707 | const onResumeSettled = (executionId: string): Effect.Effect<void> => |
| 708 | config.pausedExecutionHooks?.onResumeSettled?.(executionId) ?? Effect.void; |
| 709 | const resumeWithLifecycle = (executionId: string, response: ResumeResponse) => |
| 710 | Effect.gen(function* () { |
| 711 | yield* onResumeStarted(executionId); |
| 712 | return yield* engine.resume(executionId, response); |
| 713 | }).pipe(Effect.ensuring(onResumeSettled(executionId))); |
| 714 | |
| 715 | const localExecutionAlreadySettled = (executionId: string): Effect.Effect<boolean> => |
| 716 | engine.isExecutionSettled?.(executionId) ?? Effect.succeed(false); |
| 717 | |
| 718 | const resumeFallback = ( |
| 719 | executionId: string, |
| 720 | response: ResumeResponse, |