| 1109 | // --------------------------------------------------------------------------- |
| 1110 | |
| 1111 | export const createExecutorMcpServer = <E extends Cause.YieldableError>( |
| 1112 | config: ExecutorMcpServerConfig<E>, |
| 1113 | ): Effect.Effect<McpServer> => |
| 1114 | Effect.gen(function* () { |
| 1115 | const engine = "engine" in config ? config.engine : createExecutionEngine(config); |
| 1116 | const description = |
| 1117 | config.description ?? |
| 1118 | (yield* engine.getDescription.pipe(Effect.withSpan("mcp.host.get_description"))); |
| 1119 | // The same live integration inventory the description carries, re-used by |
| 1120 | // the `skills` tool so the `execute` guide lists what is connected too. |
| 1121 | const executeInventory = extractInventory(description); |
| 1122 | // Artifacts are on unless this connection opted out (`?artifacts=false`). |
| 1123 | // One flag decides the whole surface: the tools, the shell resource, and |
| 1124 | // the skills catalog below. |
| 1125 | const artifactsEnabled = config.artifactsEnabled ?? true; |
| 1126 | const skillCatalog: readonly Skill[] = skillCatalogFor({ artifacts: artifactsEnabled }); |
| 1127 | // Per-integration search tools are off unless this connection opted in |
| 1128 | // (`?search_tools=true`). |
| 1129 | const searchToolsEnabled = config.searchToolsEnabled ?? false; |
| 1130 | |
| 1131 | // Captured at construction time. SDK callbacks fire later (often |
| 1132 | // deferred past the outer Effect's await), so we use the runtime to |
| 1133 | // re-enter Effect-land at each callback edge. |
| 1134 | const context = yield* Effect.context<never>(); |
| 1135 | const debugEnabled = config.debug ?? readDebugDefault(); |
| 1136 | const debugLog = (event: string, data: Record<string, unknown>) => { |
| 1137 | if (!debugEnabled) return; |
| 1138 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: debug logging must tolerate non-serializable SDK capability snapshots |
| 1139 | try { |
| 1140 | console.error(`[executor:mcp] ${event} ${JSON.stringify(data)}`); |
| 1141 | } catch { |
| 1142 | console.error(`[executor:mcp] ${event}`, data); |
| 1143 | } |
| 1144 | }; |
| 1145 | const elicitationMode = |
| 1146 | config.elicitationMode ?? |
| 1147 | ({ |
| 1148 | mode: "model", |
| 1149 | } as const); |
| 1150 | const pauseDeadline = (): PausedExecutionDeadline | undefined => { |
| 1151 | const ttlMs = config.pausedExecutionLeaseMs; |
| 1152 | return ttlMs === undefined || ttlMs <= 0 |
| 1153 | ? undefined |
| 1154 | : { ttlMs, expiresAt: new Date(Date.now() + ttlMs).toISOString() }; |
| 1155 | }; |
| 1156 | const onExecutionPaused = ( |
| 1157 | executionId: string, |
| 1158 | deadline: PausedExecutionDeadline | undefined, |
| 1159 | ): Effect.Effect<void> => |
| 1160 | config.pausedExecutionHooks?.onExecutionPaused?.(executionId, deadline) ?? Effect.void; |
| 1161 | const onResumeStarted = (executionId: string): Effect.Effect<void> => |
| 1162 | config.pausedExecutionHooks?.onResumeStarted?.(executionId) ?? Effect.void; |
| 1163 | const onResumeSettled = (executionId: string): Effect.Effect<void> => |
| 1164 | config.pausedExecutionHooks?.onResumeSettled?.(executionId) ?? Effect.void; |
| 1165 | const resumeWithLifecycle = (executionId: string, response: ResumeResponse) => |
| 1166 | Effect.gen(function* () { |
| 1167 | yield* onResumeStarted(executionId); |
| 1168 | return yield* engine.resume(executionId, response); |