From f2a52e609ba6be0ce40029f222561aaa4393e856 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 21:26:47 +0000 Subject: [PATCH] fix(webhooks): promote McpWebhookPayload.token from extras shim to typed field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream adcontextprotocol/adcp#4339 added token as a typed field in mcp-webhook-payload.json. The generated type already has the field; remove the additionalProperties round-trip shim from create_mcp_webhook_payload and wire token directly like any other typed kwarg. Wire shape is unchanged — adopters reading to_wire_dict() output see no difference. Adopters using payload.token now get typed attribute access instead of model_extra["token"]. Closes #638 --- src/adcp/webhooks.py | 12 +------ tests/test_webhooks_to_wire_dict.py | 54 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/adcp/webhooks.py b/src/adcp/webhooks.py index 36a52d788..5f0bf616e 100644 --- a/src/adcp/webhooks.py +++ b/src/adcp/webhooks.py @@ -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, @@ -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 diff --git a/tests/test_webhooks_to_wire_dict.py b/tests/test_webhooks_to_wire_dict.py index 558004228..64e652ed1 100644 --- a/tests/test_webhooks_to_wire_dict.py +++ b/tests/test_webhooks_to_wire_dict.py @@ -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