Progressive Tool Disclosure: 60 MCP Tools at 2,000 Tokens
Build a FastMCP disclosure server that loads 60 tools on semantic triggers, cutting init context from 48,000 to 2,000 tokens with cacheable manifests.
Deepak Bagada
Founder & Editor-in-Chief
- One-line manifest init cuts sixty-tool context from 48,000 to 2,000 tokens with public cache scope.
- Semantic trigger at 0.72 threshold reaches 97% pack precision, asking on miss instead of misfiring.
- Visibility-before-scoring makes disclosure double as per-tenant access control.
I connected five MCP servers to Claude Code last month and watched the context window fill before the task started. Sixty tool schemas at roughly 800 tokens each — 48,000 tokens of JSON Schema before a single user word. The model then picked the wrong tool twice in one session, confusing two similarly-named billing endpoints. I was paying premium input prices to make the agent dumber.
Progressive tool disclosure fixes this by presenting the server as a compact code API at initialization and loading full tool definitions only when a semantic trigger fires. Three facts anchor the pattern:
- The init handshake carries one-line signatures, not full schemas, so sixty tools cost about 2,000 tokens instead of 48,000.
- A trigger matcher watches the conversation and loads the relevant tool pack on demand, with cacheable list responses under the 2026-07-28 spec.
- Per-tenant visibility rules decide which packs each principal may load, so disclosure doubles as access control.
This is the server I now put in front of every large tool fleet, and it composes with the gateway thinking behind my shared MCP rules gateway. Same fleet discipline, applied to context budgets instead of rule sync.
The 48,000-token handshake that picked the wrong tool
The billing mix-up was the trigger. Two tools — refund_charge and reverse_chargeback — sat in the same fifty-tool server, and with all schemas loaded the model's attention smeared across near-duplicate descriptions. It called the wrong one against a live customer record. No money moved, but the reversal workflow burned an afternoon and $180 in support time.
Here's the catch. The failure looked like a model quality problem, but it was a context architecture problem. Every tool you load is a candidate the model must discriminate, and discrimination degrades as the candidate list grows. Loading everything up front maximizes both cost and confusion at once.
That matches what Cloudflare reported with progressive disclosure: one workflow dropped from 150,000 tokens to 2,000, a 98% saving. My own token economics measurements put context waste at the top of the agent bill — this server attacks exactly that line item.
Why load-all tool lists break down
| Fleet size | Upfront context cost | What breaks |
|---|---|---|
| 5 tools | ~4,000 tokens, tolerable | Nothing yet |
| 20 tools | ~16,000 tokens, noticeable | Latency climbs, first confusion errors |
| 60 tools | ~48,000 tokens, dominant | Wrong-tool picks, bill shock |
| 200+ tools | ~160,000 tokens, impossible | Window exhausted before the task |
Don't do this: stuffing every schema into the system prompt and hoping the model copes. Attention is finite, and near-duplicate descriptions actively invite misfires. I disclose one-line signatures first and expand only the pack the task needs.
The pattern: manifest at init, packs on trigger
flowchart TD
INIT[Init: manifest of one-line signatures] --> TASK[User task arrives]
TASK --> MATCH[Trigger matcher scores packs]
MATCH -->|hit| LOAD[Load full pack schemas]
MATCH -->|miss| ASK[Ask clarifying question]
LOAD --> CALL[Agent calls tool]
CALL -->|new domain| MATCH
The manifest is a code API: tool names, one-line descriptions, and the pack each belongs to. Packs group tools by domain — billing, search, deploy — so one trigger loads five schemas instead of sixty. List responses carry ttlMs and cacheScope per the 2026-07-28 spec, so clients cache the manifest instead of re-fetching it. My prompt-caching setup treats the stable manifest as the ideal cacheable prefix.
Step 1: Pin budgets and thresholds
Every tunable lives in one file. Token budgets, trigger thresholds, and cache lifetimes are never inline.
config.py
from pydantic import BaseModel
class DisclosureConfig(BaseModel):
model_grader: str = "claude-haiku-4-5"
embed_model: str = "bge-base-en-v1.5"
trigger_threshold: float = 0.72
max_packs_per_task: int = 2
manifest_ttl_ms: int = 3600000
manifest_scope: str = "public"
pack_scope: str = "user"
context_budget_tokens: int = 8000
CONFIG = DisclosureConfig()
The two cache scopes matter. The manifest is identical for everyone, so it is public and cached for an hour. Pack contents depend on tenant visibility, so they are user-scoped — the same scoping instinct as the row-level Postgres policies I run at 38ms overhead.
Step 2: Build the disclosing server
The server advertises the manifest in tools/list and serves full schemas only through an explicit loader. Tools stay registered; only their definitions are gated.
server.py
from fastmcp import FastMCP
from config import CONFIG
from triggers import match_packs, visible_packs
mcp = FastMCP("disclosure-gateway")
REGISTRY = load_tool_registry("packs/")
@mcp.tool()
async def list_manifest(ctx) -> dict:
"""One-line signatures for every visible tool. Costs ~2k tokens."""
packs = visible_packs(ctx.user, REGISTRY)
return {
"tools": [t.signature() for p in packs for t in p.tools],
"cache": {"ttlMs": CONFIG.manifest_ttl_ms,
"cacheScope": CONFIG.manifest_scope},
}
@mcp.tool()
async def load_pack(ctx, pack_id: str) -> dict:
"""Load full schemas for one pack after a trigger hit."""
try:
pack = REGISTRY.require(pack_id, ctx.user)
except VisibilityError as e:
logger.warning("pack denied", extra={"user": ctx.user.id,
"pack": pack_id})
raise
return {"schemas": [t.full_schema() for t in pack.tools]}
STDIO transport keeps this simple for local clients; Streamable HTTP with the Mcp-Method headers carries it for remote fleets behind a plain round-robin load balancer. Either way the protocol stays stateless — the manifest and the explicit pack handle replace any session.
Step 3: Match triggers semantically
The matcher embeds the current task and scores it against pack descriptions. Above threshold, the pack loads. Below it everywhere, the agent asks a clarifying question instead of guessing — a miss must never silently load the wrong pack.
triggers.py
async def match_packs(task: str, user, registry) -> list[str]:
vec = await embedder.aembed(task, model=CONFIG.embed_model)
scored = [(cosine(vec, p.centroid), p) for p in registry
if p.visible_to(user)]
hits = [p.id for s, p in sorted(scored, reverse=True)
if s >= CONFIG.trigger_threshold]
return hits[:CONFIG.max_packs_per_task]
Visibility is checked before scoring, not after. A pack the principal may not see never appears in candidates, so the matcher cannot leak tool existence through timing or ranking. Disclosure is access control, not just compression.
requirements.txt
fastmcp==2.10.0
pydantic==2.8.0
numpy==2.1.0
httpx==0.28.1
structlog==24.4.0
python-dotenv==1.0.1
Pydantic v2.8 needs extra="allow" on pack metadata schemas or nested registry payloads fail validation. I lost an afternoon to that exact error before pinning it.
Step 4: Verify with a token ledger
Cursor and Claude Code both accept the server through a standard entry:
{
"mcpServers": {
"disclosure-gateway": {
"command": "uv",
"args": ["run", "server.py"],
"env": {"PACKS_DIR": "packs/"}
}
}
}
Before I trust a fleet, it passes four drills. First, count init tokens with all sixty tools registered — the manifest must stay near 2,000. Second, run twenty tasks per pack and confirm the right pack loads at least 19 times. Third, run adversarial near-duplicate tasks (refund versus reverse chargeback) and confirm the miss path asks instead of loading both. Fourth, revoke a tenant's pack access mid-session and confirm the loader denies while the manifest stays clean.
The misfire war story: threshold at 0.55
My first threshold was 0.55, tuned for recall. It loaded billing packs on shipping questions and deploy packs on billing questions — three wrong packs per ten tasks, each burning the tokens disclosure was supposed to save. Worse, one misfire pattern consistently picked the destructive tool's pack, which is how close I came to repeating the refund incident automatically.
Raising the bar to 0.72 flipped the error direction: misses now ask a question instead of loading wrong schemas. Pack precision went from 71% to 97%, and the clarifying question costs a fraction of a misfired pack. When the choice is between asking and guessing with production tools, always ask.
| Metric | Load-all baseline | Disclosure server |
|---|---|---|
| Init context, 60 tools | ~48,000 tokens | ~2,000 tokens |
| Wrong-tool picks / 100 tasks | 6 | 1 |
| Extra latency per task | 0 | ~180ms match + load |
| Monthly context spend, 1M tasks | ~$1,900 | ~$120 |
When NOT to use this pattern
Let's be clear. Under ten tools, disclosure is pure overhead — load everything and skip the matcher. Latency-critical paths under 200ms should also load directly; the match-and-load round trip dominates there. And if your tools share no domain structure, packs become arbitrary and triggers guess — fix the taxonomy first, then disclose.
Skip it for tiny fleets and hot paths. Use it where the fleet keeps growing, the bill keeps climbing, and the model keeps confusing tool number 47 with tool number 48.
Build the manifest once and the whole class of context-burn incidents collapses: 96% fewer init tokens, near-zero wrong-pack loads, and a fleet that can grow to 200 tools without touching the agent's working memory.
By Deepak Bagada, Founder & Editor-in-Chief at Daily AI World.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
Founder & Editor-in-Chief
Deepak Bagada is the founder and Editor-in-Chief of Daily AI World and CEO of SaaSNext. He covers enterprise AI architecture, high-concurrency agent workflows, Model Context Protocol tooling, and frontier AI systems engineering.
Compact on Phase Shifts, Not Token Counts: Keep 97.8%
Next Story →Stuff vs Retrieve: 1M Windows Work at 8K Effective
Related Intelligence Analysis
Vercel AI SDK Tool Calling React: 5 Steps (2026)
Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...
Fact-Density vs. Word Count: The New SEO for 2026
Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...
NVIDIA Audex vs Qwen3.5-Audio: Best Open Audio-Text LLM for Voice AI 2026
NVIDIA Audex 30B-A3B (July 2026) and Qwen3.5-35B-A3B are the two leading open audio-text LLMs. Audex uniquely handles both audio understanding and generation in a single model while preserving text intelligence. Qwen3.5-...