MCP Server Design · AgentBridge
How an AI agent explores a platform with thousands of functions — without ever loading thousands of tool schemas into its 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.
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.
explore(["reservation.availability", "reservation.search"]) —
fetch several nodes in one round-trip instead of walking them one by one.
explore("reservation.*") expands a whole subtree; explore("*")
returns the entire tree — flattened descendants in a single response.
Navigation is shared. The only fork is how a discovered function gets executed. Both paths are supported; the agent picks per situation.
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.
tools/list stays tiny forever
load_tools([...]) promotes selected functions to real MCP
tools with full schemas. Backed by a per-token selection store, re-read on
every request.
| Dimension | B · invoke | A · load_tools |
|---|---|---|
| Function becomes… | an argument to one invoke tool | a real, native MCP tool |
| Server-side state | none (stateless) | required — per-token store |
| tools/list size | fixed & tiny | grows with loaded tools |
| Refresh mechanism | none needed | load, then re-list |
| Arg validation | server-side, inside invoke | native, client-side |
| Best when… | one-off / exploratory calls | repeated calls this session |
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.
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.
Promise.all
Order-sensitive — awaited one by one
$1.field reference syntax). The agent sequences those itself — one extra
round-trip keeps the server simple.
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.
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.
Regardless of whether the platform exposes 9 or 2,500 operations, this is the entire root surface the agent ever sees at connection time.