( connector: McpConnector, timeoutMs: number = Duration.toMillis(DEFAULT_DISCOVER_TIMEOUT), )
| 93 | * interrupted fiber). |
| 94 | */ |
| 95 | export const discoverTools = ( |
| 96 | connector: McpConnector, |
| 97 | timeoutMs: number = Duration.toMillis(DEFAULT_DISCOVER_TIMEOUT), |
| 98 | ): Effect.Effect<McpToolManifest, McpToolDiscoveryError> => |
| 99 | Effect.gen(function* () { |
| 100 | // Acquire connection |
| 101 | const connection = yield* connector.pipe( |
| 102 | Effect.mapError((failure) => { |
| 103 | // Preserve the handshake HTTP status (401/403 = auth wall) so the |
| 104 | // liveness health check can classify structurally. |
| 105 | const httpStatus = Predicate.isTagged(failure, "McpConnectionError") |
| 106 | ? failure.httpStatus |
| 107 | : undefined; |
| 108 | return new McpToolDiscoveryError({ |
| 109 | stage: "connect", |
| 110 | message: `Failed connecting to MCP server: ${failure.message}`, |
| 111 | ...(httpStatus !== undefined ? { httpStatus } : {}), |
| 112 | }); |
| 113 | }), |
| 114 | ); |
| 115 | |
| 116 | const manifest = yield* listAllTools(connection).pipe( |
| 117 | Effect.onExit(() => closeConnection(connection)), |
| 118 | ); |
| 119 | |
| 120 | return manifest; |
| 121 | }).pipe( |
| 122 | Effect.timeoutOrElse({ |
| 123 | duration: Duration.millis(timeoutMs), |
| 124 | orElse: () => |
| 125 | Effect.fail( |
| 126 | new McpToolDiscoveryError({ |
| 127 | stage: "connect", |
| 128 | message: `MCP discovery timed out after ${timeoutMs}ms`, |
| 129 | }), |
| 130 | ), |
| 131 | }), |
| 132 | ); |
| 133 | |
| 134 | const closeConnection = (connection: { |
| 135 | readonly close: () => Promise<void>; |
no test coverage detected