Repository navigation
fix(server): compact a pinned seller's tools/list so an SDK 10 buyer can call it - #1702
Conversation
…can call it A version-pinned MCP seller (an adcp_version pin, which every decisioning PlatformHandler carries) advertised the exact bundled wire schemas in tools/list, ignoring schema_mode. get_products's response alone resolves 278 definitions, and MCP cannot share definitions between tools, so a plain eleven-tool seller listed 106,250 JSON members (2.5 MB). An SDK 10 buyer bounds a response's JSON members and its MCP client lists tools before every call, so every call to such a seller failed with ADCPResponseTooComplexError before the handler ran (#1692). This line's client has no member bound; the fix is to the listing a 9.x seller serves. The default schema_mode="compact" now compacts the bundled schema by the rules the unpinned path already applied to Pydantic output (every root field with its description and rules, every required nested field, every scalar with its constraints, each dropped optional nested field re-admitted as true), after dropping nested cross-field rules (if/then/else, not), nested x- metadata, nested $schema and definition descriptions, and advertising oneOf as anyOf so a loosened arm can never make a valid response fail the client's output-schema check. Local $ref pointers are JSON-Pointer-unescaped (the bundled definitions are named after paths, "external:core~1product.json"). The compact shape is memoized with the loader state. schema_mode="defs" and "inline" keep the exact bundled schema. The startup size line reports JSON members as well as bytes and warns past half of either SDK 10 buyer bound. The issue's seller lists 17,181 members (467 KB). Every tool the SDK can serve, pinned to the packaged release, lists 66,028 members and 1.8 MB, under half of both bounds; a guard test pins that, and an end-to-end test has an SDK client at defaults call get_products on an SDK seller at defaults over MCP. Fixes #1692 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — backport of #1701 to 9.x with no blocking findings.
Checked: this PR compacts a pinned MCP seller's tools/list under the default schema_mode="compact" (106,250→17,181 members on the #1692 repro) so an SDK 10 buyer's member bound no longer rejects every call. The load-bearing soundness invariant holds — compaction only loosens: if/not copied verbatim (mcp_tools.py:85), oneOf→anyOf, pruned optionals re-admitted as true. Covered by test_pinned_compact_output_schemas_accept_what_the_exact_schema_accepts against real DemoSeller output across 7 tools, plus root-contract preservation, no-dangling-$ref, and JSON-Pointer unescape tests. Memoization returns independent trees (schema_loader.py:523), so outputSchema.setdefault mutation is safe. E2e server has a 30s start deadline and bounded thread join.
Semver: fix(server): is correct — no public export signature changes, and the behavior change is a superset-accepting loosening (no buyer/seller deserialization break).
Decision-table walk: no critical/high/medium findings → rows 1, 4, 5, 6, 8 don't fire. gated_paths is false → row 2 doesn't fire. high_risk is true but all high-risk reasons are (modified) with zero medium findings → rows 3 and 5 don't fire. No no-auto-approve team match → row 7 doesn't fire. Falls through to row 9 = approve.
One non-blocking Low noted by the reviewer (_simplify_wire_schema can drop a oneOf when a node carries anyOf+oneOf+allOf, mcp_tools.py:278-284 — only ever loosens, soundness holds); Low findings are omitted from blocking consideration.
| async def get_products(self, params, context=None): | ||
| return {"products": []} | ||
|
|
||
| async def create_media_buy(self, params, context=None): ... |
|
|
||
| async def create_media_buy(self, params, context=None): ... | ||
|
|
||
| async def update_media_buy(self, params, context=None): ... |
|
|
||
| async def update_media_buy(self, params, context=None): ... | ||
|
|
||
| async def get_media_buys(self, params, context=None): ... |
|
|
||
| async def get_media_buys(self, params, context=None): ... | ||
|
|
||
| async def get_media_buy_delivery(self, params, context=None): ... |
|
|
||
| async def get_media_buy_delivery(self, params, context=None): ... | ||
|
|
||
| async def sync_creatives(self, params, context=None): ... |
|
|
||
| async def sync_creatives(self, params, context=None): ... | ||
|
|
||
| async def list_creatives(self, params, context=None): ... |
|
|
||
| async def list_creatives(self, params, context=None): ... | ||
|
|
||
| async def provide_performance_feedback(self, params, context=None): ... |
|
|
||
| async def provide_performance_feedback(self, params, context=None): ... | ||
|
|
||
| async def list_creative_formats(self, params, context=None): ... |
|
|
||
| async def list_creative_formats(self, params, context=None): ... | ||
|
|
||
| async def list_accounts(self, params, context=None): ... |
|
|
||
| async def list_accounts(self, params, context=None): ... | ||
|
|
||
| async def sync_accounts(self, params, context=None): ... |
Backport of #1701 to
main(9.x). Fixes #1692 for 9.x sellers.Why 9.x is affected
A version-pinned MCP seller — an
adcp_versionpin, which every decisioningPlatformHandlercarries (resolve_adcp_version(adcp_version), default 3.2) — advertises the exact bundled wire schemas intools/list, ignoringschema_mode.get_products's response alone resolves 278 definitions, and MCP has no way to share definitions between tools, so a plain eleven-tool seller lists 106,250 JSON members (2.5 MB) on 9.3.0, measured. An SDK 10 buyer bounds a response's JSON members and its MCP client lists tools before every call, so every SDK 10 call to a 9.x decisioning seller fails withADCPResponseTooComplexErrorbefore the handler runs. This line's own client has no member bound, so the fix here is to the listing a 9.x seller serves.Fix (same as #1701, server side only)
Under the default
schema_mode="compact", a pinned server's bundled schemas are compacted by the rules the unpinned path already applied to Pydantic output — every root field with its description and rules, every required nested field, every scalar with its constraints, each dropped optional nested field re-admitted astrue— after dropping nested cross-field rules (if/then/else,not), nestedx-metadata, nested$schemaand definition descriptions, and advertisingoneOfasanyOfso a loosened arm can never make a valid response fail the MCP client'sstructuredContentvalidation. Local$refpointers are JSON-Pointer-unescaped (external:core~1product.json); the compact shape is memoized with the loader state.schema_mode="defs"and"inline"keep the exact bundled schema. The startup size line reports JSON members as well as bytes and warns past half of either SDK 10 buyer bound (BUYER_RESPONSE_MEMBER_BUDGET = 524_288, the figure #1701 sets; 10.0.0b0/b1 enforced 100,000).Not ported: the client bound change (no member bound exists on 9.x) and the
supported_versions→discovery-version selection (that is #1689 machinery; on 9.xsupported_versionsalone does not pin, andPlatformHandlerpins itself).Numbers (full
tools/listJSON-RPC frame, 9.3.0)_adcp_version="3.2",supported_versions=["3.1","3.2"]advertise_all=True, pinned to the packaged 3.2Tests
Same as #1701: the all-tools guard (≤ 50% of both SDK 10 buyer bounds; pinned explicitly here, see the docstring), the end-to-end
ADCPClient→create_mcp_servercall over MCP, compaction soundness against realDemoSellerresponses, root-contract preservation, no dangling$refs, simplifier and JSON-Pointer unit tests, the member-count startup warning, and recomputed pinned-schema digests.🤖 Generated with Claude Code