Skip to content

Commit f2a52e6

Browse files
committed
fix(webhooks): promote McpWebhookPayload.token from extras shim to typed field
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
1 parent 65f84d2 commit f2a52e6

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

src/adcp/webhooks.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,6 @@ def create_mcp_webhook_payload(
233233
else:
234234
result_value = result
235235

236-
# `token` isn't a typed schema field but is accepted via `extra='allow'`;
237-
# it round-trips through `model_dump`. Tracked upstream for promotion to
238-
# a typed field on `mcp-webhook-payload.json`.
239-
extras: dict[str, Any] = {}
240-
if token is not None:
241-
# Buyer-supplied token from push_notification_config.token,
242-
# echoed back per push-notification-config.json spec text:
243-
# "Echoed back in webhook payload to validate request authenticity."
244-
extras["token"] = token
245-
246236
payload = McpWebhookPayload.model_validate(
247237
{
248238
"idempotency_key": idempotency_key,
@@ -254,7 +244,7 @@ def create_mcp_webhook_payload(
254244
"operation_id": operation_id,
255245
"message": message,
256246
"context_id": context_id,
257-
**extras,
247+
"token": token,
258248
}
259249
)
260250
# Preserve task result payloads byte-for-byte. Validating through the

tests/test_webhooks_to_wire_dict.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,57 @@ def test_create_mcp_webhook_payload_protocol_kwarg() -> None:
237237
protocol="media_buy",
238238
idempotency_key="whk_01HW9D2T3VXQ5M7K9N1P3R5S7U",
239239
)
240+
241+
242+
def test_token_is_typed_field_not_model_extra() -> None:
243+
"""``McpWebhookPayload.token`` is now a typed schema field.
244+
245+
Regression for adcp#4339 promotion: token must appear in
246+
``model_fields``, not in ``model_extra``, and the typed kwarg path
247+
must produce a wire dict byte-identical to what the old
248+
``additionalProperties`` shim produced.
249+
"""
250+
token_value = "buyer-supplied-token-abc123456789"
251+
ik = "whk_01HW9D2T3VXQ5M7K9N1P3R5S7U"
252+
253+
payload = create_mcp_webhook_payload(
254+
task_id="task_123",
255+
status="completed",
256+
task_type="create_media_buy",
257+
token=token_value,
258+
idempotency_key=ik,
259+
)
260+
261+
# token is a typed field, not a stray extra
262+
assert "token" in McpWebhookPayload.model_fields
263+
assert payload.token == token_value
264+
assert "token" not in (payload.model_extra or {})
265+
266+
wire = to_wire_dict(payload)
267+
assert wire["token"] == token_value
268+
269+
# Wire parity: dict built by hand must match the typed kwarg path
270+
hand_built = McpWebhookPayload.model_validate(
271+
{
272+
"idempotency_key": ik,
273+
"task_id": "task_123",
274+
"task_type": "create_media_buy",
275+
"status": "completed",
276+
"timestamp": payload.timestamp,
277+
"token": token_value,
278+
}
279+
)
280+
hand_wire = to_wire_dict(hand_built)
281+
assert hand_wire["token"] == wire["token"]
282+
283+
284+
def test_token_none_omitted_from_wire() -> None:
285+
"""When no token is supplied the key is absent from the wire dict."""
286+
payload = create_mcp_webhook_payload(
287+
task_id="task_123",
288+
status="completed",
289+
task_type="create_media_buy",
290+
idempotency_key="whk_01HW9D2T3VXQ5M7K9N1P3R5S7U",
291+
)
292+
wire = to_wire_dict(payload)
293+
assert "token" not in wire

0 commit comments

Comments
 (0)