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