Skip to main content

Protocol Envelope Examples

This guide shows how task responses are wrapped by different protocol layers. The same task response payload appears in different envelope formats depending on the protocol.

The Separation Principle

Task Response (what you implement):
{
  "products": [
    {
      "product_id": "ctv_premium",
      "name": "CTV Premium",
      "pricing": { "model": "cpm", "amount": 45.00, "currency": "USD" }
    }
  ]
}
Protocol Envelope (what the protocol layer adds):
  • Session tracking (context_id)
  • Async operation tracking (task_id, status)
  • Human-readable message
  • Webhook configuration

MCP (Model Context Protocol)

Successful Response

{
  "content": [
    {
      "type": "text",
      "text": "Found 3 products matching your criteria for CTV inventory"
    },
    {
      "type": "resource",
      "resource": {
        "uri": "adcp://response/get_products",
        "mimeType": "application/json",
        "text": "{\"products\": [...]}"
      }
    }
  ],
  "metadata": {
    "context_id": "ctx_abc123",
    "status": "completed"
  }
}
Key Points:
  • Human message in content[].text
  • Task payload in content[].resource.text (includes application-level context when present)
  • Protocol metadata in metadata
  • MCP doesn’t expose task_id - async is handled via MCP’s progress notifications

Async Response (Long-Running Operation)

{
  "content": [
    {
      "type": "text",
      "text": "Creating media buy. This will take 5-10 minutes."
    }
  ],
  "metadata": {
    "context_id": "ctx_def456",
    "status": "working"
  },
  "isError": false,
  "_meta": {
    "progressToken": "prog_789"
  }
}
MCP sends progress notifications separately:
{
  "method": "notifications/progress",
  "params": {
    "progressToken": "prog_789",
    "progress": 50,
    "total": 100
  }
}

A2A (Agent-to-Agent Protocol)

Successful Response

{
  "task": {
    "task_id": "task_123",
    "state": "completed",
    "artifacts": [
      {
        "name": "get_products_result",
        "content_type": "application/json",
        "content": "{\"products\": [...]}"
      }
    ]
  },
  "messages": [
    {
      "role": "assistant",
      "content": "Found 3 products matching your criteria for CTV inventory"
    }
  ],
  "context_id": "ctx_abc123"
}
Key Points:
  • Human message in messages[] array
  • Task payload in task.artifacts[]
  • Explicit task_id for tracking
  • state field for task status
  • A2A native support for async via task state machine

Async Response (Submitted)

{
  "task": {
    "task_id": "task_456",
    "state": "working",
    "estimated_duration_seconds": 600,
    "webhook_url": "https://buyer.example.com/webhooks/a2a"
  },
  "messages": [
    {
      "role": "assistant",
      "content": "Creating media buy. This will take 5-10 minutes. I'll notify you via webhook when complete."
    }
  ],
  "context_id": "ctx_def456"
}
Later, webhook notification:
{
  "task_id": "task_456",
  "state": "completed",
  "artifacts": [
    {
      "name": "create_media_buy_result",
      "content_type": "application/json",
      "content": "{\"media_buy_id\": \"mb_789\", ...}"
    }
  ]
}

REST API

Successful Response (200 OK)

HTTP/1.1 200 OK
Content-Type: application/json
X-AdCP-Context-Id: ctx_abc123
X-AdCP-Status: completed

{
  "message": "Found 3 products matching your criteria for CTV inventory",
  "data": {
    "context": { "ui": "buyer_dashboard" },
    "products": [...]
  }
}
Key Points:
  • HTTP status code indicates success/failure
  • Protocol fields in custom headers
  • Human message at top level
  • Task payload in data field

Async Response (202 Accepted)

HTTP/1.1 202 Accepted
Content-Type: application/json
Location: /api/v1/tasks/task_789
X-AdCP-Context-Id: ctx_def456
X-AdCP-Task-Id: task_789

{
  "message": "Creating media buy. This will take 5-10 minutes.",
  "task_id": "task_789",
  "status": "submitted",
  "status_url": "https://sales.example.com/api/v1/tasks/task_789",
  "estimated_duration_seconds": 600,
  "data": {
    "context": { "ui": "buyer_dashboard" },
    "buyer_ref": "campaign_2024_q1"
  }
}
Poll for status:
GET /api/v1/tasks/task_789
Response when complete:
HTTP/1.1 200 OK
Content-Type: application/json
X-AdCP-Context-Id: ctx_def456
X-AdCP-Task-Id: task_789
X-AdCP-Status: completed

{
  "message": "Media buy created successfully",
  "status": "completed",
  "data": {
    "context": { "ui": "buyer_dashboard" },
    "media_buy_id": "mb_123",
    "buyer_ref": "campaign_2024_q1",
    "packages": [...]
  }
}

Error Response (400 Bad Request)

HTTP/1.1 400 Bad Request
Content-Type: application/json
X-AdCP-Context-Id: ctx_ghi789

{
  "message": "Invalid targeting parameters",
  "status": "failed",
  "data": {
    "errors": [
      {
        "code": "INVALID_TARGETING",
        "message": "Geographic targeting codes are invalid",
        "field": "targeting.geo_codes",
        "severity": "error"
      }
    ]
  }
}

Protocol Compliance Testing

Validating Your Implementation

Here’s how to test that your protocol adapter is compliant:
import { validateTaskResponse } from '@adcp/schemas';
import { mcpAdapter, a2aAdapter, restAdapter } from './protocol-adapters';

// 1. Test task response schema (domain-specific)
const taskResponse = getProductsHandler(request);
const isValid = validateTaskResponse('get_products', taskResponse);
assert(isValid, 'Task response must match schema');

// 2. Test protocol envelope construction
const mcpEnvelope = mcpAdapter.wrap({
  contextId: 'ctx_123',
  status: 'completed',
  message: 'Found 3 products',
  payload: taskResponse
});

// Verify MCP structure
assert(mcpEnvelope.content, 'MCP must have content array');
assert(mcpEnvelope.content[0].type === 'text', 'First content must be text');
assert(mcpEnvelope.metadata.context_id, 'Must include context_id');

// 3. Test A2A envelope
const a2aEnvelope = a2aAdapter.wrap({
  contextId: 'ctx_123',
  taskId: 'task_456',
  status: 'completed',
  message: 'Found 3 products',
  payload: taskResponse
});

assert(a2aEnvelope.task.task_id, 'A2A must have task_id');
assert(a2aEnvelope.messages[0].role === 'assistant', 'Must have assistant message');

// 4. Test REST envelope
const restEnvelope = restAdapter.wrap({
  contextId: 'ctx_123',
  status: 'completed',
  message: 'Found 3 products',
  payload: taskResponse
});

assert(restEnvelope.data, 'REST must have data field');
assert(restEnvelope.message, 'REST must have message field');

Testing Cross-Protocol Compatibility

// The same task response should work in all protocols
const taskResponse = { products: [...] };

test('task response works across all protocols', () => {
  // All adapters should successfully wrap the response
  const mcp = mcpAdapter.wrap({ payload: taskResponse, status: 'completed' });
  const a2a = a2aAdapter.wrap({ payload: taskResponse, status: 'completed' });
  const rest = restAdapter.wrap({ payload: taskResponse, status: 'completed' });

  // All should extract the same payload
  assert.deepEqual(
    mcpAdapter.unwrap(mcp),
    a2aAdapter.unwrap(a2a)
  );
  assert.deepEqual(
    a2aAdapter.unwrap(a2a),
    restAdapter.unwrap(rest)
  );
});

Implementation Checklist

When implementing AdCP support:
  • Task handlers return ONLY domain data (no message, context_id, status, task_id)
  • Protocol adapter adds envelope fields based on protocol requirements
  • Message generation creates human-readable text from task results
  • Context tracking maintains conversation state across operations
  • Async support handles long-running operations appropriately
  • Error mapping translates domain errors to protocol-specific formats
  • Schema validation validates task responses against AdCP schemas
  • Cross-protocol tests verify same task response works everywhere

Common Pitfalls

❌ Don’t Do This

// Task handler includes protocol fields - WRONG!
function getProducts(request) {
  return {
    status: 'completed',  // ❌ Protocol concern
    message: 'Found 3',   // ❌ Protocol concern
    products: [...]       // ✅ Domain data
  };
}

✅ Do This Instead

// Task handler returns only domain data
function getProducts(request) {
  return {
    products: [...]  // ✅ Domain data only
  };
}

// Protocol adapter adds envelope
function wrapResponse(taskResponse, metadata) {
  return {
    status: metadata.status,
    message: generateMessage(taskResponse),
    context_id: metadata.contextId,
    data: taskResponse  // Task response becomes payload
  };
}

Summary

Webhook

Task Status Webhook Payload

{
  "operation_id": "op_456",
  "task_id": "task_456",
  "task_type": "create_media_buy",
  "status": "completed",
  "message": "Media buy created successfully",
  "timestamp": "2025-01-22T10:30:00Z",
  "result": {
    "media_buy_id": "mb_123",
    "buyer_ref": "campaign_2024_q1",
    "packages": [
      { "package_id": "pkg_001" }
    ]
  }
}
ProtocolMessage LocationPayload LocationAsync Mechanism
MCPcontent[].textcontent[].resource.textProgress notifications
A2Amessages[].contenttask.artifacts[].contentTask state + webhooks
RESTmessage fielddata fieldPolling or webhooks
The envelope schema (/schemas/v1/core/protocol-envelope.json) provides the conceptual model. This document shows how each protocol actually serializes that model on the wire.