Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .changeset/4399-idempotency-key-read-tool-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

docs: add Python Pydantic guidance for compliance runner idempotency_key injection on read tools (#4399)
4 changes: 4 additions & 0 deletions docs/building/by-layer/L4/build-an-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ serve(MySeller(), name="my-seller")

Response builders (`adcp.server.responses`) handle schema compliance so you don't construct raw JSON. Use them for every tool return.

<Note>
**Pydantic input models: allow unknown fields.** The compliance runner injects `idempotency_key` on all tool calls including reads. If your read tool input models use strict Pydantic validation (FastMCP default, or any model without `extra='ignore'`), add `model_config = ConfigDict(extra='ignore')` to a base model your inputs inherit from — otherwise `get_products` and similar read tools will fail validation before the handler runs. See [Known spec ambiguities](/docs/building/cross-cutting/known-ambiguities) for details.
</Note>

- [PyPI Package](https://pypi.org/project/adcp/)
- [GitHub Repository](https://github.com/adcontextprotocol/adcp-client-python)
</Tab>
Expand Down
23 changes: 23 additions & 0 deletions docs/building/cross-cutting/known-ambiguities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ For the opposite direction — failures that are **not** in this list and do hav
- **Resolution**: [#2607](https://github.com/adcontextprotocol/adcp/issues/2607) — the step declares `omit_idempotency_key: true`, which signals the runner to skip both its own `applyIdempotencyInvariant` and the SDK's auto-inject. The request arrives at your agent without a key, letting the vector probe the rejection path honestly.
- **Workaround**: nothing required — honor the existing spec requirement (reject missing `idempotency_key` on mutating tasks with `INVALID_REQUEST` or `VALIDATION_ERROR`).

### Compliance runner injects `idempotency_key` on read tool calls

- **Storyboards**: Any storyboard that calls a read tool — `get_products`, `list_accounts`, `get_adcp_capabilities`, and similar read-verb tools.
- **Gap**: the `@adcp/sdk` compliance runner injects `idempotency_key` on every tool call it dispatches, including read tools. The spec requires `idempotency_key` only on mutating tools — read tools are safe to call without one. Python sellers using FastMCP or a strict Pydantic input model receive a validation error (`TypeError: unexpected keyword argument` for function-based dispatch; `ValidationError` with `extra_forbidden` for Pydantic-model–based dispatch) before the handler runs. (The read tool JSON Schemas already set `"additionalProperties": true`; the strict behavior is at the MCP wrapper layer, not the schema.)
- **Resolution**: [#4399](https://github.com/adcontextprotocol/adcp/issues/4399) — normative guidance that sellers MUST use permissive MCP parameter handling is a 3.1.x item.
- **Workaround (Python/FastMCP)**: Add `ConfigDict(extra='ignore')` to a base request model that all read tool input models inherit from:

```python
from typing import Optional
from pydantic import BaseModel, ConfigDict

class AdcpReadRequest(BaseModel):
model_config = ConfigDict(extra='ignore')

class GetProductsRequest(AdcpReadRequest):
brief: Optional[str] = None
# ... other fields
```

If you use per-function handler signatures without a base model, add `idempotency_key: Optional[str] = None` as an accept-and-ignore parameter on each read tool instead.

- **Workaround (JS/TS)**: No action needed — the `@adcp/sdk` runner uses open parameter dispatch. Go sellers using default `encoding/json` struct unmarshaling are also unaffected; if your struct decoder uses `DisallowUnknownFields`, apply the same accept-and-ignore pattern.

### Response schema fields asserted by storyboards

- **Storyboards**: `sales_catalog_driven` (catalog counts), `creative_ad_server` (pricing_options), `media_buy_seller/inventory_list_targeting` (property_list echo), `creative_ad_server` (vendor_cost required).
Expand Down
4 changes: 4 additions & 0 deletions docs/building/verification/get-test-ready.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ For custom MCP wrappers — AsyncLocalStorage for per-request auth, transport-le

## Step 4 — Run the storyboard runner

<Warning>
**Python sellers: configure Pydantic to accept unknown fields before running.** The compliance runner injects `idempotency_key` on every tool call, including read tools like `get_adcp_capabilities` and `get_products`. Strict Pydantic validation (FastMCP default, or any model without `extra='ignore'`) rejects unknown fields with a validation error before your handler runs — the first storyboard tool call fails before any business logic executes. Add `model_config = ConfigDict(extra='ignore')` to a base request model that all your read tool input models inherit from. See [Known spec ambiguities](/docs/building/cross-cutting/known-ambiguities) for the full workaround and context.
</Warning>

Once the three surfaces are in place, the runner takes over:

```bash
Expand Down
Loading