| 176 | } |
| 177 | |
| 178 | private async discoverA2ATools(url: string): Promise<ToolCapability[]> { |
| 179 | try { |
| 180 | // Use AdCPClient to connect to agent |
| 181 | const { AdCPClient } = await import("@adcp/client"); |
| 182 | const multiClient = new AdCPClient([{ |
| 183 | id: "discovery", |
| 184 | name: "Discovery Client", |
| 185 | agent_uri: url, |
| 186 | protocol: "a2a", |
| 187 | }], { userAgent: AAO_UA_DISCOVERY }); |
| 188 | const client = multiClient.agent("discovery"); |
| 189 | |
| 190 | const agentInfo = await client.getAgentInfo(); |
| 191 | logger.debug({ url, toolCount: agentInfo.tools.length }, 'A2A discovery completed'); |
| 192 | |
| 193 | return agentInfo.tools.map((tool: any) => ({ |
| 194 | name: tool.name, |
| 195 | description: tool.description || "", |
| 196 | input_schema: tool.inputSchema || tool.parameters || {}, |
| 197 | verified_at: new Date().toISOString(), |
| 198 | })); |
| 199 | } catch (error: any) { |
| 200 | // Re-throw AuthenticationRequiredError to preserve OAuth metadata for callers |
| 201 | if (error instanceof AuthenticationRequiredError) { |
| 202 | logger.info({ url, hasOAuth: error.hasOAuth }, 'A2A agent requires OAuth authentication'); |
| 203 | throw error; |
| 204 | } |
| 205 | // For generic 401 errors, wrap in AuthenticationRequiredError |
| 206 | if (is401Error(error)) { |
| 207 | logger.info({ url }, 'A2A agent returned 401'); |
| 208 | throw new AuthenticationRequiredError(url, undefined, 'Agent requires authentication'); |
| 209 | } |
| 210 | logger.debug({ url, error: error.message }, 'A2A discovery failed'); |
| 211 | throw error; |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Infer agent type from discovered tools. |