@adcp/client 3.3.3 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/lib/core/ADCPMultiAgentClient.d.ts +3 -1
  2. package/dist/lib/core/ADCPMultiAgentClient.d.ts.map +1 -1
  3. package/dist/lib/core/ADCPMultiAgentClient.js +4 -2
  4. package/dist/lib/core/ADCPMultiAgentClient.js.map +1 -1
  5. package/dist/lib/core/AgentClient.d.ts +29 -1
  6. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  7. package/dist/lib/core/AgentClient.js +51 -2
  8. package/dist/lib/core/AgentClient.js.map +1 -1
  9. package/dist/lib/core/AsyncHandler.d.ts +23 -27
  10. package/dist/lib/core/AsyncHandler.d.ts.map +1 -1
  11. package/dist/lib/core/AsyncHandler.js +9 -19
  12. package/dist/lib/core/AsyncHandler.js.map +1 -1
  13. package/dist/lib/core/SingleAgentClient.d.ts +126 -22
  14. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  15. package/dist/lib/core/SingleAgentClient.js +371 -83
  16. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  17. package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
  18. package/dist/lib/core/TaskExecutor.js +3 -0
  19. package/dist/lib/core/TaskExecutor.js.map +1 -1
  20. package/dist/lib/index.d.ts +2 -1
  21. package/dist/lib/index.d.ts.map +1 -1
  22. package/dist/lib/index.js +36 -1
  23. package/dist/lib/index.js.map +1 -1
  24. package/dist/lib/types/adcp.d.ts +13 -25
  25. package/dist/lib/types/adcp.d.ts.map +1 -1
  26. package/dist/lib/types/core.generated.d.ts +489 -39
  27. package/dist/lib/types/core.generated.d.ts.map +1 -1
  28. package/dist/lib/types/core.generated.js +1 -1
  29. package/dist/lib/types/index.d.ts +1 -1
  30. package/dist/lib/types/index.d.ts.map +1 -1
  31. package/dist/lib/types/index.js.map +1 -1
  32. package/dist/lib/types/schemas.generated.d.ts +833 -212
  33. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  34. package/dist/lib/types/schemas.generated.js +146 -134
  35. package/dist/lib/types/schemas.generated.js.map +1 -1
  36. package/dist/lib/types/tools.generated.d.ts +241 -187
  37. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  38. package/dist/lib/types/tools.generated.js +1 -1
  39. package/dist/lib/types/tools.generated.js.map +1 -1
  40. package/dist/lib/utils/typeGuards.d.ts +147 -0
  41. package/dist/lib/utils/typeGuards.d.ts.map +1 -0
  42. package/dist/lib/utils/typeGuards.js +240 -0
  43. package/dist/lib/utils/typeGuards.js.map +1 -0
  44. package/dist/lib/version.d.ts +3 -3
  45. package/dist/lib/version.js +3 -3
  46. package/package.json +1 -1
@@ -62,7 +62,8 @@ class SingleAgentClient {
62
62
  executor;
63
63
  asyncHandler;
64
64
  normalizedAgent;
65
- discoveredEndpoint; // Cache discovered endpoint
65
+ discoveredEndpoint; // Cache discovered MCP endpoint
66
+ canonicalBaseUrl; // Cache canonical base URL (from agent card or stripped /mcp)
66
67
  constructor(agent, config = {}) {
67
68
  this.agent = agent;
68
69
  this.config = config;
@@ -89,6 +90,7 @@ class SingleAgentClient {
89
90
  *
90
91
  * If the agent needs discovery, perform it now and cache the result.
91
92
  * Returns the agent config with the discovered endpoint.
93
+ * Also computes the canonical base URL by stripping /mcp suffix.
92
94
  */
93
95
  async ensureEndpointDiscovered() {
94
96
  const needsDiscovery = this.normalizedAgent._needsDiscovery;
@@ -104,11 +106,101 @@ class SingleAgentClient {
104
106
  }
105
107
  // Perform discovery
106
108
  this.discoveredEndpoint = await this.discoverMCPEndpoint(this.normalizedAgent.agent_uri);
109
+ // Compute canonical base URL by stripping /mcp suffix
110
+ this.canonicalBaseUrl = this.computeBaseUrl(this.discoveredEndpoint);
107
111
  return {
108
112
  ...this.normalizedAgent,
109
113
  agent_uri: this.discoveredEndpoint,
110
114
  };
111
115
  }
116
+ /**
117
+ * Ensure A2A canonical URL is resolved (lazy initialization)
118
+ *
119
+ * Fetches the agent card and extracts the canonical URL.
120
+ * Returns the agent config with the canonical URL.
121
+ */
122
+ async ensureCanonicalUrlResolved() {
123
+ const needsCanonicalUrl = this.normalizedAgent._needsCanonicalUrl;
124
+ if (!needsCanonicalUrl) {
125
+ return this.normalizedAgent;
126
+ }
127
+ // Already resolved? Use cached value
128
+ if (this.canonicalBaseUrl) {
129
+ return {
130
+ ...this.normalizedAgent,
131
+ agent_uri: this.canonicalBaseUrl,
132
+ };
133
+ }
134
+ // Fetch agent card to get canonical URL
135
+ const canonicalUrl = await this.fetchA2ACanonicalUrl(this.normalizedAgent.agent_uri);
136
+ this.canonicalBaseUrl = canonicalUrl;
137
+ return {
138
+ ...this.normalizedAgent,
139
+ agent_uri: canonicalUrl,
140
+ };
141
+ }
142
+ /**
143
+ * Fetch the canonical URL from an A2A agent card
144
+ */
145
+ async fetchA2ACanonicalUrl(agentUri) {
146
+ const clientModule = require('@a2a-js/sdk/client');
147
+ const A2AClient = clientModule.A2AClient;
148
+ const authToken = this.normalizedAgent.auth_token_env;
149
+ const fetchImpl = authToken
150
+ ? async (url, options) => {
151
+ const headers = {
152
+ ...options?.headers,
153
+ Authorization: `Bearer ${authToken}`,
154
+ 'x-adcp-auth': authToken,
155
+ };
156
+ return fetch(url, { ...options, headers });
157
+ }
158
+ : undefined;
159
+ // Construct agent card URL
160
+ const cardUrl = agentUri.endsWith('/.well-known/agent-card.json')
161
+ ? agentUri
162
+ : agentUri.replace(/\/$/, '') + '/.well-known/agent-card.json';
163
+ const client = await A2AClient.fromCardUrl(cardUrl, fetchImpl ? { fetchImpl } : {});
164
+ const agentCard = client.agentCardPromise ? await client.agentCardPromise : client.agentCard;
165
+ // Use the canonical URL from the agent card, falling back to computed base URL
166
+ if (agentCard?.url) {
167
+ return agentCard.url;
168
+ }
169
+ // Fallback: strip .well-known/agent-card.json if present
170
+ return this.computeBaseUrl(agentUri);
171
+ }
172
+ /**
173
+ * Compute base URL by stripping protocol-specific suffixes
174
+ *
175
+ * - Strips /mcp or /mcp/ suffix for MCP endpoints
176
+ * - Strips /.well-known/agent-card.json for A2A discovery URLs
177
+ * - Strips trailing slash for consistency
178
+ */
179
+ computeBaseUrl(url) {
180
+ let baseUrl = url;
181
+ // Strip /.well-known/agent-card.json
182
+ if (baseUrl.match(/\/\.well-known\/agent-card\.json$/i)) {
183
+ baseUrl = baseUrl.replace(/\/\.well-known\/agent-card\.json$/i, '');
184
+ }
185
+ // Strip /mcp or /mcp/
186
+ if (baseUrl.match(/\/mcp\/?$/i)) {
187
+ baseUrl = baseUrl.replace(/\/mcp\/?$/i, '');
188
+ }
189
+ // Strip trailing slash for consistency
190
+ baseUrl = baseUrl.replace(/\/$/, '');
191
+ return baseUrl;
192
+ }
193
+ /**
194
+ * Check if URL is a .well-known/agent-card.json URL
195
+ *
196
+ * These URLs are A2A agent card discovery URLs and should use A2A protocol.
197
+ * Only matches when .well-known is at the root path (not in a subdirectory).
198
+ */
199
+ isWellKnownAgentCardUrl(url) {
200
+ // Match: https://example.com/.well-known/agent-card.json
201
+ // Don't match: https://example.com/api/.well-known/agent-card.json
202
+ return /^https?:\/\/[^/]+\/\.well-known\/agent-card\.json$/i.test(url);
203
+ }
112
204
  /**
113
205
  * Discover MCP endpoint by testing the provided path, then trying variants
114
206
  *
@@ -180,35 +272,56 @@ class SingleAgentClient {
180
272
  `None responded to MCP protocol.`);
181
273
  }
182
274
  /**
183
- * Normalize agent config - mark all MCP agents for discovery
275
+ * Normalize agent config
184
276
  *
185
- * We always test the endpoint they give us, and if it doesn't work,
186
- * we try adding /mcp. Simple.
277
+ * - If URL is a .well-known/agent-card.json URL, switch to A2A protocol
278
+ * (these are A2A discovery URLs, not MCP endpoints)
279
+ * - A2A agents are marked for canonical URL resolution (from agent card)
280
+ * - MCP agents are marked for endpoint discovery
187
281
  */
188
282
  normalizeAgentConfig(agent) {
283
+ // If URL is a well-known agent card URL, use A2A protocol regardless of what was specified
284
+ // Mark for canonical URL resolution - we'll fetch the agent card and use its url field
285
+ if (this.isWellKnownAgentCardUrl(agent.agent_uri)) {
286
+ return {
287
+ ...agent,
288
+ protocol: 'a2a',
289
+ _needsCanonicalUrl: true,
290
+ };
291
+ }
292
+ if (agent.protocol === 'a2a') {
293
+ // A2A agents need canonical URL resolution from agent card
294
+ return {
295
+ ...agent,
296
+ _needsCanonicalUrl: true,
297
+ };
298
+ }
189
299
  if (agent.protocol !== 'mcp') {
190
300
  return agent;
191
301
  }
192
- // Mark for discovery - we'll test their path, then try adding /mcp
302
+ // MCP agents need endpoint discovery - we'll test their path, then try adding /mcp
193
303
  return {
194
304
  ...agent,
195
305
  _needsDiscovery: true,
196
306
  };
197
307
  }
198
308
  /**
199
- * Handle webhook from agent (async task completion)
309
+ * Handle webhook from agent (async task status updates and completions)
200
310
  *
201
- * Accepts either:
202
- * 1. Standard WebhookPayload format (operation_id, task_type, result, etc.)
203
- * 2. Raw A2A task payload (artifacts, status, contextId, etc.) - will be transformed
311
+ * Accepts webhook payloads from both MCP and A2A protocols:
312
+ * 1. MCP: MCPWebhookPayload envelope with AdCP data in .result field
313
+ * 2. A2A: Native Task/TaskStatusUpdateEvent with AdCP data in either:
314
+ * - status.message.parts[].data (for status updates)
315
+ * - artifacts (for task completion, per A2A spec)
204
316
  *
205
- * For A2A payloads, extracts the ADCP response from artifacts[0].parts[].data
206
- * so handlers receive the unwrapped response, not the raw protocol structure.
317
+ * The method normalizes both formats so handlers receive the unwrapped
318
+ * AdCP response data (AdCPAsyncResponseData), not the raw protocol structure.
207
319
  *
208
- * @param payload - Webhook payload from agent (WebhookPayload or raw A2A task)
320
+ * @param payload - Protocol-specific webhook payload (MCPWebhookPayload | Task | TaskStatusUpdateEvent)
321
+ * @param taskType - Task type (e.g create_media_buy) from url param or url part of the webhook delivery
322
+ * @param operationId - Operation id (e.g used for client app to track the operation) from the param or url part of the webhook delivery
209
323
  * @param signature - X-ADCP-Signature header (format: "sha256=...")
210
324
  * @param timestamp - X-ADCP-Timestamp header (Unix timestamp)
211
- * @param taskType - Task type override (useful when not in payload, e.g., from URL path)
212
325
  * @returns Whether webhook was handled successfully
213
326
  *
214
327
  * @example
@@ -226,7 +339,7 @@ class SingleAgentClient {
226
339
  * });
227
340
  * ```
228
341
  */
229
- async handleWebhook(payload, signature, timestamp, taskType) {
342
+ async handleWebhook(payload, taskType, operationId, signature, timestamp) {
230
343
  // Verify signature if secret is configured
231
344
  if (this.config.webhookSecret) {
232
345
  if (!signature || !timestamp) {
@@ -236,78 +349,124 @@ class SingleAgentClient {
236
349
  if (!isValid) {
237
350
  throw new Error('Invalid webhook signature or timestamp too old');
238
351
  }
352
+ console.log('[ADCP Client]: Webhook signature is valid');
239
353
  }
240
- // Transform raw A2A task payload to WebhookPayload format
241
- const normalizedPayload = this.normalizeWebhookPayload(payload, taskType);
242
- // Emit activity
243
- await this.config.onActivity?.({
244
- type: 'webhook_received',
354
+ // Transform raw protocol payload to normalized format
355
+ const normalizedPayload = this.normalizeWebhookPayload(payload, taskType, operationId);
356
+ const metadata = {
245
357
  operation_id: normalizedPayload.operation_id,
246
- agent_id: this.agent.id,
247
358
  context_id: normalizedPayload.context_id,
248
359
  task_id: normalizedPayload.task_id,
360
+ agent_id: this.agent.id,
249
361
  task_type: normalizedPayload.task_type,
250
362
  status: normalizedPayload.status,
251
- payload: normalizedPayload.result,
363
+ message: normalizedPayload.message,
252
364
  timestamp: normalizedPayload.timestamp || new Date().toISOString(),
365
+ };
366
+ // Emit activity
367
+ await this.config.onActivity?.({
368
+ type: 'webhook_received',
369
+ operation_id: metadata.operation_id,
370
+ agent_id: metadata.agent_id,
371
+ context_id: metadata.context_id,
372
+ task_id: metadata.task_id,
373
+ task_type: metadata.task_type,
374
+ status: metadata.status,
375
+ payload: normalizedPayload.result,
376
+ timestamp: metadata.timestamp,
253
377
  });
254
378
  // Handle through async handler if configured
255
379
  if (this.asyncHandler) {
256
- await this.asyncHandler.handleWebhook(normalizedPayload, this.agent.id);
380
+ await this.asyncHandler.handleWebhook({ result: normalizedPayload.result, metadata });
257
381
  return true;
258
382
  }
259
383
  return false;
260
384
  }
261
385
  /**
262
- * Normalize webhook payload - transform raw A2A task payload to WebhookPayload format
386
+ * Normalize webhook payload - handles both MCP and A2A webhook formats
263
387
  *
264
- * Detects if payload is a raw A2A task (has artifacts, kind: 'task') and extracts
265
- * the ADCP response from artifacts[0].parts[].data where kind === 'data'.
388
+ * MCP: Uses MCPWebhookPayload envelope with AdCP data in .result field
389
+ * A2A: Uses native Task/TaskStatusUpdateEvent messages with AdCP data in either:
390
+ * - status.message.parts[].data (for status updates)
391
+ * - artifacts (for task completion responses, per A2A spec)
266
392
  *
267
- * @param payload - Raw webhook payload (could be WebhookPayload or A2A task)
268
- * @param taskType - Task type override (useful when from URL path)
269
- * @returns Normalized WebhookPayload with extracted ADCP response
393
+ * @param payload - Protocol-specific webhook payload (MCPWebhookPayload | Task | TaskStatusUpdateEvent)
394
+ * @param taskType - Task type override
395
+ * @param operationId - Operation id
396
+ * @returns Normalized webhook payload with extracted AdCP response
270
397
  */
271
- normalizeWebhookPayload(payload, taskType) {
272
- // Check if this is a raw A2A task payload (has artifacts and kind: 'task')
273
- const isA2ATaskPayload = payload.artifacts && (payload.kind === 'task' || payload.status?.state);
274
- if (!isA2ATaskPayload) {
275
- // Already in WebhookPayload format or close enough
276
- return payload;
398
+ normalizeWebhookPayload(payload, taskType, operationId) {
399
+ // 1. Check for MCP Webhook Payload (has task_id, status, task_type fields)
400
+ if ('task_id' in payload && 'task_type' in payload && 'status' in payload) {
401
+ const mcpPayload = payload;
402
+ return {
403
+ operation_id: operationId || 'unknown',
404
+ context_id: mcpPayload.context_id,
405
+ task_id: mcpPayload.task_id,
406
+ task_type: taskType,
407
+ status: mcpPayload.status,
408
+ result: mcpPayload.result,
409
+ message: mcpPayload.message,
410
+ timestamp: mcpPayload.timestamp,
411
+ };
277
412
  }
278
- // Extract status from A2A task
279
- const a2aStatus = payload.status?.state || 'unknown';
280
- // For completed tasks, extract the ADCP response from artifacts
281
- let result = undefined;
282
- if (a2aStatus === 'completed' && payload.artifacts?.length > 0) {
283
- try {
284
- // Use the response unwrapper to extract ADCP data from A2A artifacts
285
- // Wrap in the format unwrapProtocolResponse expects
286
- result = (0, response_unwrapper_1.unwrapProtocolResponse)({ result: payload }, taskType, 'a2a');
413
+ // 2. Check for A2A Task or TaskStatusUpdateEvent
414
+ if ('kind' in payload && (payload.kind === 'task' || payload.kind === 'status-update')) {
415
+ const a2aPayload = payload;
416
+ const a2aStatus = a2aPayload.status?.state || 'unknown';
417
+ let result = undefined;
418
+ // Try to extract data from status.message.parts first (for status updates)
419
+ const parts = a2aPayload.status?.message?.parts;
420
+ if (parts && Array.isArray(parts)) {
421
+ const dataPart = parts.find(p => 'data' in p && p.kind === 'data');
422
+ if (dataPart && 'data' in dataPart) {
423
+ result = dataPart.data;
424
+ }
287
425
  }
288
- catch (error) {
289
- // If unwrapping fails, pass the raw artifacts as result
290
- // The handler can deal with it
291
- console.warn('Failed to unwrap A2A webhook payload:', error);
292
- result = payload.artifacts;
426
+ // If not found in parts, check artifacts (standard A2A task output location)
427
+ if (!result && 'artifacts' in a2aPayload && a2aPayload.artifacts && a2aPayload.artifacts.length > 0) {
428
+ try {
429
+ // Try to unwrap artifacts for all statuses
430
+ result = (0, response_unwrapper_1.unwrapProtocolResponse)({ result: a2aPayload }, taskType, 'a2a');
431
+ }
432
+ catch (error) {
433
+ console.warn('Failed to unwrap A2A webhook payload:', error);
434
+ // Fallback: pass raw artifacts so handler has something to work with
435
+ result = a2aPayload.artifacts;
436
+ }
293
437
  }
438
+ // Extract message part from status.message.parts (A2A Message structure)
439
+ let message = undefined;
440
+ if (a2aPayload.status?.message?.parts) {
441
+ const textParts = a2aPayload.status.message.parts
442
+ .filter(p => p.kind === 'text' && 'text' in p)
443
+ .map(p => ('text' in p ? p.text : ''));
444
+ if (textParts.length > 0) {
445
+ message = textParts.join(' ');
446
+ }
447
+ }
448
+ // Get task_id ensuring it's a string
449
+ let taskId = 'unknown';
450
+ if ('id' in a2aPayload && a2aPayload.id) {
451
+ taskId = String(a2aPayload.id);
452
+ }
453
+ else if ('taskId' in a2aPayload && a2aPayload.taskId) {
454
+ taskId = String(a2aPayload.taskId);
455
+ }
456
+ return {
457
+ operation_id: operationId,
458
+ context_id: 'contextId' in a2aPayload ? a2aPayload.contextId : undefined,
459
+ task_id: taskId,
460
+ task_type: taskType,
461
+ status: a2aStatus,
462
+ result,
463
+ message: message,
464
+ timestamp: a2aPayload.status?.timestamp || new Date().toISOString(),
465
+ };
294
466
  }
295
- else if (payload.artifacts?.length > 0) {
296
- // For non-completed tasks (working, input-required), just pass artifacts
297
- result = payload.artifacts;
298
- }
299
- // Build normalized WebhookPayload
300
- return {
301
- operation_id: payload.metadata?.operation_id || payload.id || 'unknown',
302
- context_id: payload.contextId || payload.metadata?.adcp_context?.buyer_ref,
303
- task_id: payload.id,
304
- task_type: payload?.task_type || taskType || 'unknown',
305
- status: a2aStatus,
306
- result,
307
- error: payload.status?.message?.message || payload.error,
308
- message: payload.status?.message?.message,
309
- timestamp: payload.status?.timestamp || new Date().toISOString(),
310
- };
467
+ // 3. Unknown payload format
468
+ throw new Error('Unsupported webhook payload format. Expected MCPWebhookPayload, Task, or TaskStatusUpdateEvent. ' +
469
+ `Received: ${JSON.stringify(payload).substring(0, 200)}`);
311
470
  }
312
471
  /**
313
472
  * Generate webhook URL using macro substitution
@@ -501,23 +660,34 @@ class SingleAgentClient {
501
660
  * @param options - Task execution options
502
661
  */
503
662
  async createMediaBuy(params, inputHandler, options) {
504
- // Auto-inject reporting_webhook if supported and not provided by caller
663
+ // Merge library defaults with consumer-provided reporting_webhook config
664
+ // Library provides url/auth/frequency defaults, consumer can override any field
505
665
  // Generates a media_buy_delivery webhook URL using operation_id pattern: delivery_report_{agent_id}_{YYYY-MM}
506
- if (!params?.reporting_webhook && this.config.webhookUrlTemplate) {
666
+ if (this.config.webhookUrlTemplate) {
507
667
  const now = new Date();
508
668
  const year = now.getUTCFullYear();
509
669
  const month = String(now.getUTCMonth() + 1).padStart(2, '0');
510
670
  const operationId = `delivery_report_${this.agent.id}_${year}-${month}`;
511
671
  const deliveryWebhookUrl = this.getWebhookUrl('media_buy_delivery', operationId);
672
+ // Library defaults
673
+ const libraryDefaults = {
674
+ url: deliveryWebhookUrl,
675
+ authentication: {
676
+ schemes: ['HMAC-SHA256'],
677
+ credentials: this.config.webhookSecret || 'placeholder_secret_min_32_characters_required',
678
+ },
679
+ reporting_frequency: (this.config.reportingWebhookFrequency || 'daily'),
680
+ };
681
+ // Deep merge: consumer overrides library defaults
512
682
  params = {
513
683
  ...params,
514
684
  reporting_webhook: {
515
- url: deliveryWebhookUrl,
685
+ ...libraryDefaults,
686
+ ...params.reporting_webhook,
516
687
  authentication: {
517
- schemes: ['HMAC-SHA256'],
518
- credentials: this.config.webhookSecret || 'placeholder_secret_min_32_characters_required',
688
+ ...libraryDefaults.authentication,
689
+ ...params.reporting_webhook?.authentication,
519
690
  },
520
- reporting_frequency: this.config.reportingWebhookFrequency || 'daily',
521
691
  },
522
692
  };
523
693
  }
@@ -702,10 +872,39 @@ class SingleAgentClient {
702
872
  }
703
873
  // ====== AGENT INFORMATION ======
704
874
  /**
705
- * Get the agent configuration
875
+ * Get the agent configuration with normalized protocol
876
+ *
877
+ * Returns the agent config with:
878
+ * - Protocol normalized (e.g., .well-known URLs switch to A2A)
879
+ * - If canonical URL has been resolved, agent_uri will be the canonical URL
880
+ *
881
+ * For guaranteed canonical URL, use getResolvedAgent() instead.
706
882
  */
707
883
  getAgent() {
708
- return { ...this.agent };
884
+ // If we have resolved the canonical URL, return config with it
885
+ if (this.canonicalBaseUrl) {
886
+ const { _needsDiscovery, _needsCanonicalUrl, ...cleanAgent } = this.normalizedAgent;
887
+ return {
888
+ ...cleanAgent,
889
+ agent_uri: this.canonicalBaseUrl,
890
+ };
891
+ }
892
+ // Return normalized agent without internal flags
893
+ const { _needsDiscovery, _needsCanonicalUrl, ...cleanAgent } = this.normalizedAgent;
894
+ return { ...cleanAgent };
895
+ }
896
+ /**
897
+ * Get the fully resolved agent configuration
898
+ *
899
+ * This async method ensures the agent config has the canonical URL resolved:
900
+ * - For A2A: Fetches the agent card and uses its 'url' field
901
+ * - For MCP: Performs endpoint discovery
902
+ *
903
+ * @returns Promise resolving to agent config with canonical URL
904
+ */
905
+ async getResolvedAgent() {
906
+ await this.resolveCanonicalUrl();
907
+ return this.getAgent();
709
908
  }
710
909
  /**
711
910
  * Get the agent ID
@@ -720,10 +919,99 @@ class SingleAgentClient {
720
919
  return this.agent.name;
721
920
  }
722
921
  /**
723
- * Get the agent protocol
922
+ * Get the agent protocol (may be normalized from original config)
724
923
  */
725
924
  getProtocol() {
726
- return this.agent.protocol;
925
+ return this.normalizedAgent.protocol;
926
+ }
927
+ /**
928
+ * Get the canonical base URL for this agent
929
+ *
930
+ * Returns the canonical URL if already resolved, or computes it synchronously
931
+ * from the configured URL. For the most accurate canonical URL (especially for A2A
932
+ * where the agent card contains the authoritative URL), use resolveCanonicalUrl() first.
933
+ *
934
+ * The canonical URL is:
935
+ * - For A2A: The 'url' field from the agent card (if resolved), or base URL with
936
+ * /.well-known/agent-card.json stripped
937
+ * - For MCP: The discovered endpoint with /mcp stripped
938
+ *
939
+ * @returns The canonical base URL (synchronous, may not be fully resolved)
940
+ */
941
+ getCanonicalUrl() {
942
+ // Return cached canonical URL if available
943
+ if (this.canonicalBaseUrl) {
944
+ return this.canonicalBaseUrl;
945
+ }
946
+ // Compute from configured URL (best effort without network call)
947
+ return this.computeBaseUrl(this.normalizedAgent.agent_uri);
948
+ }
949
+ /**
950
+ * Resolve and return the canonical base URL for this agent
951
+ *
952
+ * This async method ensures the canonical URL is properly resolved:
953
+ * - For A2A: Fetches the agent card and uses its 'url' field
954
+ * - For MCP: Performs endpoint discovery and strips /mcp suffix
955
+ *
956
+ * The result is cached, so subsequent calls are fast.
957
+ *
958
+ * @returns Promise resolving to the canonical base URL
959
+ */
960
+ async resolveCanonicalUrl() {
961
+ if (this.canonicalBaseUrl) {
962
+ return this.canonicalBaseUrl;
963
+ }
964
+ if (this.normalizedAgent.protocol === 'a2a') {
965
+ await this.ensureCanonicalUrlResolved();
966
+ }
967
+ else if (this.normalizedAgent.protocol === 'mcp') {
968
+ await this.ensureEndpointDiscovered();
969
+ }
970
+ return this.canonicalBaseUrl || this.computeBaseUrl(this.normalizedAgent.agent_uri);
971
+ }
972
+ /**
973
+ * Check if this agent is the same as another agent
974
+ *
975
+ * Compares agents by their canonical base URLs. Two agents are considered
976
+ * the same if they have the same canonical URL, regardless of:
977
+ * - Protocol (MCP vs A2A)
978
+ * - URL format (with/without /mcp, with/without /.well-known/agent-card.json)
979
+ * - Trailing slashes
980
+ *
981
+ * @param other - Another agent configuration or SingleAgentClient to compare
982
+ * @returns true if agents have the same canonical URL
983
+ */
984
+ isSameAgent(other) {
985
+ const thisUrl = this.getCanonicalUrl().toLowerCase();
986
+ let otherUrl;
987
+ if (other instanceof SingleAgentClient) {
988
+ otherUrl = other.getCanonicalUrl().toLowerCase();
989
+ }
990
+ else {
991
+ otherUrl = this.computeBaseUrl(other.agent_uri).toLowerCase();
992
+ }
993
+ return thisUrl === otherUrl;
994
+ }
995
+ /**
996
+ * Async version of isSameAgent that resolves canonical URLs first
997
+ *
998
+ * This provides more accurate comparison for A2A agents since it fetches
999
+ * the agent card to get the authoritative canonical URL.
1000
+ *
1001
+ * @param other - Another agent configuration or SingleAgentClient to compare
1002
+ * @returns Promise resolving to true if agents have the same canonical URL
1003
+ */
1004
+ async isSameAgentResolved(other) {
1005
+ const thisUrl = (await this.resolveCanonicalUrl()).toLowerCase();
1006
+ let otherUrl;
1007
+ if (other instanceof SingleAgentClient) {
1008
+ otherUrl = (await other.resolveCanonicalUrl()).toLowerCase();
1009
+ }
1010
+ else {
1011
+ // For raw AgentConfig, we can only compute from the URL
1012
+ otherUrl = this.computeBaseUrl(other.agent_uri).toLowerCase();
1013
+ }
1014
+ return thisUrl === otherUrl;
727
1015
  }
728
1016
  /**
729
1017
  * Get active tasks for this agent
@@ -832,7 +1120,7 @@ class SingleAgentClient {
832
1120
  * ```
833
1121
  */
834
1122
  async getAgentInfo() {
835
- if (this.agent.protocol === 'mcp') {
1123
+ if (this.normalizedAgent.protocol === 'mcp') {
836
1124
  // Discover endpoint if needed
837
1125
  const agent = await this.ensureEndpointDiscovered();
838
1126
  // Use MCP SDK to list tools
@@ -842,7 +1130,7 @@ class SingleAgentClient {
842
1130
  name: 'AdCP-Client',
843
1131
  version: '1.0.0',
844
1132
  });
845
- const authToken = this.agent.auth_token_env;
1133
+ const authToken = this.normalizedAgent.auth_token_env;
846
1134
  const customFetch = authToken
847
1135
  ? async (input, init) => {
848
1136
  // IMPORTANT: Must preserve SDK's default headers (especially Accept header)
@@ -891,18 +1179,18 @@ class SingleAgentClient {
891
1179
  parameters: tool.inputSchema?.properties ? Object.keys(tool.inputSchema.properties) : [],
892
1180
  }));
893
1181
  return {
894
- name: this.agent.name,
1182
+ name: this.normalizedAgent.name,
895
1183
  description: undefined,
896
- protocol: this.agent.protocol,
1184
+ protocol: this.normalizedAgent.protocol,
897
1185
  url: agent.agent_uri,
898
1186
  tools,
899
1187
  };
900
1188
  }
901
- else if (this.agent.protocol === 'a2a') {
1189
+ else if (this.normalizedAgent.protocol === 'a2a') {
902
1190
  // Use A2A SDK to get agent card
903
1191
  const clientModule = require('@a2a-js/sdk/client');
904
1192
  const A2AClient = clientModule.A2AClient;
905
- const authToken = this.agent.auth_token_env;
1193
+ const authToken = this.normalizedAgent.auth_token_env;
906
1194
  const fetchImpl = authToken
907
1195
  ? async (url, options) => {
908
1196
  const headers = {
@@ -927,14 +1215,14 @@ class SingleAgentClient {
927
1215
  }))
928
1216
  : [];
929
1217
  return {
930
- name: agentCard?.displayName || agentCard?.name || this.agent.name,
1218
+ name: agentCard?.displayName || agentCard?.name || this.normalizedAgent.name,
931
1219
  description: agentCard?.description,
932
- protocol: this.agent.protocol,
1220
+ protocol: this.normalizedAgent.protocol,
933
1221
  url: this.normalizedAgent.agent_uri,
934
1222
  tools,
935
1223
  };
936
1224
  }
937
- throw new Error(`Unsupported protocol: ${this.agent.protocol}`);
1225
+ throw new Error(`Unsupported protocol: ${this.normalizedAgent.protocol}`);
938
1226
  }
939
1227
  // ====== STATIC HELPER METHODS ======
940
1228
  /**