> ## Documentation Index
> Fetch the complete documentation index at: https://agenticadvertisingorg-changeset-release-main.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Guide

> AdCP MCP integration guide: tool call patterns, transport-session continuity, response parsing, and wire format for Model Context Protocol implementations.

Transport-specific guide for integrating AdCP using the Model Context Protocol. For task handling, status management, and workflow patterns, see [Task Lifecycle](/dist/docs/3.2.0-rc.2/building/by-layer/L3/task-lifecycle).

## Testing AdCP via MCP

You can test AdCP tasks using the [CLI tools](/dist/docs/3.2.0-rc.2/building/by-layer/L4/choose-your-sdk#cli-tools) or by chatting with [Addie](https://agenticadvertising.org), the AgenticAdvertising.org assistant.

## Tool Call Patterns

### Basic Tool Invocation

```javascript theme={null}
// Standard MCP tool call
const response = await mcp.call('get_products', {
  idempotency_key: "550e8400-e29b-41d4-a716-446655442072",
  buying_mode: "brief",
  brand: {
    domain: "premiumpetfoods.com"
  },
  brief: "Video campaign for pet owners"
});

// All responses include status field (AdCP 1.6.0+)
console.log(response.status);   // "completed" | "input-required" | "working" | etc.
console.log(response.message);  // Human-readable summary
```

### Tool Call with Filters

```javascript theme={null}
// Structured parameters
const response = await mcp.call('get_products', {
  idempotency_key: "550e8400-e29b-41d4-a716-446655442073",
  buying_mode: "brief",
  brand: {
    domain: "betnow.com"
  },
  brief: "Sports betting app for March Madness",
  filters: {
    channels: ["ctv"],
    delivery_type: "guaranteed",
    max_cpm: 50
  }
});
```

### Tool Call with Application-Level Context

```javascript theme={null}
// Pass opaque application-level context; agents must carry it back
const response = await mcp.call('build_creative', {
  target_capability_id: 'premium_bespoke_display',
  creative_manifest: { /* ... */ },
  context: { ui: 'buyer_dashboard', session: '123' }
});

// Response includes the same context at the top level
console.log(response.context); // { ui: 'buyer_dashboard', session: '123' }
```

## MCP Response Format

**Normative:** AdCP MCP responses use a **flat structure** — envelope fields (`status`, `context_id`, `context`, `task_id`, `timestamp`, `replayed`, `adcp_error`, `governance_context`) and task-body fields appear as siblings at the root of the tool response. The `payload` object defined on [`core/protocol-envelope.json`](https://adcontextprotocol.org/schemas/3.2.0-rc.2/core/protocol-envelope.json) is a documentary grouping construct, NOT a serialized wire key: body fields are NOT nested under a `payload:` key on MCP. This matches MCP's native `structuredContent` convention.

```json theme={null}
{
  "status": "completed",                  // envelope: unified task status
  "message": "Found 5 products",          // envelope: human-readable summary
  "context_id": "ctx-abc123",             // envelope: compatibility metadata; not MCP session state
  "context": { "ui": "buyer_dashboard" }, // envelope: per-request opaque echo (caller-owned)
  "timestamp": "2026-05-19T14:25:30Z",    // envelope: response generation time
  "products": [...],                      // body: task-specific data, sibling of envelope fields
  "errors": [...]                         // body: per-record / payload-level errors (warning severity allowed)
}
```

**Producer rule.** MCP tool implementations MUST emit envelope fields and body fields as flat siblings at the root. Nesting body fields under a `payload:` key is non-conformant — receivers parse from the flat root, and a nested representation breaks every shipping SDK.

**Receiver rule.** MCP tool consumers MUST parse envelope and body fields from the flat root of the tool response. Receivers MUST NOT require a nested `payload:` key; the schema's `payload` is documentation, not a wire requirement. When `status` is absent on the response (legacy or transport-native state carrier), receivers MUST default to `completed` for non-error responses and inspect `adcp_error` for error envelopes.

**`context_id` vs `context` — semantically orthogonal.**

* `context_id` maps to A2A's transport-native `contextId`. On MCP it is compatibility metadata only: callers MUST NOT rely on it for continuity, and servers ignore it where the selected request schema admits the field. It does not select or resume an MCP session.
* `context` is a **caller-supplied opaque echo object** ([`core/context.json`](https://adcontextprotocol.org/schemas/3.2.0-rc.2/core/context.json)) — the agent preserves the same JSON value without interpreting it. Used for buyer-side correlation (UI session IDs, trace IDs, custom metadata).
* Both MAY appear on the same response. They are NOT aliases.

**Status handling**: see [Task Lifecycle](/dist/docs/3.2.0-rc.2/building/by-layer/L3/task-lifecycle) for complete status handling patterns.

**Status Handling**: See [Task Lifecycle](/dist/docs/3.2.0-rc.2/building/by-layer/L3/task-lifecycle) for complete status handling patterns.

## Available Tools

All AdCP tasks are available as MCP tools:

### Protocol Tools

```javascript theme={null}
await mcp.call('get_adcp_capabilities', {...});  // Discover agent capabilities (start here)
```

### Media Buy Tools

```javascript theme={null}
await mcp.call('get_products', {...});           // Discover inventory
// Read each returned product's canonical format_options[] for format specs
await mcp.call('create_media_buy', {...});       // Create campaigns
await mcp.call('update_media_buy', {...});       // Modify campaigns
await mcp.call('sync_creatives', {...});         // Manage creative assets
await mcp.call('get_media_buy_delivery', {...}); // Performance metrics
await mcp.call('provide_performance_feedback', {...}); // Share outcomes
```

### Signals Tools

```javascript theme={null}
await mcp.call('get_signals', {...});      // Discover audience signals
await mcp.call('activate_signal', {...});  // Deploy signals to platforms
```

**Task Parameters**: See individual task documentation in [Media Buy](/dist/docs/3.2.0-rc.2/media-buy) and [Signals](/dist/docs/3.2.0-rc.2/signals/overview) sections.

## MCP Tasks as a Transport Wrapper

AdCP task lifecycle state is application-layer state. MCP Tasks can wrap a `tools/call` request so the MCP client, rather than the LLM, waits for the `CallToolResult`; they do not replace the AdCP `task_id`, status payloads, webhooks, or polling/reconciliation surfaces.

A task-augmented MCP call can complete successfully after delivering an AdCP payload whose `status` is still `submitted`. From that point, the media-buy, creative, signals, or governance workflow remains open at the AdCP layer and should be observed with webhooks or AdCP polling.

:::warning Client support is limited
Most chat-based MCP clients (Claude Desktop, Cursor) do not yet support MCP Tasks. If your client doesn't support task-augmented tool calls, use standard `tools/call` plus **webhooks** or **AdCP polling** instead — these work with any MCP client. See [Async Operations](/dist/docs/3.2.0-rc.2/building/by-layer/L3/async-operations) and [Push Notifications](/dist/docs/3.2.0-rc.2/building/by-layer/L3/webhooks) for transport-independent patterns.

MCP Tasks are useful when you control the MCP client (e.g., building your own orchestrator with `@modelcontextprotocol/sdk`) and want protocol-level waiting for the initial `tools/call` result. They are optional transport plumbing, not the canonical AdCP task store.
:::

### SDK Implementation

If you use the `@modelcontextprotocol/sdk` package, MCP Tasks support requires minimal code. Pass an `InMemoryTaskStore` (or your own `TaskStore` implementation) to the Server constructor — the SDK auto-registers handlers for `tasks/get`, `tasks/result`, `tasks/list`, and `tasks/cancel`:

```typescript theme={null}
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks';

const taskStore = new InMemoryTaskStore();

const server = new Server(
  { name: 'my-adcp-agent', version: '1.0.0' },
  {
    capabilities: {
      tools: {},
      tasks: {
        list: {},
        cancel: {},
        requests: { tools: { call: {} } },
      },
    },
    taskStore,
  },
);
```

In your `tools/call` handler, check for the `task` field and use the store:

```typescript theme={null}
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
  const taskField = request.params.task;
  const result = await executeMyTool(request.params);

  if (!taskField) return result; // Synchronous path

  // Task-augmented: extra.taskStore handles requestId, sessionId,
  // and sends notifications/tasks/status on completion
  const task = await extra.taskStore.createTask({ ttl: taskField.ttl });
  await extra.taskStore.storeTaskResult(
    task.taskId,
    result.isError ? 'failed' : 'completed',
    result,
  );
  return { task: await extra.taskStore.getTask(task.taskId) };
});
```

The SDK handles polling, cancellation, TTL cleanup, and `_meta` injection for `tasks/result` responses. `InMemoryTaskStore` is non-persistent — for production, implement a `TaskStore` backed by your database.

If you use `McpServer` instead of `Server`, register task-capable tools with `server.experimental.tasks.registerToolTask()` — the higher-level API enforces this for tools that declare `taskSupport`.

:::warning Production task isolation
`InMemoryTaskStore` does not scope tasks by session — any client that knows a task ID can read, cancel, or list it. For production, implement a `TaskStore` that filters by `sessionId` on every operation. Also clamp client-provided TTL values server-side and enforce rate limits on task creation.
:::

### Server Capabilities

AdCP MCP servers declare `tasks` in their capabilities:

```json theme={null}
{
  "capabilities": {
    "tools": {},
    "tasks": {
      "list": {},
      "cancel": {},
      "requests": {
        "tools": { "call": {} }
      }
    }
  }
}
```

### Tool-Level Task Support

Each tool declares whether it supports task-augmented execution via `execution.taskSupport`:

| Tool                     | `taskSupport` | Rationale                                      |
| ------------------------ | ------------- | ---------------------------------------------- |
| `get_products`           | `optional`    | Complex searches, HITL clarification           |
| `create_media_buy`       | `optional`    | External systems, approval workflows           |
| `update_media_buy`       | `optional`    | External system updates                        |
| `build_creative`         | `optional`    | Human creative review, long production renders |
| `sync_creatives`         | `optional`    | Asset processing and transcoding               |
| `get_signals`            | `optional`    | Complex audience discovery                     |
| `activate_signal`        | `optional`    | Platform deployment                            |
| `sync_plans`             | `optional`    | Governance plan processing                     |
| `check_governance`       | `optional`    | External policy evaluation                     |
| `report_plan_outcome`    | `optional`    | External system updates                        |
| `acquire_rights`         | `optional`    | Approval workflows                             |
| `update_rights`          | `optional`    | External updates                               |
| `get_rights`             | `optional`    | External lookups                               |
| `get_adcp_capabilities`  | `forbidden`   | Instant, static                                |
| `preview_creative`       | `forbidden`   | Renders existing manifest                      |
| `list_creatives`         | `forbidden`   | Session state lookup                           |
| `get_media_buys`         | `forbidden`   | Session state lookup                           |
| `get_media_buy_delivery` | `forbidden`   | Session state lookup                           |
| `get_creative_delivery`  | `forbidden`   | Session state lookup                           |
| `get_plan_audit_logs`    | `forbidden`   | Session state lookup                           |
| `get_brand_identity`     | `forbidden`   | Instant lookup                                 |

Tools with `taskSupport: "optional"` can be called either way:

* **Without `task` field**: Synchronous — returns the result directly
* **With `task` field**: Returns a `CreateTaskResult` immediately; poll the MCP task via transport-native `tasks/get`, retrieve the `CallToolResult` via transport-native `tasks/result`, then inspect the AdCP payload inside that result.

### Invoking a Tool as a Task

Include the `task` field in your `tools/call` request:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_products",
    "arguments": {
      "idempotency_key": "550e8400-e29b-41d4-a716-446655442005",
      "buying_mode": "brief",
      "brief": "Premium CTV inventory for luxury auto"
    },
    "task": {
      "ttl": 3600000
    }
  }
}
```

The server returns a task handle immediately:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "task": {
      "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
      "status": "working",
      "statusMessage": "Searching inventory for luxury auto CTV placements",
      "createdAt": "2025-11-25T10:30:00Z",
      "lastUpdatedAt": "2025-11-25T10:30:00Z",
      "ttl": 3600000,
      "pollInterval": 5000
    }
  }
}
```

The client polls the MCP transport task with `tasks/get` (respecting `pollInterval`) until the task reaches a terminal state (`completed`, `failed`, or `cancelled`), then retrieves the `CallToolResult` via `tasks/result`. To abort the transport task, send `tasks/cancel` with the MCP `taskId`.

After retrieving the `CallToolResult`, inspect the AdCP response payload. If it contains `status: "submitted"` and an AdCP `task_id`, the transport task has delivered the queued AdCP response but the application workflow is still open. Continue with webhooks or AdCP polling (`get_task_status`, or legacy `tasks/get` in 3.x).

### MCP Task Status vs. AdCP Status

AdCP uses a richer set of statuses than MCP Tasks. If an implementation mirrors AdCP progress into a transport-native MCP Task, use this mapping only for the MCP wrapper. The AdCP payload remains the source of truth for domain workflow state:

| AdCP Status                                 | MCP Task Status  | Notes                                                                                                     |
| ------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
| `working`                                   | `working`        | Direct mapping                                                                                            |
| `submitted`                                 | `working`        | Use `statusMessage` to indicate queued state                                                              |
| `input-required`                            | `input_required` | Server moves task to `input_required`, sends elicitation via `tasks/result`                               |
| `completed`                                 | `completed`      | Direct mapping                                                                                            |
| `failed`                                    | `failed`         | Direct mapping                                                                                            |
| `rejected` (transport lifecycle)            | `failed`         | MCP Tasks has no native rejected state; use `statusMessage` for the invocation-level rejection reason     |
| `rejected` (task-specific business outcome) | `completed`      | Store the AdCP result unchanged with payload `status: "rejected"`; direct tool calls use `isError: false` |
| `canceled`                                  | `cancelled`      | Spelling difference (AdCP uses American, MCP uses British)                                                |
| `auth-required`                             | `input_required` | Elicitation requests credentials                                                                          |

### Webhooks for Long-Lived Operations

MCP Tasks handles waiting within the MCP session, but many AdCP operations outlive a single session (e.g., a media buy that takes 24 hours for publisher approval). For these, register `push_notification_config` on the AdCP call:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_media_buy",
    "arguments": {
      "buyer_ref": "nike_q1_2025",
      "packages": [],
      "push_notification_config": {
        "url": "https://buyer.com/webhooks/adcp/create_media_buy/op_abc123",
        "authentication": {
          "schemes": ["HMAC-SHA256"],
          "credentials": "shared_secret_32_chars"
        }
      }
    },
    "task": {
      "ttl": 86400000
    }
  }
}
```

The MCP Task tracks the transport wrapper within the session. The webhook tracks the AdCP application task independently and remains valid even after the MCP session ends. See [Push Notifications](/dist/docs/3.2.0-rc.2/building/by-layer/L3/webhooks) for webhook payload formats and authentication.

## Context Management (MCP-Specific)

MCP does not use an AdCP request-body `context_id` to maintain conversation state. When an MCP host provides session continuity, it comes from the MCP transport session and is managed by the host and server. Durable AdCP work is resumed with the task or resource identifiers declared by the task schema.

Where the selected request schema admits `context_id`, servers MUST ignore it for session lookup, identity, account selection, authorization, task continuation, and idempotency scope. Clients MUST NOT copy a response `context_id` into later MCP tool arguments expecting continuity.

```javascript theme={null}
// Follow-up in the same MCP transport session. Do not add context_id.
const response = await mcp.call('get_products', {
  idempotency_key: "550e8400-e29b-41d4-a716-446655442077",
  buying_mode: "brief",
  brief: "Focus on premium inventory"
});
```

### Usage Examples

#### Calls on One Transport Session

```javascript theme={null}
const products = await mcp.call('get_products', {
  idempotency_key: "550e8400-e29b-41d4-a716-446655442074",
  buying_mode: "brief",
  brief: "Sports campaign"
});

// Follow-up on the same MCP transport session, if the host provides one.
const refined = await mcp.call('get_products', {
  idempotency_key: "550e8400-e29b-41d4-a716-446655442075",
  buying_mode: "brief",
  brief: "Focus on premium CTV"
});
```

#### Async Operations with MCP Tasks

For tools with `taskSupport: "optional"`, pass the `task` option to use MCP Tasks:

```javascript theme={null}
// Synchronous call (no task augmentation)
const products = await mcp.callTool({
  name: 'get_products',
  arguments: {
    idempotency_key: '550e8400-e29b-41d4-a716-446655442006',
    buying_mode: 'brief',
    brief: "Sports campaign"
  }
});

// Task-augmented call for a long-running operation
const result = await mcp.callTool({
  name: 'create_media_buy',
  arguments: {
    packages: [...],
    push_notification_config: {  // Webhook backup for session-outliving ops
      url: "https://buyer.com/webhooks/adcp/create_media_buy/op_abc123",
      authentication: {
        schemes: ["HMAC-SHA256"],
        credentials: "shared_secret_32_chars"
      }
    }
  },
  task: { ttl: 86400000 }  // 24-hour TTL
});

// result is a CreateTaskResult for the MCP wrapper.
// After tasks/result, inspect the AdCP payload; if it is still submitted,
// continue via webhook or AdCP get_task_status / legacy tasks/get.
```

**Webhook POST format:**

```json theme={null}
{
  "task_id": "task_456",
  "status": "completed",
  "timestamp": "2025-01-22T10:30:00Z",
  "result": {
    "media_buy_id": "mb_12345",
    "packages": [...]
  }
}
```

**Note:** Receivers MUST correlate webhooks using `operation_id` (and `task_type`) from the payload body, **not** by parsing the webhook URL. Buyers MAY embed `operation_id` in the URL path or query for their own server-side routing convenience (the URL structure is opaque to the seller and entirely buyer-defined), but the seller never parses that URL — the seller echoes the buyer-supplied `operation_id` it was given at registration, and the wire-level source of truth for correlation is the payload field. See [`mcp-webhook-payload.json`](https://adcontextprotocol.org/schemas/3.2.0-rc.2/core/mcp-webhook-payload.json) and [Webhooks — Operation IDs](/dist/docs/3.2.0-rc.2/building/by-layer/L3/webhooks#operation-ids-and-url-templates).

The `result` field contains the AdCP data payload. For `completed`/`failed` statuses, this is the full task response (e.g., `create-media-buy-response.json`). For other statuses, use the status-specific schemas (e.g., `create-media-buy-async-response-working.json`).

#### MCP Webhook Envelope Fields

The [`mcp-webhook-payload.json`](https://adcontextprotocol.org/schemas/3.2.0-rc.2/core/mcp-webhook-payload.json) envelope includes:

**Required fields:**

* `idempotency_key` — Per-fire transport dedup key (see schema for full semantics)
* `operation_id` — Buyer-supplied correlation identifier echoed verbatim by the seller. Receivers use this — **not** the URL path — to route notifications to the originating task. Sellers MUST NOT derive this by parsing the URL; the URL structure is implementation-defined from the seller's point of view.
* `task_id` — Unique task identifier for correlation
* `task_type` — Task name (e.g., `create_media_buy`, `sync_creatives`) for routing to per-task handlers
* `status` — Current task status (completed, failed, working, input-required, etc.)
* `timestamp` — ISO 8601 timestamp when webhook was generated

**Optional fields:**

* `notification_id` — Event-layer stable id for re-emission tracking (see schema)
* `protocol` — AdCP protocol family (`media-buy` or `signals`)
* `context_id` — Compatibility metadata; not an MCP session selector
* `message` — Human-readable context about the status change

**Data field:**

* `result` — Task-specific AdCP payload (see Data Schema Validation below)

#### Webhook Trigger Rules

Webhooks are sent when **all** of these conditions are met:

1. **Task type supports async** (e.g., `create_media_buy`, `sync_creatives`, `get_products`)
2. **`pushNotificationConfig` is provided** in the request
3. **Task runs asynchronously** — initial response is `working` or `submitted`

If the initial response is already terminal (`completed`, `failed`, `rejected`), no webhook is sent—you already have the result.

**Status changes that trigger webhooks:**

* `working` → Progress update (task actively processing)
* `input-required` → Human input needed
* `completed` → Final result available
* `failed` → Error details

#### Data Schema Validation

The `result` field in MCP webhooks uses status-specific schemas:

| Status           | Schema                                      | Contents                             |
| ---------------- | ------------------------------------------- | ------------------------------------ |
| `completed`      | `[task]-response.json`                      | Full task response (success branch)  |
| `failed`         | `[task]-response.json`                      | Full task response (error branch)    |
| `working`        | `[task]-async-response-working.json`        | Progress info (`percentage`, `step`) |
| `input-required` | `[task]-async-response-input-required.json` | Requirements, approval data          |
| `submitted`      | `[task]-async-response-submitted.json`      | Acknowledgment (usually minimal)     |

Schema reference: [`async-response-data.json`](https://adcontextprotocol.org/schemas/3.2.0-rc.2/core/async-response-data.json)

#### Webhook Handler Example

```javascript theme={null}
const express = require('express');
const app = express();

app.post('/webhooks/adcp/:task_type/:agent_id/:operation_id', async (req, res) => {
  const { task_type, agent_id, operation_id } = req.params;
  const webhook = req.body;

  // Verify webhook authenticity (HMAC-SHA256 example)
  const signature = req.headers['x-adcp-signature'];
  const timestamp = req.headers['x-adcp-timestamp'];
  if (!verifySignature(webhook, signature, timestamp)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Handle status changes
  switch (webhook.status) {
    case 'input-required':
      // Alert human that input is needed
      await notifyHuman({
        operation_id,
        message: webhook.message,
        data: webhook.result
      });
      break;

    case 'completed':
      // Process the completed operation
      if (task_type === 'create_media_buy') {
        await handleMediaBuyCreated({
          media_buy_id: webhook.result.media_buy_id,
          packages: webhook.result.packages
        });
      }
      break;

    case 'failed':
      // Handle failure
      await handleOperationFailed({
        operation_id,
        error: webhook.result?.errors,
        message: webhook.message
      });
      break;

    case 'working':
      // Update progress UI
      await updateProgress({
        operation_id,
        percentage: webhook.result?.percentage,
        message: webhook.message
      });
      break;

    case 'canceled':
      await handleOperationCanceled(operation_id, webhook.message);
      break;
  }

  // Always return 200 for successful processing
  res.status(200).json({ status: 'processed' });
});

function verifySignature(payload, signature, timestamp) {
  const crypto = require('crypto');
  const expectedSig = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(timestamp + JSON.stringify(payload))
    .digest('hex');
  return signature === `sha256=${expectedSig}`;
}
```

#### Task Management and Polling

```javascript theme={null}
// Check status of a specific AdCP task
const taskStatus = await mcp.call('get_task_status', {
  task_id: 'task_456',
  include_result: true
});
if (taskStatus.status === 'completed') {
  console.log('Result:', taskStatus.result);
}

// State reconciliation
const reconciliation = await mcp.call('list_tasks', {
  filters: { statuses: ['submitted', 'working', 'input-required'] }
});
if (reconciliation.tasks.length > 0) {
  console.log('Found open AdCP tasks:', reconciliation.tasks);
  // Start tracking these tasks
}

console.log(`${reconciliation.tasks.length} operations in progress`);
```

**Key difference**: A2A exposes native `contextId` conversation threading. MCP session continuity is transport-local and does not use an AdCP tool argument.

## Handling Async Operations

When an AdCP response returns `working` or `submitted` status, you need a way to receive the result. This applies whether or not your MCP client supports MCP Tasks — the patterns below work with any client.

| Approach      | Best For                              | Trade-offs                                                                                    |
| ------------- | ------------------------------------- | --------------------------------------------------------------------------------------------- |
| **Webhooks**  | Production systems, any task duration | Handles hours/days, but requires a public endpoint                                            |
| **Polling**   | Simple integrations, short tasks      | Easy to implement, but inefficient for long waits                                             |
| **MCP Tasks** | Custom clients using the MCP SDK      | Protocol-native wrapper for the initial `tools/call`, but does not replace AdCP task tracking |

### Option 1: Webhooks (recommended)

Configure a webhook URL and the server will POST the result when the operation completes. This is the right approach for `submitted` operations that are blocked on external dependencies (publisher approval, human review).

```javascript theme={null}
const response = await mcp.call('create_media_buy', {
    packages: [...],
    budget: { total: 150000, currency: "USD" },
    push_notification_config: {
      url: "https://buyer.com/webhooks/adcp/create_media_buy/op_abc123",
      authentication: {
        schemes: ["HMAC-SHA256"],
        credentials: "shared_secret_32_chars"
      }
    }
});

// If status is 'submitted', the server will POST the result to your webhook
// No polling needed — just handle the webhook when it arrives
```

See [Push Notifications](/dist/docs/3.2.0-rc.2/building/by-layer/L3/webhooks) for payload formats and authentication.

### Option 2: Polling (backup)

Use AdCP polling as a backup for `submitted` operations, or when you can't expose a webhook endpoint. In 3.x, prefer `get_task_status` when the seller advertises it; otherwise use legacy `tasks/get`:

```javascript theme={null}
async function pollForResult(mcp, taskId, pollInterval = 30000) {
  while (true) {
    const response = await mcp.call('get_task_status', {
      task_id: taskId,
      include_result: true
    });

    if (['completed', 'failed', 'canceled'].includes(response.status)) {
      return response;
    }

    if (response.status === 'input-required') {
      const input = await promptUser(response.message);
      const continuation = verifiedContinuationFor(response);
      if (!continuation) return requireReconciliation(response);
      return resumeContinuation(continuation, input);
    }

    await new Promise(resolve => setTimeout(resolve, pollInterval));
  }
}
```

### Handling different statuses

```javascript theme={null}
const initial = await mcp.call('create_media_buy', {
  packages: [...],
  budget: { total: 100000, currency: "USD" }
});

switch (initial.status) {
  case 'completed':
    // Done — result is inline
    console.log('Created:', initial.media_buy_id);
    break;

  case 'working':
    // Server is actively processing (>30s) — just wait, result will arrive
    // No polling needed; 'working' is a progress signal, not a polling trigger
    console.log('Processing:', initial.message);
    break;

  case 'submitted':
    // Blocked on external dependency — use webhook or poll
    console.log(`Task ${initial.task_id} queued for approval`);
    break;

  case 'input-required':
    // Blocked on user input
    console.log('Need more info:', initial.message);
    break;
}
```

## Integration Example

```javascript theme={null}
// Use unified status handling (see Core Concepts)
async function handleAdcpCall(tool, params) {
  const response = await mcp.call(tool, params);
  
  switch (response.status) {
    case 'input-required':
      // Handle clarification (see Core Concepts for patterns)
      const input = await promptUser(response.message);
      const continuation = verifiedContinuationFor(response);
      if (!continuation) return requireReconciliation(response);
      return resumeContinuation(continuation, input);
      
    case 'working':
      // Server is actively processing — just wait, result will arrive
      console.log('Processing:', response.message);
      return response;

    case 'submitted':
      // Blocked on external dependency — webhook or poll
      console.log(`Task ${response.task_id} submitted, webhook will notify`);
      return { pending: true, task_id: response.task_id };
      
    case 'completed':
      return response; // Task-specific fields are at the top level
      
    case 'failed':
      throw new Error(response.message);
  }
}

// Example usage
const products = await handleAdcpCall('get_products', {
  brief: "CTV campaign for luxury cars"
});
```

## MCP-Specific Considerations

### Server-side tool wrappers MUST tolerate envelope fields

Buyer SDKs and generic transport wrappers may send envelope-level fields admitted by the selected request schema, either as declared properties or through `additionalProperties: true`. Reference SDKs auto-generate `idempotency_key` only for mutating requests. MCP tool implementations MUST accept schema-admitted fields and ignore the ones they do not consume. An explicitly closed schema may reject undeclared fields. An admitted `context_id` is always a compatibility no-op; callers do not send it for continuity. Common traps:

* **FastMCP / Pydantic strict signatures** — declare schema-admitted envelope fields as accept-and-ignore optionals, or use `**kwargs` for open input schemas. `model_config = ConfigDict(extra='allow')` on open input models if you control them.
* **Zod / valibot with `.strict()`** on input schemas — drop `.strict()` or use a passthrough variant.
* **OpenAPI codegen** that injects `additionalProperties: false` into open input models — fix the generator config so it preserves the selected schema.

A wrapper that raises `unexpected_keyword_argument` on `idempotency_key` will fail compliance against any buyer SDK that follows the envelope contract. See [security.mdx > Server-side tool wrapper conformance](/dist/docs/3.2.0-rc.2/building/by-layer/L1/security#server-side-tool-wrapper-conformance) for the normative rule.

### Tool Discovery

```javascript theme={null}
// List available tools — use get_adcp_capabilities for runtime feature detection
const tools = await mcp.listTools();

// Check which tools support async execution
const asyncTools = tools.filter(t => t.execution?.taskSupport === 'optional');
```

### AdCP Extension via MCP Server Card

<Note>
  **Recommended**: Use [`get_adcp_capabilities`](/dist/docs/3.2.0-rc.2/protocol/get_adcp_capabilities) for runtime capability discovery. The server card extension provides static metadata for tool catalogs and registries.
</Note>

MCP servers can declare AdCP support via a server card at `/.well-known/mcp.json` (or `/.well-known/server.json`). AdCP-specific metadata goes in the `_meta` field using the `adcontextprotocol.org` namespace.

```json theme={null}
{
  "name": "io.adcontextprotocol/media-buy-agent",
  "version": "1.0.0",
  "title": "AdCP Media Buy Agent",
  "description": "AI-powered media buying agent implementing AdCP",
  "tools": [
    { "name": "get_products" },
    { "name": "create_media_buy" },
    { "name": "get_adcp_capabilities" }
  ],
  "_meta": {
    "adcontextprotocol.org": {
      "adcp_version": "2.6.0",
      "protocols_supported": ["media_buy"],
      "extensions_supported": ["sustainability"]
    }
  }
}
```

**Discovering AdCP support:**

```javascript theme={null}
// Check both possible locations for MCP server card
const serverCard = await fetch('https://sales.example.com/.well-known/mcp.json')
  .then(r => r.ok ? r.json() : null)
  .catch(() => null)
  || await fetch('https://sales.example.com/.well-known/server.json')
    .then(r => r.json());

// Check for AdCP metadata
const adcpMeta = serverCard?._meta?.['adcontextprotocol.org'];

if (adcpMeta) {
  console.log('AdCP Version:', adcpMeta.adcp_version);
  console.log('Supported domains:', adcpMeta.protocols_supported);
  // ["media_buy", "creative", "signals"]
  console.log('Typed extensions:', adcpMeta.extensions_supported);
  // ["sustainability"]
}
```

**Benefits:**

* Clients can discover AdCP capabilities without making test calls
* Declare which protocol domains you implement (media\_buy, creative, signals)
* Declare which typed extensions you support (see [Context & Sessions](/dist/docs/3.2.0-rc.2/building/by-layer/L2/context-sessions#extension-fields-ext))
* Enable compatibility checks based on version

:::note
The `adcp_version` field in server card metadata is a v2 convention and is not part of the v3 spec. For v3 version negotiation, the buyer sends release-precision `adcp_version` (e.g., `"3.1"`) on every request, and the seller advertises supported releases via `adcp.supported_versions` on [`get_adcp_capabilities`](/dist/docs/3.2.0-rc.2/protocol/get_adcp_capabilities) and echoes `adcp_version` at the envelope root on every response. The legacy integer-only `adcp_major_version` field is still accepted for backwards compatibility. See [versioning.mdx § Version negotiation](/dist/docs/3.2.0-rc.2/reference/versioning#version-negotiation) for the full contract.
:::

**Note:** The `_meta` field uses reverse DNS namespacing per the [MCP server.json spec](https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/generic-server-json.md). AdCP servers should support both `/.well-known/mcp.json` and `/.well-known/server.json` locations.

### Parameter Validation

```javascript theme={null}
// MCP provides tool schemas for validation
const toolSchema = await mcp.getToolSchema('get_products');
// Use schema to validate parameters before calling
```

### Error Handling

AdCP errors are returned as tool-level responses with `isError: true` and the error in `structuredContent.adcp_error`. For the full extraction logic and JSON-RPC transport codes, see [Transport Error Mapping](/dist/docs/3.2.0-rc.2/building/operating/transport-errors).

```javascript theme={null}
try {
  const response = await mcp.call('get_products', params);

  // Check for AdCP application errors (isError: true with structured data)
  if (response.isError) {
    const adcpError = response.structuredContent?.adcp_error;
    if (adcpError) {
      // Structured error with code, recovery, retry_after, etc.
      console.log('AdCP error:', adcpError.code, adcpError.recovery);
    }
  }
} catch (mcpError) {
  // MCP transport errors (connection, auth, etc.)
  // Check for AdCP-structured transport errors
  const adcpError = mcpError.data?.adcp_error;
  if (adcpError) {
    console.log('Transport error:', adcpError.code);
  } else {
    console.error('MCP Error:', mcpError);
  }
}
```

## Best Practices

1. **Reuse the authenticated MCP transport session** when the host offers continuity
2. **Check status field** before processing response data
3. **Use schema-declared task or resource IDs** for durable continuation
4. **Reference Core Concepts** for status handling patterns
5. **Validate parameters** using MCP tool schemas when available

## Next Steps

* **Core Concepts**: Read [Task Lifecycle](/dist/docs/3.2.0-rc.2/building/by-layer/L3/task-lifecycle) for status handling and workflows
* **Task Reference**: See [Media Buy Tasks](/dist/docs/3.2.0-rc.2/media-buy) and [Signals](/dist/docs/3.2.0-rc.2/signals/overview)
* **Protocol Comparison**: Compare with [A2A integration](/dist/docs/3.2.0-rc.2/building/by-layer/L0/a2a-guide)
* **Examples**: Find complete workflow examples in Core Concepts

**For status handling, async operations, and clarification patterns, see [Task Lifecycle](/dist/docs/3.2.0-rc.2/building/by-layer/L3/task-lifecycle) - this guide focuses on MCP transport specifics only.**
