adcp 0.1.2__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.
adcp/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ AdCP Python Client Library
3
+
4
+ Official Python client for the Ad Context Protocol (AdCP).
5
+ Supports both A2A and MCP protocols with full type safety.
6
+ """
7
+
8
+ from adcp.client import ADCPClient, ADCPMultiAgentClient
9
+ from adcp.types.core import AgentConfig, TaskResult, WebhookMetadata
10
+
11
+ __version__ = "0.1.2"
12
+ __all__ = [
13
+ "ADCPClient",
14
+ "ADCPMultiAgentClient",
15
+ "AgentConfig",
16
+ "TaskResult",
17
+ "WebhookMetadata",
18
+ ]
adcp/client.py ADDED
@@ -0,0 +1,493 @@
1
+ """Main client classes for AdCP."""
2
+
3
+ import json
4
+ import os
5
+ from collections.abc import Callable
6
+ from datetime import datetime
7
+ from typing import Any
8
+ from uuid import uuid4
9
+
10
+ from adcp.protocols.a2a import A2AAdapter
11
+ from adcp.protocols.base import ProtocolAdapter
12
+ from adcp.protocols.mcp import MCPAdapter
13
+ from adcp.types.core import (
14
+ Activity,
15
+ ActivityType,
16
+ AgentConfig,
17
+ Protocol,
18
+ TaskResult,
19
+ )
20
+
21
+
22
+ def create_operation_id() -> str:
23
+ """Generate a unique operation ID."""
24
+ return f"op_{uuid4().hex[:12]}"
25
+
26
+
27
+ class ADCPClient:
28
+ """Client for interacting with a single AdCP agent."""
29
+
30
+ def __init__(
31
+ self,
32
+ agent_config: AgentConfig,
33
+ webhook_url_template: str | None = None,
34
+ webhook_secret: str | None = None,
35
+ on_activity: Callable[[Activity], None] | None = None,
36
+ ):
37
+ """
38
+ Initialize ADCP client for a single agent.
39
+
40
+ Args:
41
+ agent_config: Agent configuration
42
+ webhook_url_template: Template for webhook URLs with {agent_id},
43
+ {task_type}, {operation_id}
44
+ webhook_secret: Secret for webhook signature verification
45
+ on_activity: Callback for activity events
46
+ """
47
+ self.agent_config = agent_config
48
+ self.webhook_url_template = webhook_url_template
49
+ self.webhook_secret = webhook_secret
50
+ self.on_activity = on_activity
51
+
52
+ # Initialize protocol adapter
53
+ self.adapter: ProtocolAdapter
54
+ if agent_config.protocol == Protocol.A2A:
55
+ self.adapter = A2AAdapter(agent_config)
56
+ elif agent_config.protocol == Protocol.MCP:
57
+ self.adapter = MCPAdapter(agent_config)
58
+ else:
59
+ raise ValueError(f"Unsupported protocol: {agent_config.protocol}")
60
+
61
+ def get_webhook_url(self, task_type: str, operation_id: str) -> str:
62
+ """Generate webhook URL for a task."""
63
+ if not self.webhook_url_template:
64
+ raise ValueError("webhook_url_template not configured")
65
+
66
+ return self.webhook_url_template.format(
67
+ agent_id=self.agent_config.id,
68
+ task_type=task_type,
69
+ operation_id=operation_id,
70
+ )
71
+
72
+ def _emit_activity(self, activity: Activity) -> None:
73
+ """Emit activity event."""
74
+ if self.on_activity:
75
+ self.on_activity(activity)
76
+
77
+ async def get_products(self, brief: str, **kwargs: Any) -> TaskResult[Any]:
78
+ """Get advertising products."""
79
+ operation_id = create_operation_id()
80
+ params = {"brief": brief, **kwargs}
81
+
82
+ self._emit_activity(
83
+ Activity(
84
+ type=ActivityType.PROTOCOL_REQUEST,
85
+ operation_id=operation_id,
86
+ agent_id=self.agent_config.id,
87
+ task_type="get_products",
88
+ timestamp=datetime.utcnow().isoformat(),
89
+ )
90
+ )
91
+
92
+ result = await self.adapter.call_tool("get_products", params)
93
+
94
+ self._emit_activity(
95
+ Activity(
96
+ type=ActivityType.PROTOCOL_RESPONSE,
97
+ operation_id=operation_id,
98
+ agent_id=self.agent_config.id,
99
+ task_type="get_products",
100
+ status=result.status,
101
+ timestamp=datetime.utcnow().isoformat(),
102
+ )
103
+ )
104
+
105
+ return result
106
+
107
+ async def list_creative_formats(self, **kwargs: Any) -> TaskResult[Any]:
108
+ """List supported creative formats."""
109
+ operation_id = create_operation_id()
110
+
111
+ self._emit_activity(
112
+ Activity(
113
+ type=ActivityType.PROTOCOL_REQUEST,
114
+ operation_id=operation_id,
115
+ agent_id=self.agent_config.id,
116
+ task_type="list_creative_formats",
117
+ timestamp=datetime.utcnow().isoformat(),
118
+ )
119
+ )
120
+
121
+ result = await self.adapter.call_tool("list_creative_formats", kwargs)
122
+
123
+ self._emit_activity(
124
+ Activity(
125
+ type=ActivityType.PROTOCOL_RESPONSE,
126
+ operation_id=operation_id,
127
+ agent_id=self.agent_config.id,
128
+ task_type="list_creative_formats",
129
+ status=result.status,
130
+ timestamp=datetime.utcnow().isoformat(),
131
+ )
132
+ )
133
+
134
+ return result
135
+
136
+ async def create_media_buy(self, **kwargs: Any) -> TaskResult[Any]:
137
+ """Create a new media buy."""
138
+ operation_id = create_operation_id()
139
+
140
+ self._emit_activity(
141
+ Activity(
142
+ type=ActivityType.PROTOCOL_REQUEST,
143
+ operation_id=operation_id,
144
+ agent_id=self.agent_config.id,
145
+ task_type="create_media_buy",
146
+ timestamp=datetime.utcnow().isoformat(),
147
+ )
148
+ )
149
+
150
+ result = await self.adapter.call_tool("create_media_buy", kwargs)
151
+
152
+ self._emit_activity(
153
+ Activity(
154
+ type=ActivityType.PROTOCOL_RESPONSE,
155
+ operation_id=operation_id,
156
+ agent_id=self.agent_config.id,
157
+ task_type="create_media_buy",
158
+ status=result.status,
159
+ timestamp=datetime.utcnow().isoformat(),
160
+ )
161
+ )
162
+
163
+ return result
164
+
165
+ async def update_media_buy(self, **kwargs: Any) -> TaskResult[Any]:
166
+ """Update an existing media buy."""
167
+ operation_id = create_operation_id()
168
+
169
+ self._emit_activity(
170
+ Activity(
171
+ type=ActivityType.PROTOCOL_REQUEST,
172
+ operation_id=operation_id,
173
+ agent_id=self.agent_config.id,
174
+ task_type="update_media_buy",
175
+ timestamp=datetime.utcnow().isoformat(),
176
+ )
177
+ )
178
+
179
+ result = await self.adapter.call_tool("update_media_buy", kwargs)
180
+
181
+ self._emit_activity(
182
+ Activity(
183
+ type=ActivityType.PROTOCOL_RESPONSE,
184
+ operation_id=operation_id,
185
+ agent_id=self.agent_config.id,
186
+ task_type="update_media_buy",
187
+ status=result.status,
188
+ timestamp=datetime.utcnow().isoformat(),
189
+ )
190
+ )
191
+
192
+ return result
193
+
194
+ async def sync_creatives(self, **kwargs: Any) -> TaskResult[Any]:
195
+ """Synchronize creatives with the agent."""
196
+ operation_id = create_operation_id()
197
+
198
+ self._emit_activity(
199
+ Activity(
200
+ type=ActivityType.PROTOCOL_REQUEST,
201
+ operation_id=operation_id,
202
+ agent_id=self.agent_config.id,
203
+ task_type="sync_creatives",
204
+ timestamp=datetime.utcnow().isoformat(),
205
+ )
206
+ )
207
+
208
+ result = await self.adapter.call_tool("sync_creatives", kwargs)
209
+
210
+ self._emit_activity(
211
+ Activity(
212
+ type=ActivityType.PROTOCOL_RESPONSE,
213
+ operation_id=operation_id,
214
+ agent_id=self.agent_config.id,
215
+ task_type="sync_creatives",
216
+ status=result.status,
217
+ timestamp=datetime.utcnow().isoformat(),
218
+ )
219
+ )
220
+
221
+ return result
222
+
223
+ async def list_creatives(self, **kwargs: Any) -> TaskResult[Any]:
224
+ """List creatives for a media buy."""
225
+ operation_id = create_operation_id()
226
+
227
+ self._emit_activity(
228
+ Activity(
229
+ type=ActivityType.PROTOCOL_REQUEST,
230
+ operation_id=operation_id,
231
+ agent_id=self.agent_config.id,
232
+ task_type="list_creatives",
233
+ timestamp=datetime.utcnow().isoformat(),
234
+ )
235
+ )
236
+
237
+ result = await self.adapter.call_tool("list_creatives", kwargs)
238
+
239
+ self._emit_activity(
240
+ Activity(
241
+ type=ActivityType.PROTOCOL_RESPONSE,
242
+ operation_id=operation_id,
243
+ agent_id=self.agent_config.id,
244
+ task_type="list_creatives",
245
+ status=result.status,
246
+ timestamp=datetime.utcnow().isoformat(),
247
+ )
248
+ )
249
+
250
+ return result
251
+
252
+ async def get_media_buy_delivery(self, **kwargs: Any) -> TaskResult[Any]:
253
+ """Get delivery metrics for a media buy."""
254
+ operation_id = create_operation_id()
255
+
256
+ self._emit_activity(
257
+ Activity(
258
+ type=ActivityType.PROTOCOL_REQUEST,
259
+ operation_id=operation_id,
260
+ agent_id=self.agent_config.id,
261
+ task_type="get_media_buy_delivery",
262
+ timestamp=datetime.utcnow().isoformat(),
263
+ )
264
+ )
265
+
266
+ result = await self.adapter.call_tool("get_media_buy_delivery", kwargs)
267
+
268
+ self._emit_activity(
269
+ Activity(
270
+ type=ActivityType.PROTOCOL_RESPONSE,
271
+ operation_id=operation_id,
272
+ agent_id=self.agent_config.id,
273
+ task_type="get_media_buy_delivery",
274
+ status=result.status,
275
+ timestamp=datetime.utcnow().isoformat(),
276
+ )
277
+ )
278
+
279
+ return result
280
+
281
+ async def list_authorized_properties(self, **kwargs: Any) -> TaskResult[Any]:
282
+ """List properties this agent is authorized to sell."""
283
+ operation_id = create_operation_id()
284
+
285
+ self._emit_activity(
286
+ Activity(
287
+ type=ActivityType.PROTOCOL_REQUEST,
288
+ operation_id=operation_id,
289
+ agent_id=self.agent_config.id,
290
+ task_type="list_authorized_properties",
291
+ timestamp=datetime.utcnow().isoformat(),
292
+ )
293
+ )
294
+
295
+ result = await self.adapter.call_tool("list_authorized_properties", kwargs)
296
+
297
+ self._emit_activity(
298
+ Activity(
299
+ type=ActivityType.PROTOCOL_RESPONSE,
300
+ operation_id=operation_id,
301
+ agent_id=self.agent_config.id,
302
+ task_type="list_authorized_properties",
303
+ status=result.status,
304
+ timestamp=datetime.utcnow().isoformat(),
305
+ )
306
+ )
307
+
308
+ return result
309
+
310
+ async def get_signals(self, **kwargs: Any) -> TaskResult[Any]:
311
+ """Get available signals for targeting."""
312
+ operation_id = create_operation_id()
313
+
314
+ self._emit_activity(
315
+ Activity(
316
+ type=ActivityType.PROTOCOL_REQUEST,
317
+ operation_id=operation_id,
318
+ agent_id=self.agent_config.id,
319
+ task_type="get_signals",
320
+ timestamp=datetime.utcnow().isoformat(),
321
+ )
322
+ )
323
+
324
+ result = await self.adapter.call_tool("get_signals", kwargs)
325
+
326
+ self._emit_activity(
327
+ Activity(
328
+ type=ActivityType.PROTOCOL_RESPONSE,
329
+ operation_id=operation_id,
330
+ agent_id=self.agent_config.id,
331
+ task_type="get_signals",
332
+ status=result.status,
333
+ timestamp=datetime.utcnow().isoformat(),
334
+ )
335
+ )
336
+
337
+ return result
338
+
339
+ async def activate_signal(self, **kwargs: Any) -> TaskResult[Any]:
340
+ """Activate a signal for use in campaigns."""
341
+ operation_id = create_operation_id()
342
+
343
+ self._emit_activity(
344
+ Activity(
345
+ type=ActivityType.PROTOCOL_REQUEST,
346
+ operation_id=operation_id,
347
+ agent_id=self.agent_config.id,
348
+ task_type="activate_signal",
349
+ timestamp=datetime.utcnow().isoformat(),
350
+ )
351
+ )
352
+
353
+ result = await self.adapter.call_tool("activate_signal", kwargs)
354
+
355
+ self._emit_activity(
356
+ Activity(
357
+ type=ActivityType.PROTOCOL_RESPONSE,
358
+ operation_id=operation_id,
359
+ agent_id=self.agent_config.id,
360
+ task_type="activate_signal",
361
+ status=result.status,
362
+ timestamp=datetime.utcnow().isoformat(),
363
+ )
364
+ )
365
+
366
+ return result
367
+
368
+ async def provide_performance_feedback(self, **kwargs: Any) -> TaskResult[Any]:
369
+ """Provide performance feedback for a campaign."""
370
+ operation_id = create_operation_id()
371
+
372
+ self._emit_activity(
373
+ Activity(
374
+ type=ActivityType.PROTOCOL_REQUEST,
375
+ operation_id=operation_id,
376
+ agent_id=self.agent_config.id,
377
+ task_type="provide_performance_feedback",
378
+ timestamp=datetime.utcnow().isoformat(),
379
+ )
380
+ )
381
+
382
+ result = await self.adapter.call_tool("provide_performance_feedback", kwargs)
383
+
384
+ self._emit_activity(
385
+ Activity(
386
+ type=ActivityType.PROTOCOL_RESPONSE,
387
+ operation_id=operation_id,
388
+ agent_id=self.agent_config.id,
389
+ task_type="provide_performance_feedback",
390
+ status=result.status,
391
+ timestamp=datetime.utcnow().isoformat(),
392
+ )
393
+ )
394
+
395
+ return result
396
+
397
+ async def handle_webhook(
398
+ self,
399
+ payload: dict[str, Any],
400
+ signature: str | None = None,
401
+ ) -> None:
402
+ """
403
+ Handle incoming webhook.
404
+
405
+ Args:
406
+ payload: Webhook payload
407
+ signature: Webhook signature for verification
408
+ """
409
+ # TODO: Implement signature verification
410
+ if self.webhook_secret and signature:
411
+ # Verify signature
412
+ pass
413
+
414
+ operation_id = payload.get("operation_id", "unknown")
415
+ task_type = payload.get("task_type", "unknown")
416
+
417
+ self._emit_activity(
418
+ Activity(
419
+ type=ActivityType.WEBHOOK_RECEIVED,
420
+ operation_id=operation_id,
421
+ agent_id=self.agent_config.id,
422
+ task_type=task_type,
423
+ timestamp=datetime.utcnow().isoformat(),
424
+ metadata={"payload": payload},
425
+ )
426
+ )
427
+
428
+
429
+ class ADCPMultiAgentClient:
430
+ """Client for managing multiple AdCP agents."""
431
+
432
+ def __init__(
433
+ self,
434
+ agents: list[AgentConfig],
435
+ webhook_url_template: str | None = None,
436
+ webhook_secret: str | None = None,
437
+ on_activity: Callable[[Activity], None] | None = None,
438
+ handlers: dict[str, Callable[..., Any]] | None = None,
439
+ ):
440
+ """
441
+ Initialize multi-agent client.
442
+
443
+ Args:
444
+ agents: List of agent configurations
445
+ webhook_url_template: Template for webhook URLs
446
+ webhook_secret: Secret for webhook verification
447
+ on_activity: Callback for activity events
448
+ handlers: Task completion handlers
449
+ """
450
+ self.agents = {
451
+ agent.id: ADCPClient(
452
+ agent,
453
+ webhook_url_template=webhook_url_template,
454
+ webhook_secret=webhook_secret,
455
+ on_activity=on_activity,
456
+ )
457
+ for agent in agents
458
+ }
459
+ self.handlers = handlers or {}
460
+
461
+ def agent(self, agent_id: str) -> ADCPClient:
462
+ """Get client for specific agent."""
463
+ if agent_id not in self.agents:
464
+ raise ValueError(f"Agent not found: {agent_id}")
465
+ return self.agents[agent_id]
466
+
467
+ @property
468
+ def agent_ids(self) -> list[str]:
469
+ """Get list of agent IDs."""
470
+ return list(self.agents.keys())
471
+
472
+ async def get_products(self, brief: str, **kwargs: Any) -> list[TaskResult[Any]]:
473
+ """Execute get_products across all agents in parallel."""
474
+ import asyncio
475
+
476
+ tasks = [agent.get_products(brief, **kwargs) for agent in self.agents.values()]
477
+ return await asyncio.gather(*tasks)
478
+
479
+ @classmethod
480
+ def from_env(cls) -> "ADCPMultiAgentClient":
481
+ """Create client from environment variables."""
482
+ agents_json = os.getenv("ADCP_AGENTS")
483
+ if not agents_json:
484
+ raise ValueError("ADCP_AGENTS environment variable not set")
485
+
486
+ agents_data = json.loads(agents_json)
487
+ agents = [AgentConfig(**agent) for agent in agents_data]
488
+
489
+ return cls(
490
+ agents=agents,
491
+ webhook_url_template=os.getenv("WEBHOOK_URL_TEMPLATE"),
492
+ webhook_secret=os.getenv("WEBHOOK_SECRET"),
493
+ )
@@ -0,0 +1,7 @@
1
+ """Protocol adapters for AdCP."""
2
+
3
+ from adcp.protocols.a2a import A2AAdapter
4
+ from adcp.protocols.base import ProtocolAdapter
5
+ from adcp.protocols.mcp import MCPAdapter
6
+
7
+ __all__ = ["ProtocolAdapter", "A2AAdapter", "MCPAdapter"]
adcp/protocols/a2a.py ADDED
@@ -0,0 +1,159 @@
1
+ """A2A protocol adapter using HTTP client.
2
+
3
+ The official a2a-sdk is primarily for building A2A servers. For client functionality,
4
+ we implement the A2A protocol using HTTP requests as per the A2A specification.
5
+ """
6
+
7
+ from typing import Any
8
+ from uuid import uuid4
9
+
10
+ import httpx
11
+
12
+ from adcp.protocols.base import ProtocolAdapter
13
+ from adcp.types.core import TaskResult, TaskStatus
14
+
15
+
16
+ class A2AAdapter(ProtocolAdapter):
17
+ """Adapter for A2A protocol following the Agent2Agent specification."""
18
+
19
+ async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
20
+ """
21
+ Call a tool using A2A protocol.
22
+
23
+ A2A uses a tasks/send endpoint to initiate tasks. The agent responds with
24
+ task status and may require multiple roundtrips for completion.
25
+ """
26
+ async with httpx.AsyncClient() as client:
27
+ headers = {"Content-Type": "application/json"}
28
+
29
+ if self.agent_config.auth_token:
30
+ headers["Authorization"] = f"Bearer {self.agent_config.auth_token}"
31
+
32
+ # Construct A2A message
33
+ message = {
34
+ "role": "user",
35
+ "parts": [
36
+ {
37
+ "type": "text",
38
+ "text": self._format_tool_request(tool_name, params),
39
+ }
40
+ ],
41
+ }
42
+
43
+ # A2A uses message/send endpoint
44
+ url = f"{self.agent_config.agent_uri}/message/send"
45
+
46
+ request_data = {
47
+ "message": message,
48
+ "context_id": str(uuid4()),
49
+ }
50
+
51
+ try:
52
+ response = await client.post(
53
+ url,
54
+ json=request_data,
55
+ headers=headers,
56
+ timeout=30.0,
57
+ )
58
+ response.raise_for_status()
59
+
60
+ data = response.json()
61
+
62
+ # Parse A2A response format
63
+ # A2A tasks have lifecycle: submitted, working, completed, failed, input-required
64
+ task_status = data.get("task", {}).get("status")
65
+
66
+ if task_status in ("completed", "working"):
67
+ # Extract the result from the response message
68
+ result_data = self._extract_result(data)
69
+
70
+ return TaskResult[Any](
71
+ status=TaskStatus.COMPLETED,
72
+ data=result_data,
73
+ success=True,
74
+ metadata={"task_id": data.get("task", {}).get("id")},
75
+ )
76
+ elif task_status == "failed":
77
+ return TaskResult[Any](
78
+ status=TaskStatus.FAILED,
79
+ error=data.get("message", {})
80
+ .get("parts", [{}])[0]
81
+ .get("text", "Task failed"),
82
+ success=False,
83
+ )
84
+ else:
85
+ # Handle other states (submitted, input-required)
86
+ return TaskResult[Any](
87
+ status=TaskStatus.SUBMITTED,
88
+ data=data,
89
+ success=True,
90
+ metadata={"task_id": data.get("task", {}).get("id")},
91
+ )
92
+
93
+ except httpx.HTTPError as e:
94
+ return TaskResult[Any](
95
+ status=TaskStatus.FAILED,
96
+ error=str(e),
97
+ success=False,
98
+ )
99
+
100
+ def _format_tool_request(self, tool_name: str, params: dict[str, Any]) -> str:
101
+ """Format tool request as natural language for A2A."""
102
+ # For AdCP tools, we format as a structured request
103
+ import json
104
+
105
+ return f"Execute tool: {tool_name}\nParameters: {json.dumps(params, indent=2)}"
106
+
107
+ def _extract_result(self, response_data: dict[str, Any]) -> Any:
108
+ """Extract result data from A2A response."""
109
+ # Try to extract structured data from response
110
+ message = response_data.get("message", {})
111
+ parts = message.get("parts", [])
112
+
113
+ if not parts:
114
+ return response_data
115
+
116
+ # Return the first part's content
117
+ first_part = parts[0]
118
+ if first_part.get("type") == "text":
119
+ # Try to parse as JSON if it looks like structured data
120
+ text = first_part.get("text", "")
121
+ try:
122
+ import json
123
+
124
+ return json.loads(text)
125
+ except (json.JSONDecodeError, ValueError):
126
+ return text
127
+
128
+ return first_part
129
+
130
+ async def list_tools(self) -> list[str]:
131
+ """
132
+ List available tools from A2A agent.
133
+
134
+ Note: A2A doesn't have a standard tools/list endpoint. Agents expose
135
+ their capabilities through the agent card. For AdCP, we rely on the
136
+ standard AdCP tool set.
137
+ """
138
+ async with httpx.AsyncClient() as client:
139
+ headers = {"Content-Type": "application/json"}
140
+
141
+ if self.agent_config.auth_token:
142
+ headers["Authorization"] = f"Bearer {self.agent_config.auth_token}"
143
+
144
+ # Try to fetch agent card (OpenAPI spec)
145
+ url = f"{self.agent_config.agent_uri}/agent-card"
146
+
147
+ try:
148
+ response = await client.get(url, headers=headers, timeout=10.0)
149
+ response.raise_for_status()
150
+
151
+ data = response.json()
152
+
153
+ # Extract skills from agent card
154
+ skills = data.get("skills", [])
155
+ return [skill.get("name", "") for skill in skills if skill.get("name")]
156
+
157
+ except httpx.HTTPError:
158
+ # If agent card is not available, return empty list
159
+ return []
adcp/protocols/base.py ADDED
@@ -0,0 +1,38 @@
1
+ """Base protocol adapter interface."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from adcp.types.core import AgentConfig, TaskResult
7
+
8
+
9
+ class ProtocolAdapter(ABC):
10
+ """Base class for protocol adapters."""
11
+
12
+ def __init__(self, agent_config: AgentConfig):
13
+ """Initialize adapter with agent configuration."""
14
+ self.agent_config = agent_config
15
+
16
+ @abstractmethod
17
+ async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
18
+ """
19
+ Call a tool on the agent.
20
+
21
+ Args:
22
+ tool_name: Name of the tool to call
23
+ params: Tool parameters
24
+
25
+ Returns:
26
+ TaskResult with the response
27
+ """
28
+ pass
29
+
30
+ @abstractmethod
31
+ async def list_tools(self) -> list[str]:
32
+ """
33
+ List available tools from the agent.
34
+
35
+ Returns:
36
+ List of tool names
37
+ """
38
+ pass
adcp/protocols/mcp.py ADDED
@@ -0,0 +1,101 @@
1
+ """MCP protocol adapter using official Python MCP SDK."""
2
+
3
+ from typing import Any
4
+ from urllib.parse import urlparse
5
+
6
+ try:
7
+ from mcp import ClientSession # type: ignore[import-not-found]
8
+ from mcp.client.sse import sse_client # type: ignore[import-not-found]
9
+
10
+ MCP_AVAILABLE = True
11
+ except ImportError:
12
+ MCP_AVAILABLE = False
13
+ ClientSession = None
14
+
15
+ from adcp.protocols.base import ProtocolAdapter
16
+ from adcp.types.core import TaskResult, TaskStatus
17
+
18
+
19
+ class MCPAdapter(ProtocolAdapter):
20
+ """Adapter for MCP protocol using official Python MCP SDK."""
21
+
22
+ def __init__(self, *args: Any, **kwargs: Any):
23
+ super().__init__(*args, **kwargs)
24
+ if not MCP_AVAILABLE:
25
+ raise ImportError(
26
+ "MCP SDK not installed. Install with: pip install mcp (requires Python 3.10+)"
27
+ )
28
+ self._session: Any = None
29
+ self._exit_stack: Any = None
30
+
31
+ async def _get_session(self) -> ClientSession:
32
+ """Get or create MCP client session."""
33
+ if self._session is not None:
34
+ return self._session
35
+
36
+ # Parse the agent URI to determine transport type
37
+ parsed = urlparse(self.agent_config.agent_uri)
38
+
39
+ # Use SSE transport for HTTP/HTTPS endpoints
40
+ if parsed.scheme in ("http", "https"):
41
+ from contextlib import AsyncExitStack
42
+
43
+ self._exit_stack = AsyncExitStack()
44
+
45
+ # Create SSE client with authentication header
46
+ headers = {}
47
+ if self.agent_config.auth_token:
48
+ headers["x-adcp-auth"] = self.agent_config.auth_token
49
+
50
+ read, write = await self._exit_stack.enter_async_context(
51
+ sse_client(self.agent_config.agent_uri, headers=headers)
52
+ )
53
+
54
+ self._session = await self._exit_stack.enter_async_context(ClientSession(read, write))
55
+
56
+ # Initialize the session
57
+ await self._session.initialize()
58
+
59
+ return self._session
60
+ else:
61
+ raise ValueError(f"Unsupported transport scheme: {parsed.scheme}")
62
+
63
+ async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
64
+ """Call a tool using MCP protocol."""
65
+ try:
66
+ session = await self._get_session()
67
+
68
+ # Call the tool using MCP client session
69
+ result = await session.call_tool(tool_name, params)
70
+
71
+ # MCP tool results contain a list of content items
72
+ # For AdCP, we expect the data in the content
73
+ return TaskResult[Any](
74
+ status=TaskStatus.COMPLETED,
75
+ data=result.content,
76
+ success=True,
77
+ )
78
+
79
+ except Exception as e:
80
+ return TaskResult[Any](
81
+ status=TaskStatus.FAILED,
82
+ error=str(e),
83
+ success=False,
84
+ )
85
+
86
+ async def list_tools(self) -> list[str]:
87
+ """List available tools from MCP agent."""
88
+ try:
89
+ session = await self._get_session()
90
+ result = await session.list_tools()
91
+ return [tool.name for tool in result.tools]
92
+ except Exception:
93
+ # Return empty list on error
94
+ return []
95
+
96
+ async def close(self) -> None:
97
+ """Close the MCP session."""
98
+ if self._exit_stack is not None:
99
+ await self._exit_stack.aclose()
100
+ self._exit_stack = None
101
+ self._session = None
adcp/types/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """Type definitions for AdCP client."""
2
+
3
+ from adcp.types.core import (
4
+ Activity,
5
+ ActivityType,
6
+ AgentConfig,
7
+ Protocol,
8
+ TaskResult,
9
+ TaskStatus,
10
+ WebhookMetadata,
11
+ )
12
+
13
+ __all__ = [
14
+ "AgentConfig",
15
+ "Protocol",
16
+ "TaskResult",
17
+ "TaskStatus",
18
+ "WebhookMetadata",
19
+ "Activity",
20
+ "ActivityType",
21
+ ]
adcp/types/core.py ADDED
@@ -0,0 +1,99 @@
1
+ """Core type definitions."""
2
+
3
+ from enum import Enum
4
+ from typing import Any, Generic, Literal, TypeVar
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class Protocol(str, Enum):
10
+ """Supported protocols."""
11
+
12
+ A2A = "a2a"
13
+ MCP = "mcp"
14
+
15
+
16
+ class AgentConfig(BaseModel):
17
+ """Agent configuration."""
18
+
19
+ id: str
20
+ agent_uri: str
21
+ protocol: Protocol
22
+ auth_token: str | None = None
23
+ requires_auth: bool = False
24
+
25
+
26
+ class TaskStatus(str, Enum):
27
+ """Task execution status."""
28
+
29
+ COMPLETED = "completed"
30
+ SUBMITTED = "submitted"
31
+ NEEDS_INPUT = "needs_input"
32
+ FAILED = "failed"
33
+ WORKING = "working"
34
+
35
+
36
+ T = TypeVar("T")
37
+
38
+
39
+ class SubmittedInfo(BaseModel):
40
+ """Information about submitted async task."""
41
+
42
+ webhook_url: str
43
+ operation_id: str
44
+
45
+
46
+ class NeedsInputInfo(BaseModel):
47
+ """Information when agent needs clarification."""
48
+
49
+ message: str
50
+ field: str | None = None
51
+
52
+
53
+ class TaskResult(BaseModel, Generic[T]):
54
+ """Result from task execution."""
55
+
56
+ status: TaskStatus
57
+ data: T | None = None
58
+ submitted: SubmittedInfo | None = None
59
+ needs_input: NeedsInputInfo | None = None
60
+ error: str | None = None
61
+ success: bool = Field(default=True)
62
+ metadata: dict[str, Any] | None = None
63
+
64
+ class Config:
65
+ arbitrary_types_allowed = True
66
+
67
+
68
+ class ActivityType(str, Enum):
69
+ """Types of activity events."""
70
+
71
+ PROTOCOL_REQUEST = "protocol_request"
72
+ PROTOCOL_RESPONSE = "protocol_response"
73
+ WEBHOOK_RECEIVED = "webhook_received"
74
+ HANDLER_CALLED = "handler_called"
75
+ STATUS_CHANGE = "status_change"
76
+
77
+
78
+ class Activity(BaseModel):
79
+ """Activity event for observability."""
80
+
81
+ type: ActivityType
82
+ operation_id: str
83
+ agent_id: str
84
+ task_type: str
85
+ status: TaskStatus | None = None
86
+ timestamp: str
87
+ metadata: dict[str, Any] | None = None
88
+
89
+
90
+ class WebhookMetadata(BaseModel):
91
+ """Metadata passed to webhook handlers."""
92
+
93
+ operation_id: str
94
+ agent_id: str
95
+ task_type: str
96
+ status: TaskStatus
97
+ sequence_number: int | None = None
98
+ notification_type: Literal["scheduled", "final", "delayed"] | None = None
99
+ timestamp: str
adcp/utils/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Utility functions."""
2
+
3
+ from adcp.utils.operation_id import create_operation_id
4
+
5
+ __all__ = ["create_operation_id"]
@@ -0,0 +1,13 @@
1
+ """Operation ID generation utilities."""
2
+
3
+ from uuid import uuid4
4
+
5
+
6
+ def create_operation_id() -> str:
7
+ """
8
+ Generate a unique operation ID.
9
+
10
+ Returns:
11
+ A unique operation ID in the format 'op_{hex}'
12
+ """
13
+ return f"op_{uuid4().hex[:12]}"
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: adcp
3
+ Version: 0.1.2
4
+ Summary: Official Python client for the Ad Context Protocol (AdCP)
5
+ Author-email: AdCP Community <maintainers@adcontextprotocol.org>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/adcontextprotocol/adcp-client-python
8
+ Project-URL: Documentation, https://docs.adcontextprotocol.org
9
+ Project-URL: Repository, https://github.com/adcontextprotocol/adcp-client-python
10
+ Project-URL: Issues, https://github.com/adcontextprotocol/adcp-client-python/issues
11
+ Keywords: adcp,mcp,a2a,protocol,advertising
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: httpx>=0.24.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: typing-extensions>=4.5.0
27
+ Requires-Dist: a2a-sdk>=0.3.0
28
+ Requires-Dist: mcp>=0.9.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
+ Requires-Dist: black>=23.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # adcp - Python Client for Ad Context Protocol
39
+
40
+ [![PyPI version](https://badge.fury.io/py/adcp.svg)](https://badge.fury.io/py/adcp)
41
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
42
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
43
+
44
+ Official Python client for the **Ad Context Protocol (AdCP)**. Build distributed advertising operations that work synchronously OR asynchronously with the same code.
45
+
46
+ ## The Core Concept
47
+
48
+ AdCP operations are **distributed and asynchronous by default**. An agent might:
49
+ - Complete your request **immediately** (synchronous)
50
+ - Need time to process and **send results via webhook** (asynchronous)
51
+ - Ask for **clarifications** before proceeding
52
+ - Send periodic **status updates** as work progresses
53
+
54
+ **Your code stays the same.** You write handlers once, and they work for both sync completions and webhook deliveries.
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install adcp
60
+ ```
61
+
62
+ ## Quick Start: Distributed Operations
63
+
64
+ ```python
65
+ from adcp import ADCPMultiAgentClient
66
+ from adcp.types import AgentConfig
67
+
68
+ # Configure agents and handlers
69
+ client = ADCPMultiAgentClient(
70
+ agents=[
71
+ AgentConfig(
72
+ id="agent_x",
73
+ agent_uri="https://agent-x.com",
74
+ protocol="a2a"
75
+ ),
76
+ AgentConfig(
77
+ id="agent_y",
78
+ agent_uri="https://agent-y.com/mcp/",
79
+ protocol="mcp"
80
+ )
81
+ ],
82
+ # Webhook URL template (macros: {agent_id}, {task_type}, {operation_id})
83
+ webhook_url_template="https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}",
84
+
85
+ # Activity callback - fires for ALL events
86
+ on_activity=lambda activity: print(f"[{activity.type}] {activity.task_type}"),
87
+
88
+ # Status change handlers
89
+ handlers={
90
+ "on_get_products_status_change": lambda response, metadata: (
91
+ db.save_products(metadata.operation_id, response.products)
92
+ if metadata.status == "completed" else None
93
+ )
94
+ }
95
+ )
96
+
97
+ # Execute operation - library handles operation IDs, webhook URLs, context management
98
+ agent = client.agent("agent_x")
99
+ result = await agent.get_products(brief="Coffee brands")
100
+
101
+ # Check result
102
+ if result.status == "completed":
103
+ # Agent completed synchronously!
104
+ print(f"✅ Sync completion: {len(result.data.products)} products")
105
+
106
+ if result.status == "submitted":
107
+ # Agent will send webhook when complete
108
+ print(f"⏳ Async - webhook registered at: {result.submitted.webhook_url}")
109
+ ```
110
+
111
+ ## Features
112
+
113
+ ### Full Protocol Support
114
+ - **A2A Protocol**: Native support for Agent-to-Agent protocol
115
+ - **MCP Protocol**: Native support for Model Context Protocol
116
+ - **Auto-detection**: Automatically detect which protocol an agent uses
117
+
118
+ ### Type Safety
119
+ Full type hints with Pydantic validation:
120
+
121
+ ```python
122
+ result = await agent.get_products(brief="Coffee brands")
123
+ # result: TaskResult[GetProductsResponse]
124
+
125
+ if result.success:
126
+ for product in result.data.products:
127
+ print(product.name, product.price) # Full IDE autocomplete!
128
+ ```
129
+
130
+ ### Multi-Agent Operations
131
+ Execute across multiple agents simultaneously:
132
+
133
+ ```python
134
+ # Parallel execution across all agents
135
+ results = await client.get_products(brief="Coffee brands")
136
+
137
+ for result in results:
138
+ if result.status == "completed":
139
+ print(f"Sync: {len(result.data.products)} products")
140
+ elif result.status == "submitted":
141
+ print(f"Async: webhook to {result.submitted.webhook_url}")
142
+ ```
143
+
144
+ ### Webhook Handling
145
+ Single endpoint handles all webhooks:
146
+
147
+ ```python
148
+ from fastapi import FastAPI, Request
149
+
150
+ app = FastAPI()
151
+
152
+ @app.post("/webhook/{task_type}/{agent_id}/{operation_id}")
153
+ async def webhook(task_type: str, agent_id: str, operation_id: str, request: Request):
154
+ payload = await request.json()
155
+ payload["task_type"] = task_type
156
+ payload["operation_id"] = operation_id
157
+
158
+ # Route to agent client - handlers fire automatically
159
+ agent = client.agent(agent_id)
160
+ await agent.handle_webhook(
161
+ payload,
162
+ request.headers.get("x-adcp-signature")
163
+ )
164
+
165
+ return {"received": True}
166
+ ```
167
+
168
+ ### Security
169
+ Webhook signature verification built-in:
170
+
171
+ ```python
172
+ client = ADCPMultiAgentClient(
173
+ agents=agents,
174
+ webhook_secret=os.getenv("WEBHOOK_SECRET")
175
+ )
176
+ # Signatures verified automatically on handle_webhook()
177
+ ```
178
+
179
+ ## Available Tools
180
+
181
+ All AdCP tools with full type safety:
182
+
183
+ **Media Buy Lifecycle:**
184
+ - `get_products()` - Discover advertising products
185
+ - `list_creative_formats()` - Get supported creative formats
186
+ - `create_media_buy()` - Create new media buy
187
+ - `update_media_buy()` - Update existing media buy
188
+ - `sync_creatives()` - Upload/sync creative assets
189
+ - `list_creatives()` - List creative assets
190
+ - `get_media_buy_delivery()` - Get delivery performance
191
+
192
+ **Audience & Targeting:**
193
+ - `list_authorized_properties()` - Get authorized properties
194
+ - `get_signals()` - Get audience signals
195
+ - `activate_signal()` - Activate audience signals
196
+ - `provide_performance_feedback()` - Send performance feedback
197
+
198
+ ## Property Discovery (AdCP v2.2.0)
199
+
200
+ Build agent registries by discovering properties agents can sell:
201
+
202
+ ```python
203
+ from adcp.discovery import PropertyCrawler, get_property_index
204
+
205
+ # Crawl agents to discover properties
206
+ crawler = PropertyCrawler()
207
+ await crawler.crawl_agents([
208
+ {"agent_url": "https://agent-x.com", "protocol": "a2a"},
209
+ {"agent_url": "https://agent-y.com/mcp/", "protocol": "mcp"}
210
+ ])
211
+
212
+ index = get_property_index()
213
+
214
+ # Query 1: Who can sell this property?
215
+ matches = index.find_agents_for_property("domain", "cnn.com")
216
+
217
+ # Query 2: What can this agent sell?
218
+ auth = index.get_agent_authorizations("https://agent-x.com")
219
+
220
+ # Query 3: Find by tags
221
+ premium = index.find_agents_by_property_tags(["premium", "ctv"])
222
+ ```
223
+
224
+ ## Environment Configuration
225
+
226
+ ```bash
227
+ # .env
228
+ WEBHOOK_URL_TEMPLATE="https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}"
229
+ WEBHOOK_SECRET="your-webhook-secret"
230
+
231
+ ADCP_AGENTS='[
232
+ {
233
+ "id": "agent_x",
234
+ "agent_uri": "https://agent-x.com",
235
+ "protocol": "a2a",
236
+ "auth_token_env": "AGENT_X_TOKEN"
237
+ }
238
+ ]'
239
+ AGENT_X_TOKEN="actual-token-here"
240
+ ```
241
+
242
+ ```python
243
+ # Auto-discover from environment
244
+ client = ADCPMultiAgentClient.from_env()
245
+ ```
246
+
247
+ ## Development
248
+
249
+ ```bash
250
+ # Install with dev dependencies
251
+ pip install -e ".[dev]"
252
+
253
+ # Run tests
254
+ pytest
255
+
256
+ # Type checking
257
+ mypy src/
258
+
259
+ # Format code
260
+ black src/ tests/
261
+ ruff check src/ tests/
262
+ ```
263
+
264
+ ## Contributing
265
+
266
+ Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
267
+
268
+ ## License
269
+
270
+ Apache 2.0 License - see [LICENSE](LICENSE) file for details.
271
+
272
+ ## Support
273
+
274
+ - **Documentation**: [docs.adcontextprotocol.org](https://docs.adcontextprotocol.org)
275
+ - **Issues**: [GitHub Issues](https://github.com/adcontextprotocol/adcp-client-python/issues)
276
+ - **Protocol Spec**: [AdCP Specification](https://github.com/adcontextprotocol/adcp)
@@ -0,0 +1,15 @@
1
+ adcp/__init__.py,sha256=TpXs5-627LCcgm351m2Q1oD5WsFawGoLfe3Fj6Wi4-o,424
2
+ adcp/client.py,sha256=DaAA_5Jxw2N0xjRSdvplJdUU_QHHMJqNzow9QzeagJw,16095
3
+ adcp/protocols/__init__.py,sha256=hAGf5yAfgqpqa_-pMWXL35Bl-Aeca8Zdt_E1uEeI0_M,226
4
+ adcp/protocols/a2a.py,sha256=P9tnUYiVSRa8_OJPsZmr2_r1IasRIGS7z6LR8uTj0lg,5719
5
+ adcp/protocols/base.py,sha256=RhaeFyOj4lxCxGJajUo1ydpNDW2OZPnnMJ6QmodOESw,915
6
+ adcp/protocols/mcp.py,sha256=dLSmDs-qJrn_dcXUthABRblb2ripjNVYWi0wZ0VOOe8,3417
7
+ adcp/types/__init__.py,sha256=LuiNdxbVfS91LIt2DTW7xX0BR-chmqwXz4ScYS6Hd44,334
8
+ adcp/types/core.py,sha256=xybdwh1bsI_q1QiS8DB_6DiFdUNzWsycfVbi7sO2nfo,2119
9
+ adcp/utils/__init__.py,sha256=ME5OVQipWgFdECX0Lur8-ThcVrqHfQOtx27TwjDe9LE,117
10
+ adcp/utils/operation_id.py,sha256=_TcXbaSuaFhnN5F1zECspSJ5n08x_-LIzIWbdlkwzEQ,258
11
+ adcp-0.1.2.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
12
+ adcp-0.1.2.dist-info/METADATA,sha256=pA0Ur9lBAYwdy_u-KsWhwPSYhWNHY_jzoWrPFsl8pGc,8429
13
+ adcp-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ adcp-0.1.2.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
15
+ adcp-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2025 AdCP Community
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1 @@
1
+ adcp