adcp 2.12.2__py3-none-any.whl → 2.14.0__py3-none-any.whl

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 (62) hide show
  1. adcp/__init__.py +14 -1
  2. adcp/adagents.py +53 -9
  3. adcp/client.py +361 -57
  4. adcp/protocols/mcp.py +1 -3
  5. adcp/types/__init__.py +9 -33
  6. adcp/types/_generated.py +82 -13
  7. adcp/types/aliases.py +23 -0
  8. adcp/types/base.py +193 -0
  9. adcp/types/generated_poc/adagents.py +9 -13
  10. adcp/types/generated_poc/core/activation_key.py +12 -2
  11. adcp/types/generated_poc/core/assets/daast_asset.py +12 -2
  12. adcp/types/generated_poc/core/assets/image_asset.py +9 -5
  13. adcp/types/generated_poc/core/assets/vast_asset.py +12 -2
  14. adcp/types/generated_poc/core/assets/video_asset.py +9 -5
  15. adcp/types/generated_poc/core/async_response_data.py +72 -0
  16. adcp/types/generated_poc/core/brand_manifest_ref.py +35 -0
  17. adcp/types/generated_poc/core/creative_asset.py +4 -6
  18. adcp/types/generated_poc/core/creative_manifest.py +4 -6
  19. adcp/types/generated_poc/core/deployment.py +16 -8
  20. adcp/types/generated_poc/core/destination.py +12 -2
  21. adcp/types/generated_poc/core/format.py +3 -3
  22. adcp/types/generated_poc/core/{webhook_payload.py → mcp_webhook_payload.py} +8 -33
  23. adcp/types/generated_poc/core/pricing_option.py +51 -0
  24. adcp/types/generated_poc/core/product.py +4 -29
  25. adcp/types/generated_poc/core/promoted_offerings.py +5 -21
  26. adcp/types/generated_poc/core/property.py +4 -6
  27. adcp/types/generated_poc/core/publisher_property_selector.py +15 -2
  28. adcp/types/generated_poc/core/push_notification_config.py +1 -4
  29. adcp/types/generated_poc/core/start_timing.py +18 -0
  30. adcp/types/generated_poc/core/sub_asset.py +12 -2
  31. adcp/types/generated_poc/creative/preview_creative_response.py +3 -14
  32. adcp/types/generated_poc/creative/preview_render.py +3 -11
  33. adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py +37 -0
  34. adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py +19 -0
  35. adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py +31 -0
  36. adcp/types/generated_poc/media_buy/create_media_buy_request.py +7 -26
  37. adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +4 -2
  38. adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py +38 -0
  39. adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py +24 -0
  40. adcp/types/generated_poc/media_buy/get_products_async_response_working.py +35 -0
  41. adcp/types/generated_poc/media_buy/get_products_request.py +5 -20
  42. adcp/types/generated_poc/media_buy/list_creatives_response.py +5 -7
  43. adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py +31 -0
  44. adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py +19 -0
  45. adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py +37 -0
  46. adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py +30 -0
  47. adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py +19 -0
  48. adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py +31 -0
  49. adcp/types/generated_poc/media_buy/update_media_buy_request.py +4 -14
  50. adcp/types/generated_poc/signals/activate_signal_request.py +2 -2
  51. adcp/types/generated_poc/signals/activate_signal_response.py +2 -2
  52. adcp/types/generated_poc/signals/get_signals_request.py +2 -2
  53. adcp/types/generated_poc/signals/get_signals_response.py +2 -3
  54. adcp/utils/preview_cache.py +6 -4
  55. adcp/webhooks.py +508 -0
  56. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/METADATA +2 -2
  57. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/RECORD +61 -45
  58. adcp/types/generated_poc/core/dimensions.py +0 -18
  59. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/WHEEL +0 -0
  60. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/entry_points.txt +0 -0
  61. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
  62. {adcp-2.12.2.dist-info → adcp-2.14.0.dist-info}/top_level.txt +0 -0
adcp/webhooks.py ADDED
@@ -0,0 +1,508 @@
1
+ """Webhook creation and signing utilities for AdCP agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ from datetime import datetime, timezone
9
+ from typing import Any, cast
10
+
11
+ from a2a.types import (
12
+ Artifact,
13
+ DataPart,
14
+ Message,
15
+ Part,
16
+ Role,
17
+ Task,
18
+ TaskState,
19
+ TaskStatus,
20
+ TaskStatusUpdateEvent,
21
+ )
22
+
23
+ from adcp.types import GeneratedTaskStatus
24
+ from adcp.types.base import AdCPBaseModel
25
+ from adcp.types.generated_poc.core.async_response_data import AdcpAsyncResponseData
26
+
27
+
28
+ def create_mcp_webhook_payload(
29
+ task_id: str,
30
+ status: GeneratedTaskStatus,
31
+ result: AdcpAsyncResponseData | dict[str, Any] | None = None,
32
+ timestamp: datetime | None = None,
33
+ task_type: str | None = None,
34
+ operation_id: str | None = None,
35
+ message: str | None = None,
36
+ context_id: str | None = None,
37
+ domain: str | None = None,
38
+ ) -> dict[str, Any]:
39
+ """
40
+ Create MCP webhook payload dictionary.
41
+
42
+ This function helps agent implementations construct properly formatted
43
+ webhook payloads for sending to clients.
44
+
45
+ Args:
46
+ task_id: Unique identifier for the task
47
+ status: Current task status
48
+ task_type: Optionally type of AdCP operation (e.g., "get_products", "create_media_buy")
49
+ timestamp: When the webhook was generated (defaults to current UTC time)
50
+ result: Task-specific payload (AdCP response data)
51
+ operation_id: Publisher-defined operation identifier (deprecated from payload,
52
+ should be in URL routing, but included for backward compatibility)
53
+ message: Human-readable summary of task state
54
+ context_id: Session/conversation identifier
55
+ domain: AdCP domain this task belongs to
56
+
57
+ Returns:
58
+ Dictionary matching McpWebhookPayload schema, ready to be sent as JSON
59
+
60
+ Examples:
61
+ Create a completed webhook with results:
62
+ >>> from adcp.webhooks import create_mcp_webhook_payload
63
+ >>> from adcp.types import GeneratedTaskStatus
64
+ >>>
65
+ >>> payload = create_mcp_webhook_payload(
66
+ ... task_id="task_123",
67
+ ... task_type="get_products",
68
+ ... status=GeneratedTaskStatus.completed,
69
+ ... result={"products": [...]},
70
+ ... message="Found 5 products"
71
+ ... )
72
+
73
+ Create a failed webhook with error:
74
+ >>> payload = create_mcp_webhook_payload(
75
+ ... task_id="task_456",
76
+ ... task_type="create_media_buy",
77
+ ... status=GeneratedTaskStatus.failed,
78
+ ... result={"errors": [{"code": "INVALID_INPUT", "message": "..."}]},
79
+ ... message="Validation failed"
80
+ ... )
81
+
82
+ Create a working status update:
83
+ >>> payload = create_mcp_webhook_payload(
84
+ ... task_id="task_789",
85
+ ... task_type="sync_creatives",
86
+ ... status=GeneratedTaskStatus.working,
87
+ ... message="Processing 3 of 10 creatives"
88
+ ... )
89
+ """
90
+ if timestamp is None:
91
+ timestamp = datetime.now(timezone.utc)
92
+
93
+ # Convert status enum to string value
94
+ status_value = status.value if hasattr(status, "value") else str(status)
95
+
96
+ # Build payload matching McpWebhookPayload schema
97
+ payload: dict[str, Any] = {
98
+ "task_id": task_id,
99
+ "task_type": task_type,
100
+ "status": status_value,
101
+ "timestamp": timestamp.isoformat() if isinstance(timestamp, datetime) else timestamp,
102
+ }
103
+
104
+ # Add optional fields only if provided
105
+ if result is not None:
106
+ # Convert Pydantic model to dict if needed for JSON serialization
107
+ if hasattr(result, "model_dump"):
108
+ payload["result"] = result.model_dump(mode="json")
109
+ else:
110
+ payload["result"] = result
111
+
112
+ if operation_id is not None:
113
+ payload["operation_id"] = operation_id
114
+
115
+ if message is not None:
116
+ payload["message"] = message
117
+
118
+ if context_id is not None:
119
+ payload["context_id"] = context_id
120
+
121
+ if domain is not None:
122
+ payload["domain"] = domain
123
+
124
+ return payload
125
+
126
+
127
+ def get_adcp_signed_headers_for_webhook(
128
+ headers: dict[str, Any], secret: str, timestamp: str, payload: dict[str, Any] | AdCPBaseModel
129
+ ) -> dict[str, Any]:
130
+ """
131
+ Generate AdCP-compliant signed headers for webhook delivery.
132
+
133
+ This function creates a cryptographic signature that proves the webhook
134
+ came from an authorized agent and protects against replay attacks by
135
+ including a timestamp in the signed message.
136
+
137
+ The function adds two headers to the provided headers dict:
138
+ - X-AdCP-Signature: HMAC-SHA256 signature in format "sha256=<hex_digest>"
139
+ - X-AdCP-Timestamp: ISO 8601 timestamp used in signature generation
140
+
141
+ The signing algorithm:
142
+ 1. Constructs message as "{timestamp}.{json_payload}"
143
+ 2. JSON-serializes payload with compact separators (no sorted keys for performance)
144
+ 3. UTF-8 encodes the message
145
+ 4. HMAC-SHA256 signs with the shared secret
146
+ 5. Hex-encodes and prefixes with "sha256="
147
+
148
+ Args:
149
+ headers: Existing headers dictionary to add signature headers to
150
+ secret: Shared secret key for HMAC signing
151
+ timestamp: ISO 8601 timestamp string (e.g., "2025-01-15T10:00:00Z")
152
+ payload: Webhook payload (dict or Pydantic model - will be JSON-serialized)
153
+
154
+ Returns:
155
+ The modified headers dictionary with signature headers added
156
+
157
+ Examples:
158
+ Sign and send an MCP webhook:
159
+ >>> from adcp.webhooks import create_mcp_webhook_payload get_adcp_signed_headers_for_webhook
160
+ >>> from datetime import datetime, timezone
161
+ >>>
162
+ >>> payload = create_mcp_webhook_payload(
163
+ ... task_id="task_123",
164
+ ... task_type="get_products",
165
+ ... status="completed",
166
+ ... result={"products": [...]}
167
+ ... )
168
+ >>> headers = {"Content-Type": "application/json"}
169
+ >>> timestamp = datetime.now(timezone.utc).isoformat()
170
+ >>> signed_headers = get_adcp_signed_headers_for_webhook(
171
+ ... headers, secret="my-webhook-secret", timestamp=timestamp, payload=payload
172
+ ... )
173
+ >>>
174
+ >>> # Send webhook with signed headers
175
+ >>> import httpx
176
+ >>> response = await httpx.post(
177
+ ... webhook_url,
178
+ ... json=payload,
179
+ ... headers=signed_headers
180
+ ... )
181
+
182
+ Headers will contain:
183
+ >>> print(signed_headers)
184
+ {
185
+ "Content-Type": "application/json",
186
+ "X-AdCP-Signature": "sha256=a1b2c3...",
187
+ "X-AdCP-Timestamp": "2025-01-15T10:00:00Z"
188
+ }
189
+
190
+ Sign with Pydantic model directly:
191
+ >>> from adcp import GetMediaBuyDeliveryResponse
192
+ >>> from datetime import datetime, timezone
193
+ >>>
194
+ >>> response: GetMediaBuyDeliveryResponse = ... # From API call
195
+ >>> headers = {"Content-Type": "application/json"}
196
+ >>> timestamp = datetime.now(timezone.utc).isoformat()
197
+ >>> signed_headers = get_adcp_signed_headers_for_webhook(
198
+ ... headers, secret="my-webhook-secret", timestamp=timestamp, payload=response
199
+ ... )
200
+ >>> # Pydantic model is automatically converted to dict for signing
201
+ """
202
+ # Convert Pydantic model to dict if needed
203
+ # All AdCP types inherit from AdCPBaseModel (Pydantic BaseModel)
204
+ if hasattr(payload, "model_dump"):
205
+ payload_dict = payload.model_dump(mode="json")
206
+ else:
207
+ payload_dict = payload
208
+
209
+ # Serialize payload to JSON with consistent formatting
210
+ # Note: sort_keys=False for performance (key order doesn't affect signature)
211
+ payload_bytes = json.dumps(payload_dict, separators=(",", ":"), sort_keys=False).encode("utf-8")
212
+
213
+ # Construct signed message: timestamp.payload
214
+ # Including timestamp prevents replay attacks
215
+ signed_message = f"{timestamp}.{payload_bytes.decode('utf-8')}"
216
+
217
+ # Generate HMAC-SHA256 signature over timestamp + payload
218
+ signature_hex = hmac.new(
219
+ secret.encode("utf-8"), signed_message.encode("utf-8"), hashlib.sha256
220
+ ).hexdigest()
221
+
222
+ # Add AdCP-compliant signature headers
223
+ headers["X-AdCP-Signature"] = f"sha256={signature_hex}"
224
+ headers["X-AdCP-Timestamp"] = timestamp
225
+
226
+ return headers
227
+
228
+
229
+ def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> AdcpAsyncResponseData | None:
230
+ """
231
+ Extract result data from webhook payload (MCP or A2A format).
232
+
233
+ This utility function handles webhook payloads from both MCP and A2A protocols,
234
+ extracting the result data regardless of the webhook format. Useful for quick
235
+ inspection, logging, or custom webhook routing logic without requiring full
236
+ client initialization.
237
+
238
+ Protocol Detection:
239
+ - A2A Task: Has "artifacts" field (terminated statuses: completed, failed)
240
+ - A2A TaskStatusUpdateEvent: Has nested "status.message" structure (intermediate statuses)
241
+ - MCP: Has "result" field directly
242
+
243
+ Args:
244
+ webhook_payload: Raw webhook dictionary from HTTP request (JSON-deserialized)
245
+
246
+ Returns:
247
+ AdcpAsyncResponseData union type containing the extracted AdCP response, or None
248
+ if no result present. For A2A webhooks, unwraps data from artifacts/message parts
249
+ structure. For MCP webhooks, returns the result field directly.
250
+
251
+ Examples:
252
+ Extract from MCP webhook:
253
+ >>> mcp_payload = {
254
+ ... "task_id": "task_123",
255
+ ... "task_type": "create_media_buy",
256
+ ... "status": "completed",
257
+ ... "timestamp": "2025-01-15T10:00:00Z",
258
+ ... "result": {"media_buy_id": "mb_123", "buyer_ref": "ref_123", "packages": []}
259
+ ... }
260
+ >>> result = extract_webhook_result_data(mcp_payload)
261
+ >>> print(result["media_buy_id"])
262
+ mb_123
263
+
264
+ Extract from A2A Task webhook:
265
+ >>> a2a_task_payload = {
266
+ ... "id": "task_456",
267
+ ... "context_id": "ctx_456",
268
+ ... "status": {"state": "completed", "timestamp": "2025-01-15T10:00:00Z"},
269
+ ... "artifacts": [
270
+ ... {
271
+ ... "artifact_id": "artifact_456",
272
+ ... "parts": [
273
+ ... {
274
+ ... "data": {
275
+ ... "media_buy_id": "mb_456",
276
+ ... "buyer_ref": "ref_456",
277
+ ... "packages": []
278
+ ... }
279
+ ... }
280
+ ... ]
281
+ ... }
282
+ ... ]
283
+ ... }
284
+ >>> result = extract_webhook_result_data(a2a_task_payload)
285
+ >>> print(result["media_buy_id"])
286
+ mb_456
287
+
288
+ Extract from A2A TaskStatusUpdateEvent webhook:
289
+ >>> a2a_event_payload = {
290
+ ... "task_id": "task_789",
291
+ ... "context_id": "ctx_789",
292
+ ... "status": {
293
+ ... "state": "working",
294
+ ... "timestamp": "2025-01-15T10:00:00Z",
295
+ ... "message": {
296
+ ... "message_id": "msg_789",
297
+ ... "role": "agent",
298
+ ... "parts": [
299
+ ... {"data": {"current_step": "processing", "percentage": 50}}
300
+ ... ]
301
+ ... }
302
+ ... },
303
+ ... "final": False
304
+ ... }
305
+ >>> result = extract_webhook_result_data(a2a_event_payload)
306
+ >>> print(result["percentage"])
307
+ 50
308
+
309
+ Handle webhook with no result:
310
+ >>> empty_payload = {"task_id": "task_000", "status": "working", "timestamp": "..."}
311
+ >>> result = extract_webhook_result_data(empty_payload)
312
+ >>> print(result)
313
+ None
314
+ """
315
+ # Detect A2A Task format (has "artifacts" field)
316
+ if "artifacts" in webhook_payload:
317
+ # Extract from task.artifacts[].parts[]
318
+ artifacts = webhook_payload.get("artifacts", [])
319
+ if not artifacts:
320
+ return None
321
+
322
+ # Use last artifact (most recent)
323
+ target_artifact = artifacts[-1]
324
+ parts = target_artifact.get("parts", [])
325
+ if not parts:
326
+ return None
327
+
328
+ # Find DataPart (skip TextPart)
329
+ for part in parts:
330
+ # Check if this part has "data" field (DataPart)
331
+ if "data" in part:
332
+ data = part["data"]
333
+ # Unwrap {"response": {...}} wrapper if present (A2A convention)
334
+ if isinstance(data, dict) and "response" in data and len(data) == 1:
335
+ return cast(AdcpAsyncResponseData, data["response"])
336
+ return cast(AdcpAsyncResponseData, data)
337
+
338
+ return None
339
+
340
+ # Detect A2A TaskStatusUpdateEvent format (has nested "status.message")
341
+ status = webhook_payload.get("status")
342
+ if isinstance(status, dict):
343
+ message = status.get("message")
344
+ if isinstance(message, dict):
345
+ # Extract from status.message.parts[]
346
+ parts = message.get("parts", [])
347
+ if not parts:
348
+ return None
349
+
350
+ # Find DataPart
351
+ for part in parts:
352
+ if "data" in part:
353
+ data = part["data"]
354
+ # Unwrap {"response": {...}} wrapper if present
355
+ if isinstance(data, dict) and "response" in data and len(data) == 1:
356
+ return cast(AdcpAsyncResponseData, data["response"])
357
+ return cast(AdcpAsyncResponseData, data)
358
+
359
+ return None
360
+
361
+ # MCP format: result field directly
362
+ return cast(AdcpAsyncResponseData | None, webhook_payload.get("result"))
363
+
364
+
365
+ def create_a2a_webhook_payload(
366
+ task_id: str,
367
+ status: GeneratedTaskStatus,
368
+ context_id: str,
369
+ result: AdcpAsyncResponseData | dict[str, Any],
370
+ timestamp: datetime | None = None,
371
+ ) -> Task | TaskStatusUpdateEvent:
372
+ """
373
+ Create A2A webhook payload (Task or TaskStatusUpdateEvent).
374
+
375
+ Per A2A specification:
376
+ - Terminated statuses (completed, failed): Returns Task with artifacts[].parts[]
377
+ - Intermediate statuses (working, input-required, submitted): Returns TaskStatusUpdateEvent
378
+ with status.message.parts[]
379
+
380
+ This function helps agent implementations construct properly formatted A2A webhook
381
+ payloads for sending to clients.
382
+
383
+ Args:
384
+ task_id: Unique identifier for the task
385
+ status: Current task status
386
+ context_id: Session/conversation identifier (required by A2A protocol)
387
+ timestamp: When the webhook was generated (defaults to current UTC time)
388
+ result: Task-specific payload (AdCP response data)
389
+
390
+ Returns:
391
+ Task object for terminated statuses, TaskStatusUpdateEvent for intermediate statuses
392
+
393
+ Examples:
394
+ Create a completed Task webhook:
395
+ >>> from adcp.webhooks import create_a2a_webhook_payload
396
+ >>> from adcp.types import GeneratedTaskStatus
397
+ >>>
398
+ >>> task = create_a2a_webhook_payload(
399
+ ... task_id="task_123",
400
+ ... status=GeneratedTaskStatus.completed,
401
+ ... result={"products": [...]},
402
+ ... message="Found 5 products"
403
+ ... )
404
+ >>> # task is a Task object with artifacts containing the result
405
+
406
+ Create a working status update:
407
+ >>> event = create_a2a_webhook_payload(
408
+ ... task_id="task_456",
409
+ ... status=GeneratedTaskStatus.working,
410
+ ... message="Processing 3 of 10 items"
411
+ ... )
412
+ >>> # event is a TaskStatusUpdateEvent with status.message
413
+
414
+ Send A2A webhook via HTTP POST:
415
+ >>> import httpx
416
+ >>> from a2a.types import Task
417
+ >>>
418
+ >>> payload = create_a2a_webhook_payload(...)
419
+ >>> # Serialize to dict for JSON
420
+ >>> if isinstance(payload, Task):
421
+ ... payload_dict = payload.model_dump(mode='json')
422
+ ... else:
423
+ ... payload_dict = payload.model_dump(mode='json')
424
+ >>>
425
+ >>> response = await httpx.post(webhook_url, json=payload_dict)
426
+ """
427
+ if timestamp is None:
428
+ timestamp = datetime.now(timezone.utc)
429
+
430
+ # Convert datetime to ISO string for A2A protocol
431
+ timestamp_str = timestamp.isoformat() if isinstance(timestamp, datetime) else timestamp
432
+
433
+ # Map GeneratedTaskStatus to A2A status state string
434
+ status_value = status.value if hasattr(status, "value") else str(status)
435
+
436
+ # Map AdCP status to A2A status state
437
+ # Note: A2A uses "input-required" (hyphenated) while AdCP uses "input_required" (underscore)
438
+ status_mapping = {
439
+ "completed": "completed",
440
+ "failed": "failed",
441
+ "working": "working",
442
+ "submitted": "submitted",
443
+ "input_required": "input-required",
444
+ }
445
+ a2a_status_state = status_mapping.get(status_value, status_value)
446
+
447
+ # Build parts for the message/artifact
448
+ parts: list[Part] = []
449
+
450
+ # Add DataPart
451
+ # Convert AdcpAsyncResponseData to dict if it's a Pydantic model
452
+ if hasattr(result, "model_dump"):
453
+ result_dict: dict[str, Any] = result.model_dump(mode="json")
454
+ else:
455
+ result_dict = result
456
+
457
+ data_part = DataPart(data=result_dict)
458
+ parts.append(Part(root=data_part))
459
+
460
+ # Determine if this is a terminated status (Task) or intermediate (TaskStatusUpdateEvent)
461
+ is_terminated = status in [GeneratedTaskStatus.completed, GeneratedTaskStatus.failed]
462
+
463
+ # Convert string to TaskState enum
464
+ task_state_enum = TaskState(a2a_status_state)
465
+
466
+ if is_terminated:
467
+ # Create Task object with artifacts for terminated statuses
468
+ task_status = TaskStatus(state=task_state_enum, timestamp=timestamp_str)
469
+
470
+ # Build artifact with parts
471
+ # Note: Artifact requires artifact_id, use task_id as prefix
472
+ if parts:
473
+ artifact = Artifact(
474
+ artifact_id=f"{task_id}_result",
475
+ parts=parts,
476
+ )
477
+ artifacts = [artifact]
478
+ else:
479
+ artifacts = []
480
+
481
+ return Task(
482
+ id=task_id,
483
+ status=task_status,
484
+ artifacts=artifacts,
485
+ context_id=context_id,
486
+ )
487
+ else:
488
+ # Create TaskStatusUpdateEvent with status.message for intermediate statuses
489
+ # Build message with parts
490
+ if parts:
491
+ message_obj = Message(
492
+ message_id=f"{task_id}_msg",
493
+ role=Role.agent, # Agent is responding
494
+ parts=parts,
495
+ )
496
+ else:
497
+ message_obj = None
498
+
499
+ task_status = TaskStatus(
500
+ state=task_state_enum, timestamp=timestamp_str, message=message_obj
501
+ )
502
+
503
+ return TaskStatusUpdateEvent(
504
+ task_id=task_id,
505
+ status=task_status,
506
+ context_id=context_id,
507
+ final=False, # Intermediate statuses are not final
508
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adcp
3
- Version: 2.12.2
3
+ Version: 2.14.0
4
4
  Summary: Official Python client for the Ad Context Protocol (AdCP)
5
5
  Author-email: AdCP Community <maintainers@adcontextprotocol.org>
6
6
  License: Apache-2.0
@@ -25,7 +25,7 @@ Requires-Dist: httpx>=0.24.0
25
25
  Requires-Dist: pydantic>=2.0.0
26
26
  Requires-Dist: typing-extensions>=4.5.0
27
27
  Requires-Dist: a2a-sdk>=0.3.0
28
- Requires-Dist: mcp>=0.9.0
28
+ Requires-Dist: mcp>=1.23.2
29
29
  Requires-Dist: email-validator>=2.0.0
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: pytest>=7.0.0; extra == "dev"