perf(schema): drop the draft-07 $schema header from every tool - #125
Merged
Conversation
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
force-pushed
the
perf/strip-json-schema-dialect
branch
from
September 2, 2026 02:27
a432ea7 to
c85efc6
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Removes the
"$schema": "http://json-schema.org/draft-07/schema#"header from every tool'sinputSchema.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 (
initialize→notifications/initialized→tools/list), tokenizing the model-visible projection{name, description, input_schema}with themcp__blockrun__prefix the host adds,o200k_base. Before and after measured the same way, not subtracted: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/sdk1.29.0 for both the zod v4 branch (z4mini.toJSONSchema) and the zod v3 branch (vendoredzod-to-json-schema).So
stripJsonSchemaDialect()wraps thetools/listhandler as it is installed, using only the publicsetRequestHandler. Two deliberate properties:ListToolsRequestSchema. If a future SDK stops routing through it, the wrapper stops matching and the header simply comes back. Nothing breaks.tool-annotationsandappssuites pass a minimal fakeMcpServer, and an earlier version that assumedserver.serverexists 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
annotationson all 20 tools and_meta.uion the 2 app tools are untouched.-32602both), 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 toensure_ascii=Trueand re-encodes every—,→and·as\uXXXX— six ASCII characters where the model sees one glyph. Independently reproduced with aJSON.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