diff --git a/.changeset/4399-idempotency-key-read-tool-guidance.md b/.changeset/4399-idempotency-key-read-tool-guidance.md
new file mode 100644
index 0000000000..279da72b7f
--- /dev/null
+++ b/.changeset/4399-idempotency-key-read-tool-guidance.md
@@ -0,0 +1,4 @@
+---
+---
+
+docs: add Python Pydantic guidance for compliance runner idempotency_key injection on read tools (#4399)
diff --git a/docs/building/by-layer/L4/build-an-agent.mdx b/docs/building/by-layer/L4/build-an-agent.mdx
index 7deb3e7546..f47ba219ee 100644
--- a/docs/building/by-layer/L4/build-an-agent.mdx
+++ b/docs/building/by-layer/L4/build-an-agent.mdx
@@ -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.
+
+**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.
+
+
- [PyPI Package](https://pypi.org/project/adcp/)
- [GitHub Repository](https://github.com/adcontextprotocol/adcp-client-python)
diff --git a/docs/building/cross-cutting/known-ambiguities.mdx b/docs/building/cross-cutting/known-ambiguities.mdx
index adbe53deca..2e742314b4 100644
--- a/docs/building/cross-cutting/known-ambiguities.mdx
+++ b/docs/building/cross-cutting/known-ambiguities.mdx
@@ -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).
diff --git a/docs/building/verification/get-test-ready.mdx b/docs/building/verification/get-test-ready.mdx
index fdd4f2a50d..1658add03b 100644
--- a/docs/building/verification/get-test-ready.mdx
+++ b/docs/building/verification/get-test-ready.mdx
@@ -156,6 +156,10 @@ For custom MCP wrappers — AsyncLocalStorage for per-request auth, transport-le
## Step 4 — Run the storyboard runner
+
+**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.
+
+
Once the three surfaces are in place, the runner takes over:
```bash