Skip to content

perf(schema): drop the draft-07 $schema header from every tool - #125

Merged
VickyXAI merged 1 commit into
mainfrom
perf/strip-json-schema-dialect
Sep 2, 2026
Merged

perf(schema): drop the draft-07 $schema header from every tool#125
VickyXAI merged 1 commit into
mainfrom
perf/strip-json-schema-dialect

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Removes the "$schema": "http://json-schema.org/draft-07/schema#" header from every tool's inputSchema.

Clients validate the schema either way and the Anthropic API ignores it, but it ships in the tool block the model carries in its context on every turn.

The number

Measured against the built server — real MCP handshake (initializenotifications/initializedtools/list), tokenizing the model-visible projection {name, description, input_schema} with the mcp__blockrun__ prefix the host adds, o200k_base. Before and after measured the same way, not subtracted:

Profile Tools Before After Saved
full (default) 20 13,200 12,900 −300
trading 9 5,689 5,554 −135
media 7 5,541 5,436 −105
research 6 3,114 3,024 −90
chat 3 1,969 1,924 −45

Exactly 15 tokens per tool.

This is one item from a wider tool-schema audit. The larger prize — route catalogues and per-model pricing tables living in tool descriptions, which are ~53% of the block — is a separate batch, because it changes what the model reads and needs its own review.

Why it isn't a one-line deletion

It isn't our string. The SDK's zod→JSON-Schema conversion stamps it on, and exposes no option to suppress it — verified on @modelcontextprotocol/sdk 1.29.0 for both the zod v4 branch (z4mini.toJSONSchema) and the zod v3 branch (vendored zod-to-json-schema).

So stripJsonSchemaDialect() wraps the tools/list handler as it is installed, using only the public setRequestHandler. Two deliberate properties:

  • Never load-bearing. The wrapper is identity-matched against ListToolsRequestSchema. If a future SDK stops routing through it, the wrapper stops matching and the header simply comes back. Nothing breaks.
  • Degrades to a no-op. It skips when there is no low-level server to wrap — the tool-annotations and apps suites pass a minimal fake McpServer, and an earlier version that assumed server.server exists broke 8 of them.

Tests

test/schema-dialect.test.ts (2 tests). Because the wrapper is identity-matched, an SDK bump could silently restore the header with nobody noticing — an optimization that reverts unobserved is worse than not having done it. These tests are what notices.

No behaviour change

  • 425/425 tests pass (423 before, +2 new).
  • annotations on all 20 tools and _meta.ui on the 2 app tools are untouched.
  • Live stdio check: schema validation still rejects a missing required argument and a mistyped argument (-32602 both), and a free tool call still succeeds. No paid tool was called.

Note on the figures

An earlier revision of this PR quoted 13,765 → 13,465. Those numbers were 4.4% high: the measuring script used Python's json.dumps, which defaults to ensure_ascii=True and re-encodes every , and · as \uXXXX — six ASCII characters where the model sees one glyph. Independently reproduced with a JSON.stringify-based harness, which matches the wire. The saving is unaffected at −300, since the removed string is pure ASCII; only the totals it is measured against moved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WUL3ExR4Nz7uebxKaKwjDi

The SDK's zod->JSON-Schema conversion stamps
"$schema": "http://json-schema.org/draft-07/schema#" onto every tool's
inputSchema. Clients validate the schema either way and the Anthropic API
ignores it, but it ships in the tool block the model carries on every turn.

Measured against the built server (real MCP handshake, tools/list, tokenized
with o200k_base): the full profile drops 13,200 -> 12,900 tokens. Per profile:
trading 5,689 -> 5,554, media 5,541 -> 5,436, research 3,114 -> 3,024,
chat 1,969 -> 1,924. Exactly 15 tokens per tool.

The SDK exposes no option to suppress it, so we wrap the tools/list handler as
it is installed, using only the public setRequestHandler. If a future SDK stops
routing through ListToolsRequestSchema the wrapper stops matching and the
header simply comes back — it is never load-bearing, and it no-ops entirely
when there is no low-level server to wrap (the tool-annotation and apps tests
pass a minimal fake).

No behaviour change: 425/425 tests pass, annotations and _meta.ui are
untouched, and a live check confirms schema validation still rejects missing
and mistyped arguments.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WUL3ExR4Nz7uebxKaKwjDi
@VickyXAI
VickyXAI force-pushed the perf/strip-json-schema-dialect branch from a432ea7 to c85efc6 Compare September 2, 2026 02:27
@VickyXAI
VickyXAI merged commit 659f2dc into main Sep 2, 2026
1 check passed
@VickyXAI
VickyXAI deleted the perf/strip-json-schema-dialect branch September 2, 2026 03:01
VickyXAI added a commit that referenced this pull request Sep 2, 2026
Package managers have shown install size for decades. Installing an MCP server
spends the user's context on every turn — whether or not the tools are ever
called — and almost nothing shows that number. This publishes ours.

- README hero badge: 12.9K context tokens, plus a per-profile table at the
  point where a reader is already choosing a profile. `--profile trading` is
  57% less context than the default for the same trading workflow, which was
  already true and already shipped, but nowhere stated.

- scripts/measure-tool-schema.mjs: the harness, committed rather than left in
  a scratch directory. Runs against this repo's server or, with `-- <cmd>`,
  any stdio MCP server, so the claim is checkable by anyone against anyone.
  `npm run measure:schema`.

- test/schema-tokens.test.ts: the guard. Measures the live server in-process
  and fails if the README disagrees. Asserts the PUBLISHED form ("12.9K") and
  the table's exact per-profile totals — pinning raw counts would fail CI on
  every wording tweak, while pinning the published figure fails exactly when
  the claim becomes wrong. Verified by deliberately corrupting both the badge
  and a table row and confirming each fails.

The projection counted is {name, description, input_schema} per tool with the
host prefix, matching what lands in the API tools array. Numbers agree to the
token across four independent implementations: this script, an earlier Python
harness, a third-party rewrite, and the in-process test.

Stacked on #125 — the 12,900 figure assumes the $schema header is gone. Merge
that first; without it every number here is 300 higher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WUL3ExR4Nz7uebxKaKwjDi
VickyXAI added a commit that referenced this pull request Sep 2, 2026
Adds a 🧮 12.9K Context Tokens badge and a per-profile context table to the README, commits the measurement harness (npm run measure:schema, works against any stdio MCP server), and guards both with test/schema-tokens.test.ts so the published number cannot go stale. Ships alongside the $schema removal from #125.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant