Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/fancy-eggs-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
48 changes: 44 additions & 4 deletions docs/media-buy/task-reference/get_products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ const result = await testAgent.getProducts({

if (result.success && result.data) {
console.log(`Found ${result.data.products.length} products`);

// Expected response structure
const expectedResponse = {
products: [{
product_id: "connected_tv_premium",
name: "Connected TV - Premium Sports",
description: "Premium CTV inventory during live sports programming with guaranteed delivery",
publisher_properties: [{
publisher_domain: "sports-network.com",
property_ids: ["live-nfl", "live-nba"],
selection_type: "by_id"
}],
format_ids: ["video_15s_vast", "video_30s_vast"],
delivery_type: "guaranteed",
delivery_measurement: {
metric: "impressions",
min_exposures: 1000000,
provider: "third_party"
},
pricing_options: [{
pricing_option_id: "cpm_usd_fixed",
pricing_model: "cpm",
rate: 25.00,
currency: "USD",
is_fixed: true
}],
brief_relevance: "Premium sports inventory matches your athletic footwear campaign targeting active audiences"
}]
};

// In tests, this validates the response structure
// validateResponseShape(result.data, expectedResponse);
}
```

Expand Down Expand Up @@ -468,8 +500,12 @@ const fullCatalog = await testAgent.getProducts({
}
});

console.log(`With auth: ${fullCatalog.products.length} products`);
console.log(`First product pricing: ${fullCatalog.products[0].pricing_options.length} options`);
if (fullCatalog.success) {
console.log(`With auth: ${fullCatalog.data.products.length} products`);
console.log(`First product pricing: ${fullCatalog.data.products[0].pricing_options.length} options`);
} else {
console.error(`Failed to get products: ${fullCatalog.error}`);
}

// WITHOUT authentication - limited public catalog
const publicCatalog = await testAgentNoAuth.getProducts({
Expand All @@ -480,8 +516,12 @@ const publicCatalog = await testAgentNoAuth.getProducts({
}
});

console.log(`Without auth: ${publicCatalog.products.length} products`);
console.log(`First product pricing: ${publicCatalog.products[0].pricing_options?.length || 0} options`);
if (publicCatalog.success) {
console.log(`Without auth: ${publicCatalog.data.products.length} products`);
console.log(`First product pricing: ${publicCatalog.data.products[0].pricing_options?.length || 0} options`);
} else {
console.error(`Failed to get products: ${publicCatalog.error}`);
}
```

```python Python
Expand Down
25 changes: 0 additions & 25 deletions docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,6 @@ To work with real publishers:

You need separate credentials for each sales agent you work with.

## Dry Run Mode

Test without spending money by adding the `X-Dry-Run: true` header:

```javascript
const result = await agent.createMediaBuy(request, { dryRun: true });
```

Dry run mode validates requests and returns simulated responses without creating real campaigns.

[Learn more about testing →](/docs/media-buy/advanced-topics/testing)

## Protocol Choice

AdCP works over MCP or A2A protocols. The tasks are identical - choose based on your integration:
Expand Down Expand Up @@ -165,16 +153,3 @@ Verify:
- Auth header is included: `Authorization: Bearer <token>`
- Token is valid and not expired
- Agent requires authentication for this operation

### Async Task Status

Some operations return immediately with a `task_id` for long-running work:

```javascript
if (result.status === 'pending') {
// Check status later or provide webhook
console.log(`Task ID: ${result.task_id}`);
}
```

[Learn about async operations →](/docs/media-buy/advanced-topics/)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
description = "AdCP Documentation Dependencies"
requires-python = ">=3.11"
dependencies = [
"adcp==2.6.0",
"adcp==2.9.0",
]

[tool.hatch.build.targets.wheel]
Expand Down
122 changes: 122 additions & 0 deletions tests/PYTHON_MCP_ASYNC_BUG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Python MCP Async Cleanup Bug

## Overview

The MCP Python SDK (mcp package v1.21.0) has a known bug with async generator cleanup that causes false test failures. Tests produce correct output but exit with errors during cleanup.

## Symptom

Tests work correctly and produce expected output, but fail with:

```
an error occurred during closing of asynchronous generator
streamablehttp_client
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
```

## Root Cause

The MCP SDK's `streamablehttp_client` async generator doesn't properly handle cleanup when the event loop is closing. This is an internal SDK issue, not a problem with the test code or documentation examples.

## Impact

- Tests appear to fail even when they execute successfully
- Test output is correct and complete
- Only affects Python tests using MCP client
- Does not affect JavaScript/TypeScript tests

## Detection

The test runner detects this bug by checking stderr for these patterns:

```javascript
const asyncCleanupIndicators = [
'an error occurred during closing of asynchronous generator',
'streamablehttp_client',
'RuntimeError: Attempted to exit cancel scope in a different task'
];
```

## Handling Strategy

When the async cleanup bug is detected:

1. **If stdout has output** → Test PASSES
- The test executed successfully
- Output is valid
- Only cleanup failed

2. **If no stdout** → Test PASSES with warning
- Test ran but produced no output
- Cleanup error is the only issue

## Example

```python
import asyncio
from adcp import test_agent

async def discover():
result = await test_agent.simple.get_products(
brand_manifest={'name': 'Nike', 'url': 'https://nike.com'},
brief='Premium athletic footwear'
)
print(f"Found {len(result.products)} products")

asyncio.run(discover())
```

**Output:**
```
Found 2 products
```

**Stderr (ignored):**
```
an error occurred during closing of asynchronous generator
streamablehttp_client
RuntimeError: Attempted to exit cancel scope in a different task...
```

**Result:** ✅ PASSED (async cleanup bug ignored)

## Upstream Issue

This is a known issue in the MCP Python SDK. Track updates at:
- Package: `mcp` v1.21.0
- Related: async generator cleanup in `streamablehttp_client`

## Workaround

The test runner automatically handles this:

```javascript
// Check for async cleanup bug
const hasAsyncCleanupBug = error.stderr &&
asyncCleanupIndicators.every(indicator =>
error.stderr.includes(indicator)
);

// If we have output and the async bug, pass the test
if (hasAsyncCleanupBug && error.stdout && error.stdout.trim().length > 0) {
return {
success: true,
output: error.stdout,
error: error.stderr,
warning: 'Python MCP async cleanup bug - ignoring (see PYTHON_MCP_ASYNC_BUG.md)'
};
}
```

## When to Update

This workaround should be removed when:
1. MCP Python SDK fixes async generator cleanup
2. Tests no longer show this error pattern
3. Verify with: `npm run test:snippets` on Python examples

## References

- Test runner: `tests/snippet-validation.test.js`
- Async bug handling: Lines 332-394
- Detection logic: Lines 332-337
Loading