Skip to content

server: generated MCP inputSchema for the 13 media-buy seller tools is 13.7 MB / 3.45M tokens on 8.0.0b15 (5.1 MB / 1.27M on 7.0.2); update_media_buy alone exceeds a 1M-token context window #1212

Description

@chinmoyacharjee

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: .

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions