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
12 changes: 1 addition & 11 deletions src/adcp/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,6 @@ def create_mcp_webhook_payload(
else:
result_value = result

# `token` isn't a typed schema field but is accepted via `extra='allow'`;
# it round-trips through `model_dump`. Tracked upstream for promotion to
# a typed field on `mcp-webhook-payload.json`.
extras: dict[str, Any] = {}
if token is not None:
# Buyer-supplied token from push_notification_config.token,
# echoed back per push-notification-config.json spec text:
# "Echoed back in webhook payload to validate request authenticity."
extras["token"] = token

payload = McpWebhookPayload.model_validate(
{
"idempotency_key": idempotency_key,
Expand All @@ -254,7 +244,7 @@ def create_mcp_webhook_payload(
"operation_id": operation_id,
"message": message,
"context_id": context_id,
**extras,
"token": token,
}
)
# Preserve task result payloads byte-for-byte. Validating through the
Expand Down
54 changes: 54 additions & 0 deletions tests/test_webhooks_to_wire_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,57 @@ def test_create_mcp_webhook_payload_protocol_kwarg() -> None:
protocol="media_buy",
idempotency_key="whk_01HW9D2T3VXQ5M7K9N1P3R5S7U",
)


def test_token_is_typed_field_not_model_extra() -> None:
"""``McpWebhookPayload.token`` is now a typed schema field.

Regression for adcp#4339 promotion: token must appear in
``model_fields``, not in ``model_extra``, and the typed kwarg path
must produce a wire dict byte-identical to what the old
``additionalProperties`` shim produced.
"""
token_value = "buyer-supplied-token-abc123456789"
ik = "whk_01HW9D2T3VXQ5M7K9N1P3R5S7U"

payload = create_mcp_webhook_payload(
task_id="task_123",
status="completed",
task_type="create_media_buy",
token=token_value,
idempotency_key=ik,
)

# token is a typed field, not a stray extra
assert "token" in McpWebhookPayload.model_fields
assert payload.token == token_value
assert "token" not in (payload.model_extra or {})

wire = to_wire_dict(payload)
assert wire["token"] == token_value

# Wire parity: dict built by hand must match the typed kwarg path
hand_built = McpWebhookPayload.model_validate(
{
"idempotency_key": ik,
"task_id": "task_123",
"task_type": "create_media_buy",
"status": "completed",
"timestamp": payload.timestamp,
"token": token_value,
}
)
hand_wire = to_wire_dict(hand_built)
assert hand_wire["token"] == wire["token"]


def test_token_none_omitted_from_wire() -> None:
"""When no token is supplied the key is absent from the wire dict."""
payload = create_mcp_webhook_payload(
task_id="task_123",
status="completed",
task_type="create_media_buy",
idempotency_key="whk_01HW9D2T3VXQ5M7K9N1P3R5S7U",
)
wire = to_wire_dict(payload)
assert "token" not in wire
Loading