Skip to main content
Subscribe
Front Page / AI Tools / Deep Dive

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

Deepak Bagada

Founder & Editor-in-Chief

Sep 20, 2026 Published
|
Sep 20, 2026 Updated
|
7 Minutes Reading Time
Core Takeaways for Founders & Builders
  • 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 , Founder & Editor-in-Chief at Daily AI World.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

🎉 Thank You for Subscribing!

Frequently Asked Questions
Each tool schema costs roughly 800 tokens, so sixty tools burn about 48,000 tokens before the task starts. Worse, near-duplicate descriptions smear the model's attention and cause wrong-tool picks — I measured 6 per 100 tasks on a sixty-tool fleet.
The manifest carries one-line signatures for every visible tool at about 2,000 tokens. When the trigger matcher scores a pack above threshold, the agent loads that pack's full schemas on demand — typically five schemas instead of sixty.
Visibility is checked before scoring, so packs a principal may not load never appear as candidates. Pack list responses use user cache scope while the shared manifest uses public scope with a one-hour TTL.
Below ten tools, load everything — the matcher is pure overhead. Skip disclosure on sub-200ms hot paths where the match-and-load round trip dominates, and fix pack taxonomy before disclosing if tools share no domain structure.
Deepak Bagada
Author Profile

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.

Related Intelligence Analysis

Briefing AI Tools

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...

Deepak Bagada Deepak Bagada
12m read
Breaking AI Tools

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...

Deepak Bagada Deepak Bagada
4m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc

Cookie & Privacy Preferences

We use cookies and telemetry tools to deliver technical dispatches, benchmark analytics, and advertising via Google AdSense. Review our Privacy Policy.