MCP Server Design · AgentBridge

Progressive Tool Disclosure via a Navigable Module Tree

How an AI agent explores a platform with thousands of functions — without ever loading thousands of tool schemas into its context window.

01 / PROBLEM

2500 tools would blow the context window

MCP normally advertises every tool in one flat tools/list response. That is fine for a demo with 9 tools. The target is a platform with ~50 modules × ~50 functions — and dumping all of their schemas into the agent's context defeats the entire purpose.

2,500
operations at full scale (50 modules × 50 functions)
1 list
every schema pushed to the agent up front, whether used or not
~5
tools actually needed with progressive disclosure
Flat tools/list
2,500 schemas loaded
Progressive
5
Tools present in tools/list at connection time — lower is better.
02 / NAVIGATION

A recursive tree the agent walks on demand

Every node returns the same shape — { description, submodules[], functions[] } — so the agent recurses the same way at any depth. A node can hold sub-modules and functions; a leaf simply has no sub-modules. The root tools/list stays tiny no matter how deep the tree grows.

Bulk explore

explore(["reservation.availability", "reservation.search"]) — fetch several nodes in one round-trip instead of walking them one by one.

Wildcard explore

explore("reservation.*") expands a whole subtree; explore("*") returns the entire tree — flattened descendants in a single response.

03 / EXECUTION

Two ways to run a function — the agent chooses

Navigation is shared. The only fork is how a discovered function gets executed. Both paths are supported; the agent picks per situation.

Path B · invoke

Stateless dispatcher — the default

One universal invoke(name, args) tool runs any function by name. The function never enters tools/list; the server validates args against its Zod schema and executes.

  • No server-side state — fully stateless
  • tools/list stays tiny forever
  • Client fully controls what it keeps in context
  • No native per-function schema in the client
Path A · load_tools

Native promotion — for repeated use

load_tools([...]) promotes selected functions to real MCP tools with full schemas. Backed by a per-token selection store, re-read on every request.

  • Native tools — client-side schema validation
  • Ideal when calling the same fns repeatedly
  • Needs a server-side selection store (per token)
  • Two-step refresh: load → re-list
DimensionB · invokeA · load_tools
Function becomes…an argument to one invoke toola real, native MCP tool
Server-side statenone (stateless)required — per-token store
tools/list sizefixed & tinygrows with loaded tools
Refresh mechanismnone neededload, then re-list
Arg validationserver-side, inside invokenative, client-side
Best when…one-off / exploratory callsrepeated calls this session
Why the store is unavoidable for Path A: the transport is stateless — a fresh MCP server is built on every request. For a loaded function to reappear in the next tools/list, the selection must be persisted server-side (keyed by bearer token) and re-read while the tool set is rebuilt. Path B avoids this entirely.
04 / BULK INVOKE

Batch calls with per-operation parallel-safety

Every operation declares parallelSafe (defaulting to true for reads, false for writes). A bulk invoke([...]) runs the parallel-safe calls concurrently and everything else in submission order — one round-trip, correct ordering guaranteed.

Parallel-safe
(run together)
read searchAvailability read listReservations read getReservation
Sequential
(in order)
write createReservation write cancelReservation
Parallel-safe — resolved with Promise.all Order-sensitive — awaited one by one
Not in v1: dependent / chained calls (where step 2 needs step 1's result via a $1.field reference syntax). The agent sequences those itself — one extra round-trip keeps the server simple.
05 / WORKFLOW

A real booking spans three branches

The agent gathers functions from different parts of the tree — availability, booking, and search — then executes. Navigation is cheap (descriptions only); execution touches just the functions it chose.

Navigate & execute

explore()
sees the reservation module
explore("reservation.*")
availability · booking · search · admin
searchAvailability(date, 4) → slot_42
from reservation.availability
createReservation(slot_42, …) → booking_99
from reservation.booking
listReservations() → validates booking_99 ✓
from reservation.search — a different branch
Navigate Execute
agent ⇄ mcp
▸ explore() → app + [reservation, crm, task] ▸ explore("reservation.*") → availability · booking · search # gathered 3 fns from 2 branches ▸ invoke("searchAvailability", {date, partySize:4}) → { slotId: "slot_42" } ▸ invoke("createReservation", {slotId:"slot_42", …}) → { id: "booking_99" } ▸ invoke("listReservations", {}) → [ booking_99 ] ✓ validated
Under Path A, the agent would first load_tools([...]) and then call each function natively. Under Path B (shown) it calls invoke(...) directly. The navigation and the tree are identical either way.
06 / SURFACE

Everything runs on five always-on tools

Regardless of whether the platform exposes 9 or 2,500 operations, this is the entire root surface the agent ever sees at connection time.

explore — walk the tree (bulk + wildcard) describe_tool — full schema for a function invoke — run a function statelessly load_tools — promote to native tools unload_tools — remove native tools