the tools/list boundary

The tools your model trusts, checked at the gate.

Every MCP server hands your model a stack of tool definitions before you type a word. mcp-turnstile stands at the tools/list boundary and does two things: measures how much of your context that stack wastes, and reveals what's hidden inside it.

context spent on tool definitions // 72 tools, 200k window 0%
33,709 tokens · ~468 per tool · a 6.6× redundancy over a minimal schema — gone before you type.
here's the part nobody puts in the readme
01the waste

Connect a few servers and your model is handed full JSON schemas for every tool — a sixth of its whole window — spent on instructions it will mostly never use.

02the boundary

All of it arrives at one place: the moment the catalog crosses from server into context. That's the gate.

03the part that should scare you

A tool description is just text, and the model reads it as instruction — so it can hide things a human reviewer will never see.

watch it happen

This description looks clean. It isn't.

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 forensics lab

Five ways to lie in a tool description.

Each one abuses a Unicode mechanism that is also legitimate somewhere — so you can't just ban the characters. The detector reads context instead: 5/5 core techniques caught, 27/41 evasion variants built to slip past them caught too — deterministically. No LLM, no network, pure stdlib, and it leaves real languages alone.

…and it flags none of these.

Every string below uses the exact mechanisms above — legitimately. Ban the codepoints and you break all of them. Verified: 0 false positives on a 35-string legitimate corpus, and 0 on the 98 legitimate strings inside a 139-string adversarial battery — roughly 40 scripts and mechanisms, from Persian ZWNJ to Mongolian FVS to CJK ideographic variation sequences.

فا Persian ZWNJ ع Arabic isolate हि Hindi conjunct த் Tamil conjunct ไทย Thai word-break 👨‍👩‍👧 family emoji 🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland flag ❤️ VS-16 heart 1️⃣ keycap 👋🏽 skin tone ᏣᎳᎩ Cherokee Mongolian FVS 10μs GitHub-репозиторий
one gate, both problems

Let one tool through when the task needs it.

The same checkpoint that catches the lies also indexes the catalog — the model searches and loads on demand instead of swallowing everything up front.

72 tools offered · 1 loaded for the task · 71 held behind the turnstile.

97% fewer tokens per task — while every one of the five attacks is still caught at the gate.
One boundary, both fixes. The security check costs almost nothing once you're already mediating the gate for tokens. That's the whole idea: the safety rides in free on the savings everyone already wants.
honest by construction

What this is — and what it isn't.

Credibility is the product. The scope is stated plainly; the prior art is credited openly.

Prior art — no single leg is novel

  • 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
  • Rug-pull byte-pinning ships in Invariant Labs MCP-Scan (Snyk); SEP-1766 already holds the digest/versioning slot in the spec

The contribution is the compound: reproducible measurement, plus static tools/list catalog token-pricing and concealment audit in one offline, dependency-free pass — no tool we found does both at this boundary — plus context-aware detection that stays i18n-honest: the deterministic prior art trades coverage for false positives (stripping codepoints, blunt thresholds), and LLM approaches add latency and a network call. This uses grapheme/script context and does neither.

Stated limitations

  • Token counts are a floor by default (stdlib heuristic under-counts; optional tiktoken gives exact counts)
  • Byte-pinning is trust-on-first-use — no defense against first-contact poisoning or $ref referents
  • It reports and grades — it does not block or intercept; findings are signals for review and CI, not a security boundary
  • Injection/exfil regex signals are deliberately noisy hints — a benign "read a file" tool can match — and are separate from the Unicode detector; the 0-false-positive result covers concealment detection only
  • Documented residuals: wholly-non-Latin homoglyph word spoofs (need the full UTS #39 fold table), and Latin words carrying Armenian confusables (Armenian agglutinates onto Latin brand words); Latin-block / visibly-distinct look-alikes (script-ɡ, dotless-ı, fullwidth, small-caps — a human can see them); bidi embeddings and isolates (only the RLO/LRO overrides of the classic Trojan-Source PoCs are flagged); and a zero-width payload spread as strictly isolated singletons padded below 30% density
reproduce everything

One command. No dependencies.

Every number here comes from a harness you can run in under a minute — Python 3.10+, standard library only, all servers local. No network access.

# clone and run the full report card
git clone https://github.com/singhpratech/mcp-turnstile && cd mcp-turnstile
./run.sh

# or one lens at a time
python3 -m bench.token_tax        # the token tax
python3 -m bench.security_proof   # poisoning · concealment · rug-pull
python3 -m bench.concealment_lab  # 5 lies vs the 35-string i18n corpus
python3 -m bench.progressive_demo # both fixes at one gate

Zero required dependencies — the standard library counts tokens with a conservative heuristic, so the tax is a floor, never inflated. Installing tiktoken is optional and only swaps in exact cl100k counts.