( connector: McpConnector, timeoutMs: number = Duration.toMillis(DEFAULT_DISCOVER_TIMEOUT), )
| 181 | * or the timeout above can still cancel them promptly. |
| 182 | */ |
| 183 | export const discoverTools = ( |
| 184 | connector: McpConnector, |
| 185 | timeoutMs: number = Duration.toMillis(DEFAULT_DISCOVER_TIMEOUT), |
| 186 | ): Effect.Effect<McpToolManifest, McpToolDiscoveryError> => |
| 187 | Effect.uninterruptibleMask((restore) => |
| 188 | Effect.gen(function* () { |
| 189 | // Acquire connection |
| 190 | const connection = yield* restore( |
| 191 | connector.pipe( |
| 192 | Effect.mapError((failure) => { |
| 193 | // Preserve the handshake HTTP status (401/403 = auth wall) and a |
| 194 | // connect-level timeout so the liveness health check can classify |
| 195 | // structurally — dropping `failureKind: "timeout"` here is what |
| 196 | // made a timed-out handshake read as a generic probe failure. |
| 197 | const httpStatus = Predicate.isTagged(failure, "McpConnectionError") |
| 198 | ? failure.httpStatus |
| 199 | : undefined; |
| 200 | const reauthorizationRequired = Predicate.isTagged( |
| 201 | failure, |
| 202 | "McpOAuthReauthorizationRequired", |
| 203 | ); |
| 204 | const timedOut = |
| 205 | Predicate.isTagged(failure, "McpConnectionError") && |
| 206 | failure.failureKind === "timeout"; |
| 207 | return new McpToolDiscoveryError({ |
| 208 | stage: "connect", |
| 209 | message: `Failed connecting to MCP server: ${failure.message}`, |
| 210 | ...(httpStatus !== undefined ? { httpStatus } : {}), |
| 211 | ...(reauthorizationRequired ? { reauthorizationRequired: true } : {}), |
| 212 | ...(timedOut ? { timedOut } : {}), |
| 213 | }); |
| 214 | }), |
| 215 | ), |
| 216 | ); |
| 217 | |
| 218 | // The connection advertises the elicitation capability (connection.ts), |
| 219 | // so a server may elicit mid-listTools — the Codex desktop plugins do |
| 220 | // this for first-use approvals. Discovery has no user to route the |
| 221 | // request to (unlike the invoke path's bridge in invoke.ts), and a |
| 222 | // handler-less request would surface as a method-not-found error on the |
| 223 | // server's side of an otherwise healthy sync. Decline explicitly: the |
| 224 | // server completes the list with whatever it allows unapproved. |
| 225 | connection.client.setRequestHandler("elicitation/create", () => |
| 226 | Promise.resolve({ action: "decline" }), |
| 227 | ); |
| 228 | |
| 229 | const manifest = yield* restore(listAllTools(connection)).pipe( |
| 230 | Effect.onExit(() => closeConnection(connection)), |
| 231 | ); |
| 232 | |
| 233 | return manifest; |
| 234 | }), |
| 235 | ).pipe( |
| 236 | Effect.timeoutOrElse({ |
| 237 | duration: Duration.millis(timeoutMs), |
| 238 | orElse: () => |
| 239 | Effect.fail( |
| 240 | new McpToolDiscoveryError({ |
no test coverage detected