adcp 2.12.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.
- adcp/__init__.py +364 -0
- adcp/__main__.py +440 -0
- adcp/adagents.py +642 -0
- adcp/client.py +1057 -0
- adcp/config.py +82 -0
- adcp/exceptions.py +185 -0
- adcp/protocols/__init__.py +9 -0
- adcp/protocols/a2a.py +484 -0
- adcp/protocols/base.py +190 -0
- adcp/protocols/mcp.py +440 -0
- adcp/py.typed +0 -0
- adcp/simple.py +451 -0
- adcp/testing/__init__.py +53 -0
- adcp/testing/test_helpers.py +311 -0
- adcp/types/__init__.py +561 -0
- adcp/types/_generated.py +237 -0
- adcp/types/aliases.py +748 -0
- adcp/types/base.py +26 -0
- adcp/types/core.py +174 -0
- adcp/types/generated_poc/__init__.py +3 -0
- adcp/types/generated_poc/adagents.py +411 -0
- adcp/types/generated_poc/core/__init__.py +3 -0
- adcp/types/generated_poc/core/activation_key.py +30 -0
- adcp/types/generated_poc/core/assets/__init__.py +3 -0
- adcp/types/generated_poc/core/assets/audio_asset.py +26 -0
- adcp/types/generated_poc/core/assets/css_asset.py +20 -0
- adcp/types/generated_poc/core/assets/daast_asset.py +61 -0
- adcp/types/generated_poc/core/assets/html_asset.py +18 -0
- adcp/types/generated_poc/core/assets/image_asset.py +19 -0
- adcp/types/generated_poc/core/assets/javascript_asset.py +23 -0
- adcp/types/generated_poc/core/assets/text_asset.py +20 -0
- adcp/types/generated_poc/core/assets/url_asset.py +28 -0
- adcp/types/generated_poc/core/assets/vast_asset.py +63 -0
- adcp/types/generated_poc/core/assets/video_asset.py +24 -0
- adcp/types/generated_poc/core/assets/webhook_asset.py +53 -0
- adcp/types/generated_poc/core/brand_manifest.py +201 -0
- adcp/types/generated_poc/core/context.py +15 -0
- adcp/types/generated_poc/core/creative_asset.py +102 -0
- adcp/types/generated_poc/core/creative_assignment.py +27 -0
- adcp/types/generated_poc/core/creative_filters.py +86 -0
- adcp/types/generated_poc/core/creative_manifest.py +68 -0
- adcp/types/generated_poc/core/creative_policy.py +28 -0
- adcp/types/generated_poc/core/delivery_metrics.py +111 -0
- adcp/types/generated_poc/core/deployment.py +78 -0
- adcp/types/generated_poc/core/destination.py +43 -0
- adcp/types/generated_poc/core/dimensions.py +18 -0
- adcp/types/generated_poc/core/error.py +29 -0
- adcp/types/generated_poc/core/ext.py +15 -0
- adcp/types/generated_poc/core/format.py +260 -0
- adcp/types/generated_poc/core/format_id.py +50 -0
- adcp/types/generated_poc/core/frequency_cap.py +19 -0
- adcp/types/generated_poc/core/measurement.py +40 -0
- adcp/types/generated_poc/core/media_buy.py +40 -0
- adcp/types/generated_poc/core/package.py +68 -0
- adcp/types/generated_poc/core/performance_feedback.py +78 -0
- adcp/types/generated_poc/core/placement.py +37 -0
- adcp/types/generated_poc/core/product.py +164 -0
- adcp/types/generated_poc/core/product_filters.py +97 -0
- adcp/types/generated_poc/core/promoted_offerings.py +102 -0
- adcp/types/generated_poc/core/promoted_products.py +38 -0
- adcp/types/generated_poc/core/property.py +64 -0
- adcp/types/generated_poc/core/property_id.py +21 -0
- adcp/types/generated_poc/core/property_tag.py +21 -0
- adcp/types/generated_poc/core/protocol_envelope.py +61 -0
- adcp/types/generated_poc/core/publisher_property_selector.py +75 -0
- adcp/types/generated_poc/core/push_notification_config.py +51 -0
- adcp/types/generated_poc/core/reporting_capabilities.py +51 -0
- adcp/types/generated_poc/core/response.py +24 -0
- adcp/types/generated_poc/core/signal_filters.py +29 -0
- adcp/types/generated_poc/core/sub_asset.py +55 -0
- adcp/types/generated_poc/core/targeting.py +53 -0
- adcp/types/generated_poc/core/webhook_payload.py +96 -0
- adcp/types/generated_poc/creative/__init__.py +3 -0
- adcp/types/generated_poc/creative/list_creative_formats_request.py +88 -0
- adcp/types/generated_poc/creative/list_creative_formats_response.py +55 -0
- adcp/types/generated_poc/creative/preview_creative_request.py +153 -0
- adcp/types/generated_poc/creative/preview_creative_response.py +169 -0
- adcp/types/generated_poc/creative/preview_render.py +152 -0
- adcp/types/generated_poc/enums/__init__.py +3 -0
- adcp/types/generated_poc/enums/adcp_domain.py +12 -0
- adcp/types/generated_poc/enums/asset_content_type.py +23 -0
- adcp/types/generated_poc/enums/auth_scheme.py +12 -0
- adcp/types/generated_poc/enums/available_metric.py +19 -0
- adcp/types/generated_poc/enums/channels.py +19 -0
- adcp/types/generated_poc/enums/co_branding_requirement.py +13 -0
- adcp/types/generated_poc/enums/creative_action.py +15 -0
- adcp/types/generated_poc/enums/creative_agent_capability.py +14 -0
- adcp/types/generated_poc/enums/creative_sort_field.py +16 -0
- adcp/types/generated_poc/enums/creative_status.py +14 -0
- adcp/types/generated_poc/enums/daast_tracking_event.py +21 -0
- adcp/types/generated_poc/enums/daast_version.py +12 -0
- adcp/types/generated_poc/enums/delivery_type.py +12 -0
- adcp/types/generated_poc/enums/dimension_unit.py +14 -0
- adcp/types/generated_poc/enums/feed_format.py +13 -0
- adcp/types/generated_poc/enums/feedback_source.py +14 -0
- adcp/types/generated_poc/enums/format_category.py +17 -0
- adcp/types/generated_poc/enums/format_id_parameter.py +12 -0
- adcp/types/generated_poc/enums/frequency_cap_scope.py +16 -0
- adcp/types/generated_poc/enums/history_entry_type.py +12 -0
- adcp/types/generated_poc/enums/http_method.py +12 -0
- adcp/types/generated_poc/enums/identifier_types.py +29 -0
- adcp/types/generated_poc/enums/javascript_module_type.py +13 -0
- adcp/types/generated_poc/enums/landing_page_requirement.py +13 -0
- adcp/types/generated_poc/enums/markdown_flavor.py +12 -0
- adcp/types/generated_poc/enums/media_buy_status.py +14 -0
- adcp/types/generated_poc/enums/metric_type.py +18 -0
- adcp/types/generated_poc/enums/notification_type.py +14 -0
- adcp/types/generated_poc/enums/pacing.py +13 -0
- adcp/types/generated_poc/enums/preview_output_format.py +12 -0
- adcp/types/generated_poc/enums/pricing_model.py +17 -0
- adcp/types/generated_poc/enums/property_type.py +17 -0
- adcp/types/generated_poc/enums/publisher_identifier_types.py +15 -0
- adcp/types/generated_poc/enums/reporting_frequency.py +13 -0
- adcp/types/generated_poc/enums/signal_catalog_type.py +13 -0
- adcp/types/generated_poc/enums/sort_direction.py +12 -0
- adcp/types/generated_poc/enums/standard_format_ids.py +45 -0
- adcp/types/generated_poc/enums/task_status.py +19 -0
- adcp/types/generated_poc/enums/task_type.py +15 -0
- adcp/types/generated_poc/enums/update_frequency.py +14 -0
- adcp/types/generated_poc/enums/url_asset_type.py +13 -0
- adcp/types/generated_poc/enums/validation_mode.py +12 -0
- adcp/types/generated_poc/enums/vast_tracking_event.py +26 -0
- adcp/types/generated_poc/enums/vast_version.py +15 -0
- adcp/types/generated_poc/enums/webhook_response_type.py +14 -0
- adcp/types/generated_poc/enums/webhook_security_method.py +13 -0
- adcp/types/generated_poc/media_buy/__init__.py +3 -0
- adcp/types/generated_poc/media_buy/build_creative_request.py +41 -0
- adcp/types/generated_poc/media_buy/build_creative_response.py +51 -0
- adcp/types/generated_poc/media_buy/create_media_buy_request.py +94 -0
- adcp/types/generated_poc/media_buy/create_media_buy_response.py +56 -0
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +47 -0
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +235 -0
- adcp/types/generated_poc/media_buy/get_products_request.py +48 -0
- adcp/types/generated_poc/media_buy/get_products_response.py +28 -0
- adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +38 -0
- adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +84 -0
- adcp/types/generated_poc/media_buy/list_creative_formats_request.py +74 -0
- adcp/types/generated_poc/media_buy/list_creative_formats_response.py +56 -0
- adcp/types/generated_poc/media_buy/list_creatives_request.py +76 -0
- adcp/types/generated_poc/media_buy/list_creatives_response.py +214 -0
- adcp/types/generated_poc/media_buy/package_request.py +63 -0
- adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +125 -0
- adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +53 -0
- adcp/types/generated_poc/media_buy/sync_creatives_request.py +63 -0
- adcp/types/generated_poc/media_buy/sync_creatives_response.py +105 -0
- adcp/types/generated_poc/media_buy/update_media_buy_request.py +195 -0
- adcp/types/generated_poc/media_buy/update_media_buy_response.py +55 -0
- adcp/types/generated_poc/pricing_options/__init__.py +3 -0
- adcp/types/generated_poc/pricing_options/cpc_option.py +43 -0
- adcp/types/generated_poc/pricing_options/cpcv_option.py +45 -0
- adcp/types/generated_poc/pricing_options/cpm_auction_option.py +58 -0
- adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +43 -0
- adcp/types/generated_poc/pricing_options/cpp_option.py +64 -0
- adcp/types/generated_poc/pricing_options/cpv_option.py +77 -0
- adcp/types/generated_poc/pricing_options/flat_rate_option.py +93 -0
- adcp/types/generated_poc/pricing_options/vcpm_auction_option.py +61 -0
- adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +47 -0
- adcp/types/generated_poc/protocols/__init__.py +3 -0
- adcp/types/generated_poc/protocols/adcp_extension.py +37 -0
- adcp/types/generated_poc/signals/__init__.py +3 -0
- adcp/types/generated_poc/signals/activate_signal_request.py +32 -0
- adcp/types/generated_poc/signals/activate_signal_response.py +51 -0
- adcp/types/generated_poc/signals/get_signals_request.py +53 -0
- adcp/types/generated_poc/signals/get_signals_response.py +59 -0
- adcp/utils/__init__.py +7 -0
- adcp/utils/operation_id.py +15 -0
- adcp/utils/preview_cache.py +491 -0
- adcp/utils/response_parser.py +171 -0
- adcp/validation.py +172 -0
- adcp-2.12.0.data/data/ADCP_VERSION +1 -0
- adcp-2.12.0.dist-info/METADATA +992 -0
- adcp-2.12.0.dist-info/RECORD +176 -0
- adcp-2.12.0.dist-info/WHEEL +5 -0
- adcp-2.12.0.dist-info/entry_points.txt +2 -0
- adcp-2.12.0.dist-info/licenses/LICENSE +17 -0
- adcp-2.12.0.dist-info/top_level.txt +1 -0
adcp/protocols/mcp.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""MCP protocol adapter using official Python MCP SDK."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import AsyncExitStack
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from mcp import ClientSession
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from mcp import ClientSession as _ClientSession
|
|
19
|
+
from mcp.client.sse import sse_client
|
|
20
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
21
|
+
|
|
22
|
+
MCP_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
MCP_AVAILABLE = False
|
|
25
|
+
|
|
26
|
+
from adcp.exceptions import ADCPConnectionError, ADCPTimeoutError
|
|
27
|
+
from adcp.protocols.base import ProtocolAdapter
|
|
28
|
+
from adcp.types.core import DebugInfo, TaskResult, TaskStatus
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MCPAdapter(ProtocolAdapter):
|
|
32
|
+
"""Adapter for MCP protocol using official Python MCP SDK."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
35
|
+
super().__init__(*args, **kwargs)
|
|
36
|
+
if not MCP_AVAILABLE:
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"MCP SDK not installed. Install with: pip install mcp (requires Python 3.10+)"
|
|
39
|
+
)
|
|
40
|
+
self._session: Any = None
|
|
41
|
+
self._exit_stack: Any = None
|
|
42
|
+
|
|
43
|
+
async def _cleanup_failed_connection(self, context: str) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Clean up resources after a failed connection attempt.
|
|
46
|
+
|
|
47
|
+
This method handles cleanup without raising exceptions to avoid
|
|
48
|
+
masking the original connection error.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
context: Description of the context for logging (e.g., "during connection attempt")
|
|
52
|
+
"""
|
|
53
|
+
if self._exit_stack is not None:
|
|
54
|
+
old_stack = self._exit_stack
|
|
55
|
+
self._exit_stack = None
|
|
56
|
+
self._session = None
|
|
57
|
+
try:
|
|
58
|
+
await old_stack.aclose()
|
|
59
|
+
except asyncio.CancelledError:
|
|
60
|
+
logger.debug(f"MCP session cleanup cancelled {context}")
|
|
61
|
+
except RuntimeError as cleanup_error:
|
|
62
|
+
# Known anyio task group cleanup issue
|
|
63
|
+
error_msg = str(cleanup_error).lower()
|
|
64
|
+
if "cancel scope" in error_msg or "async context" in error_msg:
|
|
65
|
+
logger.debug(f"Ignoring anyio cleanup error {context}: {cleanup_error}")
|
|
66
|
+
else:
|
|
67
|
+
logger.warning(
|
|
68
|
+
f"Unexpected RuntimeError during cleanup {context}: {cleanup_error}"
|
|
69
|
+
)
|
|
70
|
+
except Exception as cleanup_error:
|
|
71
|
+
# Log unexpected cleanup errors but don't raise to preserve original error
|
|
72
|
+
logger.warning(
|
|
73
|
+
f"Unexpected error during cleanup {context}: {cleanup_error}", exc_info=True
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def _get_session(self) -> ClientSession:
|
|
77
|
+
"""
|
|
78
|
+
Get or create MCP client session with URL fallback handling.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ADCPConnectionError: If connection to agent fails
|
|
82
|
+
"""
|
|
83
|
+
if self._session is not None:
|
|
84
|
+
return self._session # type: ignore[no-any-return]
|
|
85
|
+
|
|
86
|
+
logger.debug(f"Creating MCP session for agent {self.agent_config.id}")
|
|
87
|
+
|
|
88
|
+
# Parse the agent URI to determine transport type
|
|
89
|
+
parsed = urlparse(self.agent_config.agent_uri)
|
|
90
|
+
|
|
91
|
+
# Use SSE transport for HTTP/HTTPS endpoints
|
|
92
|
+
if parsed.scheme in ("http", "https"):
|
|
93
|
+
self._exit_stack = AsyncExitStack()
|
|
94
|
+
|
|
95
|
+
# Create SSE client with authentication header
|
|
96
|
+
headers = {}
|
|
97
|
+
if self.agent_config.auth_token:
|
|
98
|
+
# Support custom auth headers and types
|
|
99
|
+
if self.agent_config.auth_type == "bearer":
|
|
100
|
+
headers[self.agent_config.auth_header] = (
|
|
101
|
+
f"Bearer {self.agent_config.auth_token}"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
headers[self.agent_config.auth_header] = self.agent_config.auth_token
|
|
105
|
+
|
|
106
|
+
# Try the user's exact URL first
|
|
107
|
+
urls_to_try = [self.agent_config.agent_uri]
|
|
108
|
+
|
|
109
|
+
# If URL doesn't end with /mcp, also try with /mcp suffix
|
|
110
|
+
if not self.agent_config.agent_uri.rstrip("/").endswith("/mcp"):
|
|
111
|
+
base_uri = self.agent_config.agent_uri.rstrip("/")
|
|
112
|
+
urls_to_try.append(f"{base_uri}/mcp")
|
|
113
|
+
|
|
114
|
+
last_error = None
|
|
115
|
+
for url in urls_to_try:
|
|
116
|
+
try:
|
|
117
|
+
# Choose transport based on configuration
|
|
118
|
+
if self.agent_config.mcp_transport == "streamable_http":
|
|
119
|
+
# Use streamable HTTP transport (newer, bidirectional)
|
|
120
|
+
read, write, _get_session_id = await self._exit_stack.enter_async_context(
|
|
121
|
+
streamablehttp_client(
|
|
122
|
+
url, headers=headers, timeout=self.agent_config.timeout
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
# Use SSE transport (legacy, but widely supported)
|
|
127
|
+
read, write = await self._exit_stack.enter_async_context(
|
|
128
|
+
sse_client(url, headers=headers)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self._session = await self._exit_stack.enter_async_context(
|
|
132
|
+
_ClientSession(read, write)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Initialize the session
|
|
136
|
+
await self._session.initialize()
|
|
137
|
+
|
|
138
|
+
logger.info(
|
|
139
|
+
f"Connected to MCP agent {self.agent_config.id} at {url} "
|
|
140
|
+
f"using {self.agent_config.mcp_transport} transport"
|
|
141
|
+
)
|
|
142
|
+
if url != self.agent_config.agent_uri:
|
|
143
|
+
logger.info(
|
|
144
|
+
f"Note: Connected using fallback URL {url} "
|
|
145
|
+
f"(configured: {self.agent_config.agent_uri})"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return self._session # type: ignore[no-any-return]
|
|
149
|
+
except Exception as e:
|
|
150
|
+
last_error = e
|
|
151
|
+
# Clean up the exit stack on failure to avoid resource leaks
|
|
152
|
+
await self._cleanup_failed_connection("during connection attempt")
|
|
153
|
+
|
|
154
|
+
# If this isn't the last URL to try, create a new exit stack and continue
|
|
155
|
+
if url != urls_to_try[-1]:
|
|
156
|
+
logger.debug(f"Retrying with next URL after error: {last_error}")
|
|
157
|
+
self._exit_stack = AsyncExitStack()
|
|
158
|
+
continue
|
|
159
|
+
# If this was the last URL, raise the error
|
|
160
|
+
logger.error(
|
|
161
|
+
f"Failed to connect to MCP agent {self.agent_config.id} using "
|
|
162
|
+
f"{self.agent_config.mcp_transport} transport. "
|
|
163
|
+
f"Tried URLs: {', '.join(urls_to_try)}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Classify error type for better exception handling
|
|
167
|
+
error_str = str(last_error).lower()
|
|
168
|
+
if "401" in error_str or "403" in error_str or "unauthorized" in error_str:
|
|
169
|
+
from adcp.exceptions import ADCPAuthenticationError
|
|
170
|
+
|
|
171
|
+
raise ADCPAuthenticationError(
|
|
172
|
+
f"Authentication failed: {last_error}",
|
|
173
|
+
agent_id=self.agent_config.id,
|
|
174
|
+
agent_uri=self.agent_config.agent_uri,
|
|
175
|
+
) from last_error
|
|
176
|
+
elif "timeout" in error_str:
|
|
177
|
+
raise ADCPTimeoutError(
|
|
178
|
+
f"Connection timeout: {last_error}",
|
|
179
|
+
agent_id=self.agent_config.id,
|
|
180
|
+
agent_uri=self.agent_config.agent_uri,
|
|
181
|
+
timeout=self.agent_config.timeout,
|
|
182
|
+
) from last_error
|
|
183
|
+
else:
|
|
184
|
+
raise ADCPConnectionError(
|
|
185
|
+
f"Failed to connect: {last_error}",
|
|
186
|
+
agent_id=self.agent_config.id,
|
|
187
|
+
agent_uri=self.agent_config.agent_uri,
|
|
188
|
+
) from last_error
|
|
189
|
+
|
|
190
|
+
# This shouldn't be reached, but just in case
|
|
191
|
+
raise RuntimeError(f"Failed to connect to MCP agent at {self.agent_config.agent_uri}")
|
|
192
|
+
else:
|
|
193
|
+
raise ValueError(f"Unsupported transport scheme: {parsed.scheme}")
|
|
194
|
+
|
|
195
|
+
def _serialize_mcp_content(self, content: list[Any]) -> list[dict[str, Any]]:
|
|
196
|
+
"""
|
|
197
|
+
Convert MCP SDK content objects to plain dicts.
|
|
198
|
+
|
|
199
|
+
The MCP SDK returns Pydantic objects (TextContent, ImageContent, etc.)
|
|
200
|
+
but the rest of the ADCP client expects protocol-agnostic dicts.
|
|
201
|
+
This method handles the translation at the protocol boundary.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
content: List of MCP content items (may be dicts or Pydantic objects)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of plain dicts representing the content
|
|
208
|
+
"""
|
|
209
|
+
result = []
|
|
210
|
+
for item in content:
|
|
211
|
+
# Already a dict, pass through
|
|
212
|
+
if isinstance(item, dict):
|
|
213
|
+
result.append(item)
|
|
214
|
+
# Pydantic v2 model with model_dump()
|
|
215
|
+
elif hasattr(item, "model_dump"):
|
|
216
|
+
result.append(item.model_dump())
|
|
217
|
+
# Pydantic v1 model with dict()
|
|
218
|
+
elif hasattr(item, "dict") and callable(item.dict):
|
|
219
|
+
result.append(item.dict())
|
|
220
|
+
# Fallback: try to access __dict__
|
|
221
|
+
elif hasattr(item, "__dict__"):
|
|
222
|
+
result.append(dict(item.__dict__))
|
|
223
|
+
# Last resort: serialize as unknown type
|
|
224
|
+
else:
|
|
225
|
+
logger.warning(f"Unknown MCP content type: {type(item)}, serializing as string")
|
|
226
|
+
result.append({"type": "unknown", "data": str(item)})
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
async def _call_mcp_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
230
|
+
"""Call a tool using MCP protocol."""
|
|
231
|
+
start_time = time.time() if self.agent_config.debug else None
|
|
232
|
+
debug_info = None
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
session = await self._get_session()
|
|
236
|
+
|
|
237
|
+
if self.agent_config.debug:
|
|
238
|
+
debug_request = {
|
|
239
|
+
"protocol": "MCP",
|
|
240
|
+
"tool": tool_name,
|
|
241
|
+
"params": params,
|
|
242
|
+
"transport": self.agent_config.mcp_transport,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Call the tool using MCP client session
|
|
246
|
+
result = await session.call_tool(tool_name, params)
|
|
247
|
+
|
|
248
|
+
# Check if this is an error response
|
|
249
|
+
is_error = hasattr(result, "isError") and result.isError
|
|
250
|
+
|
|
251
|
+
# Extract human-readable message from content
|
|
252
|
+
message_text = None
|
|
253
|
+
if hasattr(result, "content") and result.content:
|
|
254
|
+
serialized_content = self._serialize_mcp_content(result.content)
|
|
255
|
+
if isinstance(serialized_content, list):
|
|
256
|
+
for item in serialized_content:
|
|
257
|
+
is_text = isinstance(item, dict) and item.get("type") == "text"
|
|
258
|
+
if is_text and item.get("text"):
|
|
259
|
+
message_text = item["text"]
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
# Handle error responses
|
|
263
|
+
if is_error:
|
|
264
|
+
# For error responses, structuredContent is optional
|
|
265
|
+
# Use the error message from content as the error
|
|
266
|
+
error_message = message_text or "Tool execution failed"
|
|
267
|
+
if self.agent_config.debug and start_time:
|
|
268
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
269
|
+
debug_info = DebugInfo(
|
|
270
|
+
request=debug_request,
|
|
271
|
+
response={
|
|
272
|
+
"error": error_message,
|
|
273
|
+
"is_error": True,
|
|
274
|
+
},
|
|
275
|
+
duration_ms=duration_ms,
|
|
276
|
+
)
|
|
277
|
+
return TaskResult[Any](
|
|
278
|
+
status=TaskStatus.FAILED,
|
|
279
|
+
error=error_message,
|
|
280
|
+
success=False,
|
|
281
|
+
debug_info=debug_info,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# For successful responses, structuredContent is required
|
|
285
|
+
if not hasattr(result, "structuredContent") or result.structuredContent is None:
|
|
286
|
+
raise ValueError(
|
|
287
|
+
f"MCP tool {tool_name} did not return structuredContent. "
|
|
288
|
+
f"This SDK requires MCP tools to provide structured responses "
|
|
289
|
+
f"for successful calls. "
|
|
290
|
+
f"Got content: {result.content if hasattr(result, 'content') else 'none'}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Extract the structured data (required for success)
|
|
294
|
+
data_to_return = result.structuredContent
|
|
295
|
+
|
|
296
|
+
if self.agent_config.debug and start_time:
|
|
297
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
298
|
+
debug_info = DebugInfo(
|
|
299
|
+
request=debug_request,
|
|
300
|
+
response={
|
|
301
|
+
"data": data_to_return,
|
|
302
|
+
"message": message_text,
|
|
303
|
+
"is_error": False,
|
|
304
|
+
},
|
|
305
|
+
duration_ms=duration_ms,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Return both the structured data and the human-readable message
|
|
309
|
+
return TaskResult[Any](
|
|
310
|
+
status=TaskStatus.COMPLETED,
|
|
311
|
+
data=data_to_return,
|
|
312
|
+
message=message_text,
|
|
313
|
+
success=True,
|
|
314
|
+
debug_info=debug_info,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
if self.agent_config.debug and start_time:
|
|
319
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
320
|
+
debug_info = DebugInfo(
|
|
321
|
+
request=debug_request if self.agent_config.debug else {},
|
|
322
|
+
response={"error": str(e)},
|
|
323
|
+
duration_ms=duration_ms,
|
|
324
|
+
)
|
|
325
|
+
return TaskResult[Any](
|
|
326
|
+
status=TaskStatus.FAILED,
|
|
327
|
+
error=str(e),
|
|
328
|
+
success=False,
|
|
329
|
+
debug_info=debug_info,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# ========================================================================
|
|
333
|
+
# ADCP Protocol Methods
|
|
334
|
+
# ========================================================================
|
|
335
|
+
|
|
336
|
+
async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
337
|
+
"""Get advertising products."""
|
|
338
|
+
return await self._call_mcp_tool("get_products", params)
|
|
339
|
+
|
|
340
|
+
async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
341
|
+
"""List supported creative formats."""
|
|
342
|
+
return await self._call_mcp_tool("list_creative_formats", params)
|
|
343
|
+
|
|
344
|
+
async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
345
|
+
"""Sync creatives."""
|
|
346
|
+
return await self._call_mcp_tool("sync_creatives", params)
|
|
347
|
+
|
|
348
|
+
async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
349
|
+
"""List creatives."""
|
|
350
|
+
return await self._call_mcp_tool("list_creatives", params)
|
|
351
|
+
|
|
352
|
+
async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
353
|
+
"""Get media buy delivery."""
|
|
354
|
+
return await self._call_mcp_tool("get_media_buy_delivery", params)
|
|
355
|
+
|
|
356
|
+
async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
357
|
+
"""List authorized properties."""
|
|
358
|
+
return await self._call_mcp_tool("list_authorized_properties", params)
|
|
359
|
+
|
|
360
|
+
async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
361
|
+
"""Get signals."""
|
|
362
|
+
return await self._call_mcp_tool("get_signals", params)
|
|
363
|
+
|
|
364
|
+
async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
365
|
+
"""Activate signal."""
|
|
366
|
+
return await self._call_mcp_tool("activate_signal", params)
|
|
367
|
+
|
|
368
|
+
async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
369
|
+
"""Provide performance feedback."""
|
|
370
|
+
return await self._call_mcp_tool("provide_performance_feedback", params)
|
|
371
|
+
|
|
372
|
+
async def preview_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
373
|
+
"""Generate preview URLs for a creative manifest."""
|
|
374
|
+
return await self._call_mcp_tool("preview_creative", params)
|
|
375
|
+
|
|
376
|
+
async def create_media_buy(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
377
|
+
"""Create media buy."""
|
|
378
|
+
return await self._call_mcp_tool("create_media_buy", params)
|
|
379
|
+
|
|
380
|
+
async def update_media_buy(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
381
|
+
"""Update media buy."""
|
|
382
|
+
return await self._call_mcp_tool("update_media_buy", params)
|
|
383
|
+
|
|
384
|
+
async def build_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
385
|
+
"""Build creative."""
|
|
386
|
+
return await self._call_mcp_tool("build_creative", params)
|
|
387
|
+
|
|
388
|
+
async def list_tools(self) -> list[str]:
|
|
389
|
+
"""List available tools from MCP agent."""
|
|
390
|
+
session = await self._get_session()
|
|
391
|
+
result = await session.list_tools()
|
|
392
|
+
return [tool.name for tool in result.tools]
|
|
393
|
+
|
|
394
|
+
async def get_agent_info(self) -> dict[str, Any]:
|
|
395
|
+
"""
|
|
396
|
+
Get agent information including AdCP extension metadata from MCP server.
|
|
397
|
+
|
|
398
|
+
MCP servers may expose metadata through:
|
|
399
|
+
- Server capabilities exposed during initialization
|
|
400
|
+
- extensions.adcp in server info (if supported)
|
|
401
|
+
- Tool list
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Dictionary with agent metadata
|
|
405
|
+
"""
|
|
406
|
+
session = await self._get_session()
|
|
407
|
+
|
|
408
|
+
# Extract basic MCP server info
|
|
409
|
+
info: dict[str, Any] = {
|
|
410
|
+
"name": getattr(session, "server_name", None),
|
|
411
|
+
"version": getattr(session, "server_version", None),
|
|
412
|
+
"protocol": "mcp",
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# Get available tools
|
|
416
|
+
try:
|
|
417
|
+
tools_result = await session.list_tools()
|
|
418
|
+
tool_names = [tool.name for tool in tools_result.tools]
|
|
419
|
+
if tool_names:
|
|
420
|
+
info["tools"] = tool_names
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.warning(f"Failed to list tools for {self.agent_config.id}: {e}")
|
|
423
|
+
|
|
424
|
+
# Try to extract AdCP extension metadata from server capabilities
|
|
425
|
+
# MCP servers may expose this in their initialization response
|
|
426
|
+
if hasattr(session, "_server_capabilities"):
|
|
427
|
+
capabilities = session._server_capabilities
|
|
428
|
+
if isinstance(capabilities, dict):
|
|
429
|
+
extensions = capabilities.get("extensions", {})
|
|
430
|
+
adcp_ext = extensions.get("adcp", {})
|
|
431
|
+
if adcp_ext:
|
|
432
|
+
info["adcp_version"] = adcp_ext.get("adcp_version")
|
|
433
|
+
info["protocols_supported"] = adcp_ext.get("protocols_supported")
|
|
434
|
+
|
|
435
|
+
logger.info(f"Retrieved agent info for {self.agent_config.id}")
|
|
436
|
+
return info
|
|
437
|
+
|
|
438
|
+
async def close(self) -> None:
|
|
439
|
+
"""Close the MCP session and clean up resources."""
|
|
440
|
+
await self._cleanup_failed_connection("during close")
|
adcp/py.typed
ADDED
|
File without changes
|