adcp 1.0.2__py3-none-any.whl → 1.0.4__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 +1 -1
- adcp/__main__.py +107 -8
- adcp/client.py +128 -73
- adcp/protocols/a2a.py +46 -4
- adcp/protocols/base.py +110 -9
- adcp/protocols/mcp.py +80 -3
- adcp/types/generated.py +19 -2
- adcp/types/tasks.py +313 -83
- adcp/utils/response_parser.py +122 -0
- {adcp-1.0.2.dist-info → adcp-1.0.4.dist-info}/METADATA +1 -1
- adcp-1.0.4.dist-info/RECORD +22 -0
- adcp-1.0.2.dist-info/RECORD +0 -21
- {adcp-1.0.2.dist-info → adcp-1.0.4.dist-info}/WHEEL +0 -0
- {adcp-1.0.2.dist-info → adcp-1.0.4.dist-info}/entry_points.txt +0 -0
- {adcp-1.0.2.dist-info → adcp-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {adcp-1.0.2.dist-info → adcp-1.0.4.dist-info}/top_level.txt +0 -0
adcp/protocols/base.py
CHANGED
|
@@ -3,30 +3,131 @@ from __future__ import annotations
|
|
|
3
3
|
"""Base protocol adapter interface."""
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from adcp.types.core import AgentConfig, TaskResult, TaskStatus
|
|
11
|
+
from adcp.utils.response_parser import parse_json_or_text, parse_mcp_content
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T", bound=BaseModel)
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class ProtocolAdapter(ABC):
|
|
12
|
-
"""
|
|
17
|
+
"""
|
|
18
|
+
Base class for protocol adapters.
|
|
19
|
+
|
|
20
|
+
Each adapter implements the ADCP protocol methods and handles
|
|
21
|
+
protocol-specific translation (MCP/A2A) while returning properly
|
|
22
|
+
typed responses.
|
|
23
|
+
"""
|
|
13
24
|
|
|
14
25
|
def __init__(self, agent_config: AgentConfig):
|
|
15
26
|
"""Initialize adapter with agent configuration."""
|
|
16
27
|
self.agent_config = agent_config
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
# ========================================================================
|
|
30
|
+
# Helper methods for response parsing
|
|
31
|
+
# ========================================================================
|
|
32
|
+
|
|
33
|
+
def _parse_response(self, raw_result: TaskResult[Any], response_type: type[T]) -> TaskResult[T]:
|
|
20
34
|
"""
|
|
21
|
-
|
|
35
|
+
Parse raw TaskResult into typed TaskResult.
|
|
36
|
+
|
|
37
|
+
Handles both MCP content arrays and A2A dict responses.
|
|
22
38
|
|
|
23
39
|
Args:
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
raw_result: Raw TaskResult from adapter
|
|
41
|
+
response_type: Expected Pydantic response type
|
|
26
42
|
|
|
27
43
|
Returns:
|
|
28
|
-
TaskResult
|
|
44
|
+
Typed TaskResult
|
|
29
45
|
"""
|
|
46
|
+
# Handle failed results or missing data
|
|
47
|
+
if not raw_result.success or raw_result.data is None:
|
|
48
|
+
# Explicitly construct typed result to satisfy type checker
|
|
49
|
+
return TaskResult[T](
|
|
50
|
+
status=raw_result.status,
|
|
51
|
+
data=None,
|
|
52
|
+
success=False,
|
|
53
|
+
error=raw_result.error or "No data returned from adapter",
|
|
54
|
+
metadata=raw_result.metadata,
|
|
55
|
+
debug_info=raw_result.debug_info,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Handle MCP content arrays
|
|
60
|
+
if isinstance(raw_result.data, list):
|
|
61
|
+
parsed_data = parse_mcp_content(raw_result.data, response_type)
|
|
62
|
+
else:
|
|
63
|
+
# Handle A2A or direct responses
|
|
64
|
+
parsed_data = parse_json_or_text(raw_result.data, response_type)
|
|
65
|
+
|
|
66
|
+
return TaskResult[T](
|
|
67
|
+
status=raw_result.status,
|
|
68
|
+
data=parsed_data,
|
|
69
|
+
success=raw_result.success,
|
|
70
|
+
error=raw_result.error,
|
|
71
|
+
metadata=raw_result.metadata,
|
|
72
|
+
debug_info=raw_result.debug_info,
|
|
73
|
+
)
|
|
74
|
+
except ValueError as e:
|
|
75
|
+
# Parsing failed - return error result
|
|
76
|
+
return TaskResult[T](
|
|
77
|
+
status=TaskStatus.FAILED,
|
|
78
|
+
error=f"Failed to parse response: {e}",
|
|
79
|
+
success=False,
|
|
80
|
+
debug_info=raw_result.debug_info,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# ========================================================================
|
|
84
|
+
# ADCP Protocol Methods - Type-safe, spec-aligned interface
|
|
85
|
+
# Each adapter MUST implement these methods explicitly.
|
|
86
|
+
# ========================================================================
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
90
|
+
"""Get advertising products."""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
95
|
+
"""List supported creative formats."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
100
|
+
"""Sync creatives."""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
105
|
+
"""List creatives."""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
110
|
+
"""Get media buy delivery."""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
115
|
+
"""List authorized properties."""
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
120
|
+
"""Get signals."""
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
125
|
+
"""Activate signal."""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
130
|
+
"""Provide performance feedback."""
|
|
30
131
|
pass
|
|
31
132
|
|
|
32
133
|
@abstractmethod
|
adcp/protocols/mcp.py
CHANGED
|
@@ -186,7 +186,41 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
186
186
|
else:
|
|
187
187
|
raise ValueError(f"Unsupported transport scheme: {parsed.scheme}")
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
def _serialize_mcp_content(self, content: list[Any]) -> list[dict[str, Any]]:
|
|
190
|
+
"""
|
|
191
|
+
Convert MCP SDK content objects to plain dicts.
|
|
192
|
+
|
|
193
|
+
The MCP SDK returns Pydantic objects (TextContent, ImageContent, etc.)
|
|
194
|
+
but the rest of the ADCP client expects protocol-agnostic dicts.
|
|
195
|
+
This method handles the translation at the protocol boundary.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
content: List of MCP content items (may be dicts or Pydantic objects)
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of plain dicts representing the content
|
|
202
|
+
"""
|
|
203
|
+
result = []
|
|
204
|
+
for item in content:
|
|
205
|
+
# Already a dict, pass through
|
|
206
|
+
if isinstance(item, dict):
|
|
207
|
+
result.append(item)
|
|
208
|
+
# Pydantic v2 model with model_dump()
|
|
209
|
+
elif hasattr(item, "model_dump"):
|
|
210
|
+
result.append(item.model_dump())
|
|
211
|
+
# Pydantic v1 model with dict()
|
|
212
|
+
elif hasattr(item, "dict") and callable(item.dict):
|
|
213
|
+
result.append(item.dict())
|
|
214
|
+
# Fallback: try to access __dict__
|
|
215
|
+
elif hasattr(item, "__dict__"):
|
|
216
|
+
result.append(dict(item.__dict__))
|
|
217
|
+
# Last resort: serialize as unknown type
|
|
218
|
+
else:
|
|
219
|
+
logger.warning(f"Unknown MCP content type: {type(item)}, serializing as string")
|
|
220
|
+
result.append({"type": "unknown", "data": str(item)})
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
async def _call_mcp_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
190
224
|
"""Call a tool using MCP protocol."""
|
|
191
225
|
start_time = time.time() if self.agent_config.debug else None
|
|
192
226
|
debug_info = None
|
|
@@ -205,12 +239,15 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
205
239
|
# Call the tool using MCP client session
|
|
206
240
|
result = await session.call_tool(tool_name, params)
|
|
207
241
|
|
|
242
|
+
# Serialize MCP SDK types to plain dicts at protocol boundary
|
|
243
|
+
serialized_content = self._serialize_mcp_content(result.content)
|
|
244
|
+
|
|
208
245
|
if self.agent_config.debug and start_time:
|
|
209
246
|
duration_ms = (time.time() - start_time) * 1000
|
|
210
247
|
debug_info = DebugInfo(
|
|
211
248
|
request=debug_request,
|
|
212
249
|
response={
|
|
213
|
-
"content":
|
|
250
|
+
"content": serialized_content,
|
|
214
251
|
"is_error": result.isError if hasattr(result, "isError") else False,
|
|
215
252
|
},
|
|
216
253
|
duration_ms=duration_ms,
|
|
@@ -220,7 +257,7 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
220
257
|
# For AdCP, we expect the data in the content
|
|
221
258
|
return TaskResult[Any](
|
|
222
259
|
status=TaskStatus.COMPLETED,
|
|
223
|
-
data=
|
|
260
|
+
data=serialized_content,
|
|
224
261
|
success=True,
|
|
225
262
|
debug_info=debug_info,
|
|
226
263
|
)
|
|
@@ -240,6 +277,46 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
240
277
|
debug_info=debug_info,
|
|
241
278
|
)
|
|
242
279
|
|
|
280
|
+
# ========================================================================
|
|
281
|
+
# ADCP Protocol Methods
|
|
282
|
+
# ========================================================================
|
|
283
|
+
|
|
284
|
+
async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
285
|
+
"""Get advertising products."""
|
|
286
|
+
return await self._call_mcp_tool("get_products", params)
|
|
287
|
+
|
|
288
|
+
async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
289
|
+
"""List supported creative formats."""
|
|
290
|
+
return await self._call_mcp_tool("list_creative_formats", params)
|
|
291
|
+
|
|
292
|
+
async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
293
|
+
"""Sync creatives."""
|
|
294
|
+
return await self._call_mcp_tool("sync_creatives", params)
|
|
295
|
+
|
|
296
|
+
async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
297
|
+
"""List creatives."""
|
|
298
|
+
return await self._call_mcp_tool("list_creatives", params)
|
|
299
|
+
|
|
300
|
+
async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
301
|
+
"""Get media buy delivery."""
|
|
302
|
+
return await self._call_mcp_tool("get_media_buy_delivery", params)
|
|
303
|
+
|
|
304
|
+
async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
305
|
+
"""List authorized properties."""
|
|
306
|
+
return await self._call_mcp_tool("list_authorized_properties", params)
|
|
307
|
+
|
|
308
|
+
async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
309
|
+
"""Get signals."""
|
|
310
|
+
return await self._call_mcp_tool("get_signals", params)
|
|
311
|
+
|
|
312
|
+
async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
313
|
+
"""Activate signal."""
|
|
314
|
+
return await self._call_mcp_tool("activate_signal", params)
|
|
315
|
+
|
|
316
|
+
async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
317
|
+
"""Provide performance feedback."""
|
|
318
|
+
return await self._call_mcp_tool("provide_performance_feedback", params)
|
|
319
|
+
|
|
243
320
|
async def list_tools(self) -> list[str]:
|
|
244
321
|
"""List available tools from MCP agent."""
|
|
245
322
|
session = await self._get_session()
|
adcp/types/generated.py
CHANGED
|
@@ -11,9 +11,10 @@ To regenerate:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import re
|
|
14
15
|
from typing import Any, Literal
|
|
15
16
|
|
|
16
|
-
from pydantic import BaseModel, Field
|
|
17
|
+
from pydantic import BaseModel, Field, field_validator
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
# ============================================================================
|
|
@@ -22,7 +23,6 @@ from pydantic import BaseModel, Field
|
|
|
22
23
|
|
|
23
24
|
# These types are referenced in schemas but don't have schema files
|
|
24
25
|
# Defining them as type aliases to maintain type safety
|
|
25
|
-
FormatId = str
|
|
26
26
|
PackageRequest = dict[str, Any]
|
|
27
27
|
PushNotificationConfig = dict[str, Any]
|
|
28
28
|
ReportingCapabilities = dict[str, Any]
|
|
@@ -32,6 +32,23 @@ ReportingCapabilities = dict[str, Any]
|
|
|
32
32
|
# CORE DOMAIN TYPES
|
|
33
33
|
# ============================================================================
|
|
34
34
|
|
|
35
|
+
class FormatId(BaseModel):
|
|
36
|
+
"""Structured format identifier with agent URL and format name"""
|
|
37
|
+
|
|
38
|
+
agent_url: str = Field(description="URL of the agent that defines this format (e.g., 'https://creatives.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats)")
|
|
39
|
+
id: str = Field(description="Format identifier within the agent's namespace (e.g., 'display_300x250', 'video_standard_30s')")
|
|
40
|
+
|
|
41
|
+
@field_validator("id")
|
|
42
|
+
@classmethod
|
|
43
|
+
def validate_id_pattern(cls, v: str) -> str:
|
|
44
|
+
"""Validate format ID contains only alphanumeric characters, hyphens, and underscores."""
|
|
45
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Invalid format ID: {v!r}. Must contain only alphanumeric characters, hyphens, and underscores"
|
|
48
|
+
)
|
|
49
|
+
return v
|
|
50
|
+
|
|
51
|
+
|
|
35
52
|
class Product(BaseModel):
|
|
36
53
|
"""Represents available advertising inventory"""
|
|
37
54
|
|