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