adcp 1.0.4__tar.gz → 1.0.5__tar.gz
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-1.0.4/src/adcp.egg-info → adcp-1.0.5}/PKG-INFO +1 -1
- {adcp-1.0.4 → adcp-1.0.5}/pyproject.toml +1 -1
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/__init__.py +1 -1
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/__main__.py +22 -21
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/protocols/base.py +3 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/protocols/mcp.py +30 -6
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/types/core.py +1 -0
- {adcp-1.0.4 → adcp-1.0.5/src/adcp.egg-info}/PKG-INFO +1 -1
- {adcp-1.0.4 → adcp-1.0.5}/tests/test_protocols.py +48 -3
- {adcp-1.0.4 → adcp-1.0.5}/LICENSE +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/README.md +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/setup.cfg +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/client.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/config.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/exceptions.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/protocols/__init__.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/protocols/a2a.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/types/__init__.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/types/generated.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/types/tasks.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/utils/__init__.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/utils/operation_id.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp/utils/response_parser.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp.egg-info/SOURCES.txt +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp.egg-info/dependency_links.txt +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp.egg-info/entry_points.txt +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp.egg-info/requires.txt +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/src/adcp.egg-info/top_level.txt +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/tests/test_cli.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/tests/test_client.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/tests/test_code_generation.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/tests/test_format_id_validation.py +0 -0
- {adcp-1.0.4 → adcp-1.0.5}/tests/test_response_parser.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "adcp"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.5"
|
|
8
8
|
description = "Official Python client for the Ad Context Protocol (AdCP)"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "AdCP Community", email = "maintainers@adcontextprotocol.org"}
|
|
@@ -23,37 +23,38 @@ from adcp.types.core import AgentConfig, Protocol
|
|
|
23
23
|
|
|
24
24
|
def print_json(data: Any) -> None:
|
|
25
25
|
"""Print data as JSON."""
|
|
26
|
-
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
# Handle Pydantic models
|
|
29
|
+
if isinstance(data, BaseModel):
|
|
30
|
+
print(data.model_dump_json(indent=2, exclude_none=True))
|
|
31
|
+
else:
|
|
32
|
+
print(json.dumps(data, indent=2, default=str))
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
def print_result(result: Any, json_output: bool = False) -> None:
|
|
30
36
|
"""Print result in formatted or JSON mode."""
|
|
31
37
|
if json_output:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"metadata": result.metadata,
|
|
39
|
-
"debug_info": (
|
|
40
|
-
{
|
|
41
|
-
"request": result.debug_info.request,
|
|
42
|
-
"response": result.debug_info.response,
|
|
43
|
-
"duration_ms": result.debug_info.duration_ms,
|
|
44
|
-
}
|
|
45
|
-
if result.debug_info
|
|
46
|
-
else None
|
|
47
|
-
),
|
|
48
|
-
}
|
|
49
|
-
)
|
|
38
|
+
# Match JavaScript client: output just the data for scripting
|
|
39
|
+
if result.success and result.data:
|
|
40
|
+
print_json(result.data)
|
|
41
|
+
else:
|
|
42
|
+
# On error, output error info
|
|
43
|
+
print_json({"error": result.error, "success": False})
|
|
50
44
|
else:
|
|
51
|
-
|
|
45
|
+
# Pretty output with message and data (like JavaScript client)
|
|
52
46
|
if result.success:
|
|
47
|
+
print("\nSUCCESS\n")
|
|
48
|
+
# Show protocol message if available
|
|
49
|
+
if hasattr(result, "message") and result.message:
|
|
50
|
+
print("Protocol Message:")
|
|
51
|
+
print(result.message)
|
|
52
|
+
print()
|
|
53
53
|
if result.data:
|
|
54
|
-
print("
|
|
54
|
+
print("Response:")
|
|
55
55
|
print_json(result.data)
|
|
56
56
|
else:
|
|
57
|
+
print("\nFAILED\n")
|
|
57
58
|
print(f"Error: {result.error}")
|
|
58
59
|
|
|
59
60
|
|
|
@@ -49,6 +49,7 @@ class ProtocolAdapter(ABC):
|
|
|
49
49
|
return TaskResult[T](
|
|
50
50
|
status=raw_result.status,
|
|
51
51
|
data=None,
|
|
52
|
+
message=raw_result.message,
|
|
52
53
|
success=False,
|
|
53
54
|
error=raw_result.error or "No data returned from adapter",
|
|
54
55
|
metadata=raw_result.metadata,
|
|
@@ -66,6 +67,7 @@ class ProtocolAdapter(ABC):
|
|
|
66
67
|
return TaskResult[T](
|
|
67
68
|
status=raw_result.status,
|
|
68
69
|
data=parsed_data,
|
|
70
|
+
message=raw_result.message, # Preserve human-readable message from protocol
|
|
69
71
|
success=raw_result.success,
|
|
70
72
|
error=raw_result.error,
|
|
71
73
|
metadata=raw_result.metadata,
|
|
@@ -76,6 +78,7 @@ class ProtocolAdapter(ABC):
|
|
|
76
78
|
return TaskResult[T](
|
|
77
79
|
status=TaskStatus.FAILED,
|
|
78
80
|
error=f"Failed to parse response: {e}",
|
|
81
|
+
message=raw_result.message,
|
|
79
82
|
success=False,
|
|
80
83
|
debug_info=raw_result.debug_info,
|
|
81
84
|
)
|
|
@@ -239,25 +239,49 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
239
239
|
# Call the tool using MCP client session
|
|
240
240
|
result = await session.call_tool(tool_name, params)
|
|
241
241
|
|
|
242
|
-
#
|
|
243
|
-
|
|
242
|
+
# This SDK requires MCP tools to return structuredContent
|
|
243
|
+
# The content field may contain human-readable messages but the actual
|
|
244
|
+
# response data must be in structuredContent
|
|
245
|
+
if not hasattr(result, "structuredContent") or result.structuredContent is None:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"MCP tool {tool_name} did not return structuredContent. "
|
|
248
|
+
f"This SDK requires MCP tools to provide structured responses. "
|
|
249
|
+
f"Got content: {result.content if hasattr(result, 'content') else 'none'}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Extract the structured data (required)
|
|
253
|
+
data_to_return = result.structuredContent
|
|
254
|
+
|
|
255
|
+
# Extract human-readable message from content (optional)
|
|
256
|
+
# This is typically a status message like "Found 42 creative formats"
|
|
257
|
+
message_text = None
|
|
258
|
+
if hasattr(result, "content") and result.content:
|
|
259
|
+
# Serialize content using the same method used for backward compatibility
|
|
260
|
+
serialized_content = self._serialize_mcp_content(result.content)
|
|
261
|
+
if isinstance(serialized_content, list):
|
|
262
|
+
for item in serialized_content:
|
|
263
|
+
is_text = isinstance(item, dict) and item.get("type") == "text"
|
|
264
|
+
if is_text and item.get("text"):
|
|
265
|
+
message_text = item["text"]
|
|
266
|
+
break
|
|
244
267
|
|
|
245
268
|
if self.agent_config.debug and start_time:
|
|
246
269
|
duration_ms = (time.time() - start_time) * 1000
|
|
247
270
|
debug_info = DebugInfo(
|
|
248
271
|
request=debug_request,
|
|
249
272
|
response={
|
|
250
|
-
"
|
|
273
|
+
"data": data_to_return,
|
|
274
|
+
"message": message_text,
|
|
251
275
|
"is_error": result.isError if hasattr(result, "isError") else False,
|
|
252
276
|
},
|
|
253
277
|
duration_ms=duration_ms,
|
|
254
278
|
)
|
|
255
279
|
|
|
256
|
-
#
|
|
257
|
-
# For AdCP, we expect the data in the content
|
|
280
|
+
# Return both the structured data and the human-readable message
|
|
258
281
|
return TaskResult[Any](
|
|
259
282
|
status=TaskStatus.COMPLETED,
|
|
260
|
-
data=
|
|
283
|
+
data=data_to_return,
|
|
284
|
+
message=message_text,
|
|
261
285
|
success=True,
|
|
262
286
|
debug_info=debug_info,
|
|
263
287
|
)
|
|
@@ -127,6 +127,7 @@ class TaskResult(BaseModel, Generic[T]):
|
|
|
127
127
|
|
|
128
128
|
status: TaskStatus
|
|
129
129
|
data: T | None = None
|
|
130
|
+
message: str | None = None # Human-readable message from agent (e.g., MCP content text)
|
|
130
131
|
submitted: SubmittedInfo | None = None
|
|
131
132
|
needs_input: NeedsInputInfo | None = None
|
|
132
133
|
error: str | None = None
|
|
@@ -155,13 +155,15 @@ class TestMCPAdapter:
|
|
|
155
155
|
|
|
156
156
|
@pytest.mark.asyncio
|
|
157
157
|
async def test_call_tool_success(self, mcp_config):
|
|
158
|
-
"""Test successful tool call via MCP."""
|
|
158
|
+
"""Test successful tool call via MCP with proper structuredContent."""
|
|
159
159
|
adapter = MCPAdapter(mcp_config)
|
|
160
160
|
|
|
161
161
|
# Mock MCP session
|
|
162
162
|
mock_session = AsyncMock()
|
|
163
163
|
mock_result = MagicMock()
|
|
164
|
+
# Mock MCP result with structuredContent (required for AdCP)
|
|
164
165
|
mock_result.content = [{"type": "text", "text": "Success"}]
|
|
166
|
+
mock_result.structuredContent = {"products": [{"id": "prod1"}]}
|
|
165
167
|
mock_session.call_tool.return_value = mock_result
|
|
166
168
|
|
|
167
169
|
with patch.object(adapter, "_get_session", return_value=mock_session):
|
|
@@ -175,10 +177,53 @@ class TestMCPAdapter:
|
|
|
175
177
|
assert call_args[0][0] == "get_products"
|
|
176
178
|
assert call_args[0][1] == {"brief": "test"}
|
|
177
179
|
|
|
178
|
-
# Verify result
|
|
180
|
+
# Verify result uses structuredContent
|
|
181
|
+
assert result.success is True
|
|
182
|
+
assert result.status == TaskStatus.COMPLETED
|
|
183
|
+
assert result.data == {"products": [{"id": "prod1"}]}
|
|
184
|
+
|
|
185
|
+
@pytest.mark.asyncio
|
|
186
|
+
async def test_call_tool_with_structured_content(self, mcp_config):
|
|
187
|
+
"""Test successful tool call via MCP with structuredContent field."""
|
|
188
|
+
adapter = MCPAdapter(mcp_config)
|
|
189
|
+
|
|
190
|
+
# Mock MCP session
|
|
191
|
+
mock_session = AsyncMock()
|
|
192
|
+
mock_result = MagicMock()
|
|
193
|
+
# Mock MCP result with structuredContent (preferred over content)
|
|
194
|
+
mock_result.content = [{"type": "text", "text": "Found 42 creative formats"}]
|
|
195
|
+
mock_result.structuredContent = {"formats": [{"id": "format1"}, {"id": "format2"}]}
|
|
196
|
+
mock_session.call_tool.return_value = mock_result
|
|
197
|
+
|
|
198
|
+
with patch.object(adapter, "_get_session", return_value=mock_session):
|
|
199
|
+
result = await adapter._call_mcp_tool("list_creative_formats", {})
|
|
200
|
+
|
|
201
|
+
# Verify result uses structuredContent, not content array
|
|
179
202
|
assert result.success is True
|
|
180
203
|
assert result.status == TaskStatus.COMPLETED
|
|
181
|
-
assert result.data == [{"
|
|
204
|
+
assert result.data == {"formats": [{"id": "format1"}, {"id": "format2"}]}
|
|
205
|
+
# Verify message extraction from content array
|
|
206
|
+
assert result.message == "Found 42 creative formats"
|
|
207
|
+
|
|
208
|
+
@pytest.mark.asyncio
|
|
209
|
+
async def test_call_tool_missing_structured_content(self, mcp_config):
|
|
210
|
+
"""Test tool call fails when structuredContent is missing."""
|
|
211
|
+
adapter = MCPAdapter(mcp_config)
|
|
212
|
+
|
|
213
|
+
mock_session = AsyncMock()
|
|
214
|
+
mock_result = MagicMock()
|
|
215
|
+
# Mock MCP result WITHOUT structuredContent (invalid for AdCP)
|
|
216
|
+
mock_result.content = [{"type": "text", "text": "Success"}]
|
|
217
|
+
mock_result.structuredContent = None
|
|
218
|
+
mock_session.call_tool.return_value = mock_result
|
|
219
|
+
|
|
220
|
+
with patch.object(adapter, "_get_session", return_value=mock_session):
|
|
221
|
+
result = await adapter._call_mcp_tool("get_products", {"brief": "test"})
|
|
222
|
+
|
|
223
|
+
# Verify error handling for missing structuredContent
|
|
224
|
+
assert result.success is False
|
|
225
|
+
assert result.status == TaskStatus.FAILED
|
|
226
|
+
assert "did not return structuredContent" in result.error
|
|
182
227
|
|
|
183
228
|
@pytest.mark.asyncio
|
|
184
229
|
async def test_call_tool_error(self, mcp_config):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|