Summary
The inputSchema documents that adcp.server.mcp_tools._generate_pydantic_schemas()
produces for tools/list are too large for a buyer agent to load into a model.
Measured with tiktoken (o200k_base):
| adcp |
13 media-buy tools* |
4 booking tools** |
update_media_buy alone |
Max depth |
| 8.0.0b15 (latest beta, spec 3.2.0-rc.3) |
13.69 MB / 3,450,166 tok |
13.09 MB / 3,299,659 tok |
6.31 MB / 1,592,130 tok |
60 |
| 7.0.2 (latest stable, spec 3.1) |
5.08 MB / 1,268,487 tok |
4.69 MB / 1,174,016 tok |
2.29 MB / 573,547 tok |
47 |
| 6.4.0 (what we run) |
8.85 MB / 2,211,285 tok |
8.47 MB / 2,117,319 tok |
4.18 MB / 1,045,276 tok |
49 |
* The tools a media-buy sales agent registers: get_products, list_creative_formats,
sync_creatives, list_creatives, create_media_buy, update_media_buy, get_media_buy_delivery,
get_media_buys, get_signals, list_accounts, sync_accounts, provide_performance_feedback,
get_adcp_capabilities.
** get_products, create_media_buy, sync_creatives, update_media_buy.
Current frontier models document context windows of about one million tokens
(Anthropic 1M, OpenAI 1.05M, Google "1 million or more"). On every release the
four tools needed for one booking exceed that before the buyer has sent a
brief. On the current beta a single tool, update_media_buy, is 1.59M tokens
on its own, and the thirteen together are 3.4 windows.
A buyer that forwards tools/list to its model, which is what a generic
MCP-to-LLM bridge does, cannot use a publisher running this SDK. It fails on
request size, truncates the tool list, or hand-curates schemas per publisher,
which defeats discovery.
Where the bytes come from
The generator has two forms of the same schema. model_json_schema() gives a
deduplicated document with shared objects under $defs; _inline_refs() then
pastes every referenced definition into every use site. On 8.0.0b15:
| Tool |
Deduplicated ($defs) |
Inlined, as served |
Expansion |
update_media_buy |
414,799 B, 408 defs |
6,313,940 B |
15.2x |
create_media_buy |
414,997 B, 414 defs |
3,339,785 B |
8.0x |
sync_creatives |
227,241 B, 202 defs |
2,878,101 B |
12.7x |
get_products |
231,496 B, 264 defs |
555,326 B |
2.4x |
| all 13 tools |
1,678,465 B |
13,693,312 B |
8.2x |
In inlined create_media_buy the Agent Url definition appears 371 times,
Provider 360, Feature Id 240, Embedded At 240. The docstring on
_inline_refs gives the reason: some MCP clients do not resolve $ref and
would see an empty parameter list. MCP itself defines inputSchema as JSON
Schema and does not forbid $ref/$defs.
A second generator behaviour adds bytes and depth: every Optional[X] is
emitted as anyOf: [X, {"type": "null"}]. The spec has none of these; inlined
8.0.0b15 output has 26,044 across the thirteen tools (11,967 in
update_media_buy alone). Each is two extra nesting levels, and they sit
inside the shared definitions, so inlining multiplies them too.
The spec's shared objects (creative-asset, brand-reference) are large in
their own right; that is a separate issue in adcontextprotocol/adcp. The
8x is the SDK's.
Per-tool, adcp 8.0.0b15
| Tool |
Deduplicated (B) |
Served (B) |
Tokens |
Depth |
Nullable anyOf |
update_media_buy |
414,799 |
6,313,940 |
1,592,130 |
60 |
11,967 |
create_media_buy |
414,997 |
3,339,785 |
841,367 |
60 |
6,345 |
sync_creatives |
227,241 |
2,878,101 |
726,716 |
53 |
5,543 |
get_products |
231,496 |
555,326 |
139,446 |
49 |
999 |
sync_accounts |
97,493 |
215,678 |
52,931 |
43 |
375 |
provide_performance_feedback |
42,296 |
93,222 |
23,390 |
40 |
208 |
get_media_buy_delivery |
68,315 |
79,110 |
19,916 |
38 |
157 |
list_creatives |
47,399 |
74,226 |
18,505 |
45 |
168 |
get_signals |
57,381 |
54,849 |
13,471 |
38 |
93 |
get_media_buys |
38,669 |
36,862 |
9,170 |
38 |
76 |
list_accounts |
35,543 |
33,738 |
8,434 |
38 |
74 |
list_creative_formats |
|
15,839 |
3,998 |
10 |
34 |
get_adcp_capabilities |
2,836 |
2,636 |
692 |
7 |
5 |
| Total |
1,678,465 |
13,693,312 |
3,450,166 |
|
26,044 |
The four booking tools are 96% of the bytes. For comparison, the hand-written
schemas we serve for them total 13.9 KB (about 3,500 tokens), cover every
required field plus the optionals seen in real traffic, and runtime validation
still runs on the full model.
Secondary effect: depth breaks function-calling APIs
Depth here is JSON nesting (each object or array is one level). The wrappers
push served schemas to 38 to 60 levels. Gemini's function-calling API
(google-genai 2.5.0, gemini-3.6-flash) rejects declarations deeper than
about 20 with a bare 400 INVALID_ARGUMENT that does not name the tool.
Observed on 6.4.0: list_accounts, get_media_buys, get_media_buy_delivery,
get_signals, list_creatives, sync_accounts rejected as generated; the same
list_accounts schema capped at depth 20 accepted, at 30 rejected; removing
only the nullable wrappers brings it from depth 40 to 24. Every tool the API
accepted is depth 13 or less. On 8.0.0b15 provide_performance_feedback has
gone from 4 KB / depth 6 (7.0.2) to 93 KB / depth 40, so tools that were safe
cross the line as the spec grows.
Reproduction (SDK only)
import json, tiktoken
from adcp.server.mcp_tools import _generate_pydantic_schemas
enc = tiktoken.get_encoding("o200k_base")
def depth(n, d=0):
if isinstance(n, dict): return max([d] + [depth(v, d + 1) for v in n.values()])
if isinstance(n, list): return max([d] + [depth(v, d + 1) for v in n])
return d
tools = ["get_products", "list_creative_formats", "sync_creatives", "list_creatives",
"create_media_buy", "update_media_buy", "get_media_buy_delivery", "get_media_buys",
"get_signals", "list_accounts", "sync_accounts", "provide_performance_feedback",
"get_adcp_capabilities"]
schemas = _generate_pydantic_schemas()
tb = tt = 0
for name in tools:
txt = json.dumps(schemas[name]); b, t = len(txt), len(enc.encode(txt)); tb += b; tt += t
print(f"{name:30} {b:>10,} B {t:>10,} tok depth {depth(schemas[name])}")
print(f"{'TOTAL':30} {tb:>10,} B {tt:>10,} tok")
Last line on adcp 8.0.0b15 (Python 3.13.3, pydantic 2.13.5):
TOTAL 13,693,312 B 3,450,166 tok
and on 7.0.2: 5,075,294 B 1,268,487 tok.
Spec-side note
The spec's shared objects are large too: creative-asset is embedded wherever
a creative may appear, and core/account-ref.json pulls brand_kit_override
into plain list filters (list_accounts resolves to depth 24 on 3.1 via
account -> brand -> brand_kit_override -> logo -> provenance -> disclosure -> jurisdictions -> render_guidance -> positions).
Filed separately in adcontextprotocol/adcp: .
Summary
The
inputSchemadocuments thatadcp.server.mcp_tools._generate_pydantic_schemas()produces for
tools/listare too large for a buyer agent to load into a model.Measured with
tiktoken(o200k_base):update_media_buyalone* The tools a media-buy sales agent registers:
get_products,list_creative_formats,sync_creatives,list_creatives,create_media_buy,update_media_buy,get_media_buy_delivery,get_media_buys,get_signals,list_accounts,sync_accounts,provide_performance_feedback,get_adcp_capabilities.**
get_products,create_media_buy,sync_creatives,update_media_buy.Current frontier models document context windows of about one million tokens
(Anthropic 1M, OpenAI 1.05M, Google "1 million or more"). On every release the
four tools needed for one booking exceed that before the buyer has sent a
brief. On the current beta a single tool,
update_media_buy, is 1.59M tokenson its own, and the thirteen together are 3.4 windows.
A buyer that forwards
tools/listto its model, which is what a genericMCP-to-LLM bridge does, cannot use a publisher running this SDK. It fails on
request size, truncates the tool list, or hand-curates schemas per publisher,
which defeats discovery.
Where the bytes come from
The generator has two forms of the same schema.
model_json_schema()gives adeduplicated document with shared objects under
$defs;_inline_refs()thenpastes every referenced definition into every use site. On 8.0.0b15:
$defs)update_media_buycreate_media_buysync_creativesget_productsIn inlined
create_media_buytheAgent Urldefinition appears 371 times,Provider360,Feature Id240,Embedded At240. The docstring on_inline_refsgives the reason: some MCP clients do not resolve$refandwould see an empty parameter list. MCP itself defines
inputSchemaas JSONSchema and does not forbid
$ref/$defs.A second generator behaviour adds bytes and depth: every
Optional[X]isemitted as
anyOf: [X, {"type": "null"}]. The spec has none of these; inlined8.0.0b15 output has 26,044 across the thirteen tools (11,967 in
update_media_buyalone). Each is two extra nesting levels, and they sitinside the shared definitions, so inlining multiplies them too.
The spec's shared objects (
creative-asset,brand-reference) are large intheir own right; that is a separate issue in
adcontextprotocol/adcp. The8x is the SDK's.
Per-tool, adcp 8.0.0b15
anyOfupdate_media_buycreate_media_buysync_creativesget_productssync_accountsprovide_performance_feedbackget_media_buy_deliverylist_creativesget_signalsget_media_buyslist_accountslist_creative_formatsget_adcp_capabilitiesThe four booking tools are 96% of the bytes. For comparison, the hand-written
schemas we serve for them total 13.9 KB (about 3,500 tokens), cover every
required field plus the optionals seen in real traffic, and runtime validation
still runs on the full model.
Secondary effect: depth breaks function-calling APIs
Depth here is JSON nesting (each object or array is one level). The wrappers
push served schemas to 38 to 60 levels. Gemini's function-calling API
(
google-genai2.5.0,gemini-3.6-flash) rejects declarations deeper thanabout 20 with a bare
400 INVALID_ARGUMENTthat does not name the tool.Observed on 6.4.0:
list_accounts,get_media_buys,get_media_buy_delivery,get_signals,list_creatives,sync_accountsrejected as generated; the samelist_accountsschema capped at depth 20 accepted, at 30 rejected; removingonly the nullable wrappers brings it from depth 40 to 24. Every tool the API
accepted is depth 13 or less. On 8.0.0b15
provide_performance_feedbackhasgone from 4 KB / depth 6 (7.0.2) to 93 KB / depth 40, so tools that were safe
cross the line as the spec grows.
Reproduction (SDK only)
Last line on adcp 8.0.0b15 (Python 3.13.3, pydantic 2.13.5):
and on 7.0.2:
5,075,294 B 1,268,487 tok.Spec-side note
The spec's shared objects are large too:
creative-assetis embedded wherevera creative may appear, and
core/account-ref.jsonpullsbrand_kit_overrideinto plain list filters (
list_accountsresolves to depth 24 on 3.1 viaaccount -> brand -> brand_kit_override -> logo -> provenance -> disclosure -> jurisdictions -> render_guidance -> positions).Filed separately in
adcontextprotocol/adcp: .