( connector: McpConnector, timeoutMs: number = Duration.toMillis(DEFAULT_DISCOVER_TIMEOUT), )
| 124 | * or the timeout above can still cancel them promptly. |
| 125 | */ |
| 126 | export const discoverTools = ( |
| 127 | connector: McpConnector, |
| 128 | timeoutMs: number = Duration.toMillis(DEFAULT_DISCOVER_TIMEOUT), |
| 129 | ): Effect.Effect<McpToolManifest, McpToolDiscoveryError> => |
| 130 | Effect.uninterruptibleMask((restore) => |
| 131 | Effect.gen(function* () { |
| 132 | // Acquire connection |
| 133 | const connection = yield* restore( |
| 134 | connector.pipe( |
| 135 | Effect.mapError((failure) => { |
| 136 | // Preserve the handshake HTTP status (401/403 = auth wall) and a |
| 137 | // connect-level timeout so the liveness health check can classify |
| 138 | // structurally — dropping `failureKind: "timeout"` here is what |
| 139 | // made a timed-out handshake read as a generic probe failure. |
| 140 | const httpStatus = Predicate.isTagged(failure, "McpConnectionError") |
| 141 | ? failure.httpStatus |
| 142 | : undefined; |
| 143 | const reauthorizationRequired = Predicate.isTagged( |
| 144 | failure, |
| 145 | "McpOAuthReauthorizationRequired", |
| 146 | ); |
| 147 | const timedOut = |
| 148 | Predicate.isTagged(failure, "McpConnectionError") && |
| 149 | failure.failureKind === "timeout"; |
| 150 | return new McpToolDiscoveryError({ |
| 151 | stage: "connect", |
| 152 | message: `Failed connecting to MCP server: ${failure.message}`, |
| 153 | ...(httpStatus !== undefined ? { httpStatus } : {}), |
| 154 | ...(reauthorizationRequired ? { reauthorizationRequired: true } : {}), |
| 155 | ...(timedOut ? { timedOut } : {}), |
| 156 | }); |
| 157 | }), |
| 158 | ), |
| 159 | ); |
| 160 | |
| 161 | // The connection advertises the elicitation capability (connection.ts), |
| 162 | // so a server may elicit mid-listTools — the Codex desktop plugins do |
| 163 | // this for first-use approvals. Discovery has no user to route the |
| 164 | // request to (unlike the invoke path's bridge in invoke.ts), and a |
| 165 | // handler-less request would surface as a method-not-found error on the |
| 166 | // server's side of an otherwise healthy sync. Decline explicitly: the |
| 167 | // server completes the list with whatever it allows unapproved. |
| 168 | connection.client.setRequestHandler("elicitation/create", () => |
| 169 | Promise.resolve({ action: "decline" }), |
| 170 | ); |
| 171 | |
| 172 | const manifest = yield* restore(listAllTools(connection)).pipe( |
| 173 | Effect.onExit(() => closeConnection(connection)), |
| 174 | ); |
| 175 | |
| 176 | return manifest; |
| 177 | }), |
| 178 | ).pipe( |
| 179 | Effect.timeoutOrElse({ |
| 180 | duration: Duration.millis(timeoutMs), |
| 181 | orElse: () => |
| 182 | Effect.fail( |
| 183 | new McpToolDiscoveryError({ |
no test coverage detected