about · the whole picture

Two gaps in MCP.
One boundary.
One instrument that reads both.

mcp-turnstile stands at the tools/list boundary — where every MCP server hands your model its tool definitions — and gives any server a report card on two things at once: what that metadata costs in tokens, and what it hides. This page is the whole story: why the gaps exist, what we're doing about it, what works today, and where it goes next.

the story

Four beats, one design decision.

01the setup

Connect a few MCP servers and, before you type a word, your model is handed full JSON schemas for every tool. A large, growing slice of its context window, spent on instructions it will mostly never use.

02the root cause

Neither problem is a bug. Both fall out of one design decision: MCP delivers the entire tool catalog eagerly and unauthenticated at a single point — tools/list — and treats every tool's name, description, and schema as trusted documentation, streamed straight into the model's context.

03the two gaps

From that one choice: no budget → the token tax — nothing accounts for what metadata costs. No integrity → metadata trust — nothing normalizes, verifies, or pins the bytes the model actually reads.

04the part that should scare you

A tool description is just text, and the model reads it as instruction. So it can hide an instruction a human reviewer will never see — the approval-view fidelity gap: you approve clean-looking text; the model receives a concealed payload.

what we're trying to do

One mediator at one boundary — measured, not asserted.

The mission in one paragraph, plus the two disclaimers that keep it honest.

Both gaps share one address. Because the token tax and the metadata-trust gap fall out of the same design decision, one mediator at the tools/list boundary closes both — and the security check rides in nearly free on the token savings everyone already wants.

No single leg is novel — and we say so

  • Token-costing ships in mcp-checkup
  • Deterministic hidden-Unicode detection is commodity — Microsoft's Agent Governance Toolkit, Cisco's YARA rules; false-positive tuning ships in Cisco's rules
  • Byte-pinning ships in Invariant Labs' MCP-Scan (Snyk); SEP-1766 holds the digest slot in the spec

The defensible contribution is the compound: (1) reproducible measurement; (2) static tools/list catalog token-pricing and metadata concealment audit in one offline, dependency-free pass — no tool we found does both at this boundary; (3) context-aware, i18n-honest concealment detection that is deterministic — no LLM, no network — verified at 0 false positives across ~40 scripts. The deterministic prior art trades coverage for false positives (stripping codepoints — false-positiving on real Persian, Thai, Indic, emoji — or setting blunt count thresholds); LLM/neural approaches add latency and a network call. This uses grapheme/script context and does neither. Credibility is the product.

Honest boundary

mcpturn reports and grades — it does not block or intercept. Findings are signals for review and CI, not a security boundary. A flag is a reason for a human to look, not a verdict either way.

The injection/exfil regex signals are deliberately noisy hints — a benign "read a file" tool can match — and sit apart from the Unicode detector; the 0-false-positive result covers the concealment detector only. Documented residuals:

  • Wholly-non-Latin homoglyph word spoofs (an all-Cyrillic, Cherokee, or Lisu word that spells a Latin word) — needs the full UTS-39 fold table; likewise a Latin word carrying Armenian confusables (Armenian agglutinates onto Latin brand words, so flagging it would false-positive on real text)
  • Latin-block / visibly-distinct look-alikes (script-ɡ U+0261, dotless-ı, fullwidth, small-caps, math-alphanumerics) — outside the invisible approval-view threat model: a human can see them
  • Bidi embeddings and isolates (LRE/RLE, LRI/RLI/FSI, RLM/LRM) — legitimate in RTL text and W3C/ICU interpolation, so only the RLO/LRO overrides of the classic Trojan-Source PoCs are flagged
  • A zero-width payload spread as strictly isolated singletons AND padded below 30% density — which bloats the text to several times the payload length
live demo · runs in this page

Both gaps, live in your browser.

No backend, no tricks. The math and the Unicode decode below both run client-side, right here. Drag, type, press.

demo AThe token tax, live.

Every figure updates as you move the slider — priced at ≈468 tokens per tool definition (measured 466–468).

context spent on tool definitions // live, client-side 33,709 tokens
72
16.9% of your context window — before you type a word · ≈6.6× more than a minimal schema — 6.9× at 12 tools, easing to 6.6× at 72 as fixed overhead amortizes.
with progressive disclosure at the same gate: ~97% fewer per task≈883 tokens per task: the always-paid tool_search/tool_load index plus the one tool the task needs (measured; roughly flat no matter how many tools you connect).

demo BReveal the invisible.

A real tool description with a payload concealed in the invisible Unicode TAG block (U+E0000–E007F). You see plain text; the model receives a hidden instruction. Press the button — the page decodes it live, right in your browser. The scanner's own check is deterministic: pure stdlib, no LLM, no network call.

tools/list · get_issue · malicious-conceal@6.6.6
visible glyphs changed0
hidden payload99 B
detector verdict🚩 high
nothing looks wrong — that's the point.
the plan

Shipped, in motion, and next.

An honest ledger. Everything under NOW runs on your machine today; everything else is labeled for what it is.

nowshipped and verified

The harness proves both gaps. Pure-stdlib and reproducible — every number regenerates from one command.

shipped

The mcpturn CLI scans any MCP server — stdio and HTTP transports.

shipped

Python and TypeScript at parity. Same tokens, same ratios, and verdict-identical across the 35-string corpus and the 139-string battery (the two runtimes' Unicode tables can differ only for code points newer than the host Python's UCD).

shipped

Concealment detector hardened and verified i18n-honest. 0 false positives on a 35-string legitimate corpus and on the 98 legitimate strings of a 139-string adversarial battery (~40 scripts/mechanisms); 5/5 core techniques and 27/41 evasion variants caught — deterministic, no LLM, no network.

verified

Four ways to run: single-file .pyz, pip, python -m mcpturn, Docker — plus npm.

shipped

Release v0.1.0 is live and the work is merged to main.

shipped
current status

The report card, turned on ourselves.

Where the project stands right now — same grading language the scanner uses.

mcp-turnstile · project status
repository github.com/singhpratech/mcp-turnstile — author singhpratech (sole author) live
status merged to main · public · GitHub Pages live merged
release v0.1.0 · single-file mcpturn.pyz asset (23,306 bytes) · verified download-and-run from a clean directory live
parity Python + TypeScript/Node at parity — tokens, and verdict-identical on the corpus + adversarial battery — 72-tool minimal floor 5,075 tokens · 6.64× ratio in both shipped
detector Unicode concealment detector — deterministic, no LLM, no network · 0 false positives on the 35-string legit corpus and the 98 legit strings of the 139-string adversarial battery · 5/5 core techniques · 27/41 evasion variants verified