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/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 adcp.types.core import AgentConfig, TaskResult
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
- """Base class for protocol adapters."""
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
- @abstractmethod
19
- async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
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
- Call a tool on the agent.
35
+ Parse raw TaskResult into typed TaskResult.
36
+
37
+ Handles both MCP content arrays and A2A dict responses.
22
38
 
23
39
  Args:
24
- tool_name: Name of the tool to call
25
- params: Tool parameters
40
+ raw_result: Raw TaskResult from adapter
41
+ response_type: Expected Pydantic response type
26
42
 
27
43
  Returns:
28
- TaskResult with the response
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
- async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
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": result.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=result.content,
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