Skip to content
Open
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
141 changes: 141 additions & 0 deletions docs/integration/deals-api-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# deals-api-mcp Integration

The seller agent can distribute deals through [deals-api-mcp](https://github.com/IABTechLab/deals-api-mcp) — an MCP server that implements the IAB Deal Sync API v1.0. The seller agent talks to it over **MCP Streamable HTTP**.

## How It Fits Together

```
seller-agent (Python)
└── DealsAPIMCPClient ← DealSyncClient implementation
└── MCP Transport ← MCP Streamable HTTP transport
└── deals-api-mcp ← MCP server (TypeScript, SQLite)
```

`DealsAPIMCPClient` implements the `DealSyncClient` interface and is registered in the deal-sync registry (a peer of the SSP registry). All operations are exposed through the seller agent's standard REST API — no direct access to the connector is needed.

## Prerequisites

Start `deals-api-mcp` in HTTP mode before the seller agent connects:

```bash
MCP_TRANSPORT=http MCP_PORT=3100 NODE_ENV=demo node dist/index.js
```

The server listens at `http://localhost:3100/mcp`. See [deals-api-mcp HTTP Streamable Transport](https://github.com/IABTechLab/deals-api-mcp#http-streamable-transport) for full env var details.

## Configuration

Enable the connector via environment variables:

```bash
DEAL_SYNC_CONNECTORS=deals_api_mcp
DEALS_API_MCP_URL=http://localhost:3100/mcp
DEALS_API_MCP_SELLER_ORIGIN=publisher.example.com
DEALS_API_MCP_KEY= # omit for NODE_ENV=demo
```

| Variable | Description |
|----------|-------------|
| `DEAL_SYNC_CONNECTORS` | Comma-separated list of active deal-sync providers. Include `deals_api_mcp` to enable. |
| `DEALS_API_MCP_URL` | Full URL of the deals-api-mcp `/mcp` endpoint |
| `DEALS_API_MCP_SELLER_ORIGIN` | Publisher domain stamped on every created deal |
| `DEALS_API_MCP_KEY` | Auth key. Required when `NODE_ENV=production`; omit in demo mode |

## Operations

### Distribute (Create) a Deal

```bash
curl -X POST http://localhost:8000/api/v1/deals/distribute \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"deal_id": "your-internal-deal-id",
"ssp_name": "deals_api_mcp",
"name": "Q3 Sports Video",
"advertiser": "Acme Corp",
"cpm": 25.00,
"deal_type": "PMP",
"start_date": "2026-07-01T00:00:00Z",
"end_date": "2026-09-30T00:00:00Z",
"buyer_seat_ids": ["seat-001"]
}'
```

Response:

```json
{
"deal_id": "8b3f9cfe-7716-48e0-94bf-42fe44595928",
"external_deal_id": "IAB-Q3SPORTS001",
"channel": "deal_sync",
"provider": "deals_api_mcp",
"provider_name": "IAB Deals MCP",
"status": "created",
"deal": { "name": "Q3 Sports Video", "cpm": 25.00, "external_deal_id": "IAB-Q3SPORTS001", ... }
}
```

`deal_id` is the internal UUID (e.g. `8b3f9cfe-...`) required by MCP tools. `external_deal_id` is the OpenRTB / IAB deal ID (e.g. `"IAB-..."`) that DSPs need for activation — it is also present on the nested `deal` object and in `DEAL_SYNCED` event payloads.

Omit `ssp_name` to let the seller agent route based on `SSP_ROUTING_RULES`.

### Troubleshoot a Deal

```bash
curl "http://localhost:8000/api/v1/deals/8b3f9cfe-7716-48e0-94bf-42fe44595928/ssp-troubleshoot?ssp_name=deals_api_mcp" \
-H "Authorization: Bearer <api_key>"
```

Returns the deal's buyer seat statuses and any rejected seats flagged as primary issues.

## Status Mapping

`deals-api-mcp` returns seller status as an integer (`sellerStatus`). The connector normalizes it:

| `sellerStatus` | IAB label | Normalized status |
|---------------|-----------|-------------------|
| `0` | Active | `active` |
| `1` | Paused | `paused` |
| `2` | Pending | `created` |
| `4` | Complete | `expired` |
| `5` | Archived | `archived` |

!!! warning "Buyer vs seller status codes share the integer space"
A buyer-side `0` (Pending) is not the same as a deal-side `0` (Active). They live on different objects — `deal.sellerStatus` vs `buyerSeat.buyerStatus`. Always check which field you are reading.

## Connection Lifecycle

```mermaid
sequenceDiagram
participant SA as seller-agent
participant TR as MCP Transport
participant MCP as deals-api-mcp

SA->>TR: __aenter__ (first request)
TR->>MCP: POST /mcp initialize
MCP-->>TR: 200 OK + Mcp-Session-Id: <uuid>
TR-->>SA: persistent session ready ✓

SA->>TR: call_tool("deals_create", args)
TR->>MCP: POST /mcp tools/call (Mcp-Session-Id: <uuid>)
MCP-->>TR: Content-Type: text/event-stream\ndata: {"result":{...}}
TR-->>SA: parsed result dict

SA->>TR: __aenter__ (second request — session reused)
Note over SA,TR: background task still running, no re-initialize
SA->>TR: call_tool("deals_create", args)
TR->>MCP: POST /mcp tools/call (Mcp-Session-Id: <uuid>)
MCP-->>TR: Content-Type: text/event-stream\ndata: {"result":{...}}
TR-->>SA: parsed result dict
```

!!! note "Persistent session"
`DealsAPIMCPClient` keeps a **class-level persistent background task** so the MCP session is created on first use and reused across subsequent `async with` blocks without re-initializing.

If the connection drops, the stale session is detected on the next tool call (via a 30 s timeout), the background task is restarted, and the call is retried automatically — no process restart required. The first request after a drop may take up to 30 seconds while reconnection completes; subsequent requests are unaffected.

## Related

- [deals-api-mcp README](https://github.com/IABTechLab/deals-api-mcp#readme) — server setup, all 10 MCP tools, data models
- `src/ad_seller/clients/deals_api_mcp_client.py` — source for `DealsAPIMCPClient`
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ nav:
- Integration:
- Buyer Agent Guide: integration/buyer-agent.md
- Negotiation Protocol: integration/negotiation.md
- deals-api-mcp: integration/deals-api-mcp.md
- AI Assistant Setup:
- Claude (Desktop & Web): guides/claude-desktop-setup.md
- ChatGPT, Codex & AI IDEs: guides/chatgpt-setup.md
Expand Down
11 changes: 10 additions & 1 deletion src/ad_seller/clients/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Author: Green Mountain Systems AI Inc.
# Donated to IAB Tech Lab

"""Protocol clients for OpenDirect 2.1, A2A, GAM, SSP, and ad server abstraction."""
"""Protocol clients for OpenDirect 2.1, A2A, GAM, SSP, deal-sync, and ad server abstraction."""

from .a2a_client import A2AClient, A2AResponse
from .ad_server_base import (
Expand All @@ -16,6 +16,9 @@
get_ad_server_client,
)
from .csv_adapter import CSVAdServerClient
from .deal_sync_base import DealSyncClient, DealSyncProvider, DealSyncRegistry
from .deal_sync_factory import build_deal_sync_registry
from .deals_api_mcp_client import DealsAPIMCPClient
from .freewheel_adapter import FreeWheelAdServerClient
from .gam_rest_client import GAMRestClient
from .gam_soap_client import GAMSoapClient
Expand Down Expand Up @@ -65,4 +68,10 @@
"RESTSSPClient",
"build_ssp_registry",
"IndexExchangeSSPClient",
# Deal-sync abstraction
"DealSyncClient",
"DealSyncProvider",
"DealSyncRegistry",
"DealsAPIMCPClient",
"build_deal_sync_registry",
]
120 changes: 120 additions & 0 deletions src/ad_seller/clients/deal_sync_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Author: Green Mountain Systems AI Inc.
# Donated to IAB Tech Lab

"""Abstract deal-sync client interface.

Deal-sync connectors push negotiated deals to an external deal
synchronization service (e.g. the IAB deals-api-mcp server), which
propagates them to buyer-side providers. This is a peer of the other
connector families:
- AdServerClient: inventory sync, deal setup in the publisher's ad server
- SSPClient: deal distribution through SSP exchanges to DSPs
- DealSyncClient: deal sync through an external deal-sync service

Reuses the normalized deal models from ssp_base (SSPDeal,
SSPDealCreateRequest, SSPDealStatus); giving this family its own
models is out of scope for the connector split.
"""

from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, ClassVar, Optional

from .ssp_base import SSPDeal, SSPDealCreateRequest, SSPDealStatus


class DealSyncProvider(str, Enum):
"""Known deal-sync providers (extensible via config)."""

DEALS_API_MCP = "deals_api_mcp"


class DealSyncClient(ABC):
"""Abstract base class for deal-sync integrations.

Each provider implementation must provide these methods.
The deal-sync registry manages configured providers.
"""

channel: ClassVar[str] = "deal_sync"
provider: DealSyncProvider
provider_name: str = "Unknown Deal Sync Provider"

@abstractmethod
async def connect(self) -> None:
"""Establish connection to the deal-sync service."""
...

@abstractmethod
async def disconnect(self) -> None:
"""Disconnect from the deal-sync service."""
...

async def __aenter__(self) -> "DealSyncClient":
await self.connect()
return self

async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
await self.disconnect()

@abstractmethod
async def create_deal(self, request: SSPDealCreateRequest) -> SSPDeal:
"""Create a new deal on the deal-sync service."""
...

@abstractmethod
async def get_deal(self, deal_id: str) -> SSPDeal:
"""Get deal details (and sync status) by provider-internal ID."""
...

@abstractmethod
async def list_deals(
self,
*,
status: Optional[SSPDealStatus] = None,
limit: int = 100,
) -> list[SSPDeal]:
"""List deals on this provider."""
...

@abstractmethod
async def update_deal(self, deal_id: str, updates: dict[str, Any]) -> SSPDeal:
"""Update mutable deal attributes."""
...

async def health_check(self) -> bool:
"""Check if the connection is healthy. Override for custom logic."""
return True


class DealSyncRegistry:
"""Registry for configured deal-sync clients, keyed by provider name."""

def __init__(self) -> None:
self._clients: dict[str, DealSyncClient] = {}
self._default: Optional[str] = None

def register(self, name: str, client: DealSyncClient) -> None:
"""Register a deal-sync client by provider name."""
self._clients[name] = client
if self._default is None:
self._default = name

def get_client(self, name: str) -> DealSyncClient:
"""Get a deal-sync client by provider name."""
if name not in self._clients:
raise KeyError(
f"Deal-sync provider '{name}' not registered. "
f"Available: {list(self._clients.keys())}"
)
return self._clients[name]

def get_default(self) -> DealSyncClient:
"""Get the default (first-registered) deal-sync client."""
if not self._default:
raise RuntimeError("No deal-sync clients registered")
return self._clients[self._default]

def list_providers(self) -> list[str]:
"""List registered provider names."""
return list(self._clients.keys())
54 changes: 54 additions & 0 deletions src/ad_seller/clients/deal_sync_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Author: Green Mountain Systems AI Inc.
# Donated to IAB Tech Lab

"""Deal-sync registry factory — builds deal-sync clients from config.

Reads DEAL_SYNC_CONNECTORS from settings to build a DealSyncRegistry
with all configured providers.
"""

import logging
from typing import Any

from .deal_sync_base import DealSyncRegistry

logger = logging.getLogger(__name__)


def build_deal_sync_registry(settings: Any = None) -> DealSyncRegistry:
"""Build a DealSyncRegistry from application settings.

Reads DEAL_SYNC_CONNECTORS (comma-separated list) and creates the
appropriate client for each configured provider.
"""
if settings is None:
from ..config import get_settings

settings = get_settings()

registry = DealSyncRegistry()

connectors = [s.strip() for s in settings.deal_sync_connectors.split(",") if s.strip()]

for name in connectors:
name_lower = name.lower()
if name_lower == "deals_api_mcp":
from .deals_api_mcp_client import DealsAPIMCPClient

if not settings.deals_api_mcp_url:
logger.warning("deals_api_mcp configured but DEALS_API_MCP_URL not set")
continue

registry.register(
name_lower,
DealsAPIMCPClient(
mcp_url=settings.deals_api_mcp_url,
api_key=settings.deals_api_mcp_key,
seller_origin=settings.deals_api_mcp_seller_origin,
),
)
logger.info("Registered deal-sync connector: %s", name_lower)
else:
logger.warning("Unknown deal-sync provider '%s'", name)

return registry
Loading
Loading