( connection: McpConnection, )
| 55 | * returns one page, not the catalog). |
| 56 | */ |
| 57 | const listAllTools = ( |
| 58 | connection: McpConnection, |
| 59 | ): Effect.Effect<McpToolManifest, McpToolDiscoveryError> => |
| 60 | Effect.gen(function* () { |
| 61 | const tools: unknown[] = []; |
| 62 | let cursor: string | undefined = undefined; |
| 63 | |
| 64 | for (let page = 0; page < MAX_LIST_TOOLS_PAGES; page++) { |
| 65 | const params: { cursor?: string } | undefined = cursor === undefined ? undefined : { cursor }; |
| 66 | const listResult = yield* Effect.tryPromise({ |
| 67 | try: () => connection.client.listTools(params), |
| 68 | // A bearer accepted at the handshake can be rejected by the time the |
| 69 | // listing runs (revocation landing mid-discovery). The fetch adapter's |
| 70 | // reauthorization interception fires here exactly as during connect, |
| 71 | // so keep the structural signals — the reauthorization tag and the |
| 72 | // HTTP status — instead of collapsing every listing failure into a |
| 73 | // generic discovery error; discarding them left the connection on a |
| 74 | // sticky degraded verdict instead of reconnect-required. Statuses come |
| 75 | // from `httpStatusFromCause` only: the connection-only numeric-code |
| 76 | // decode must stay out, since JSON-RPC error codes are not HTTP |
| 77 | // statuses. |
| 78 | catch: (cause) => { |
| 79 | if (hasNestedOAuthReauthorization(cause)) { |
| 80 | return new McpToolDiscoveryError({ |
| 81 | stage: "list_tools", |
| 82 | message: "Failed listing MCP tools: MCP OAuth re-authorization required", |
| 83 | reauthorizationRequired: true, |
| 84 | }); |
| 85 | } |
| 86 | const failure = Option.getOrNull(decodeSdkFailure(cause)); |
| 87 | // A modern-era connection whose server breaks the modern response |
| 88 | // contract means the server echoed our proposed revision without |
| 89 | // implementing it; callers can retry with legacy negotiation. |
| 90 | const modernContractViolation = |
| 91 | connection.client.getProtocolEra?.() === "modern" && failure?.code === "INVALID_RESULT"; |
| 92 | const httpStatus = httpStatusFromCause(cause); |
| 93 | return new McpToolDiscoveryError({ |
| 94 | stage: "list_tools", |
| 95 | message: |
| 96 | failure?.message !== undefined |
| 97 | ? `Failed listing MCP tools: ${failure.message.slice(0, 300)}` |
| 98 | : httpStatus === undefined |
| 99 | ? "Failed listing MCP tools" |
| 100 | : `Failed listing MCP tools (HTTP ${httpStatus})`, |
| 101 | ...(httpStatus === undefined ? {} : { httpStatus }), |
| 102 | ...(modernContractViolation ? { modernContractViolation: true } : {}), |
| 103 | }); |
| 104 | }, |
| 105 | }); |
| 106 | |
| 107 | const decoded = decodeListToolsPage(listResult); |
| 108 | if (Option.isNone(decoded)) { |
| 109 | return yield* new McpToolDiscoveryError({ |
| 110 | stage: "list_tools", |
| 111 | message: "MCP listTools response did not match the expected schema", |
| 112 | }); |
| 113 | } |
| 114 |
no test coverage detected