# MCP Progressive Tool Discovery: Native vs. Hand-Rolled

> A 3-tool MCP entry point cuts tools/list bytes 94% vs a flat 50-tool catalog. agentgateway's Search mode does this, Enterprise-only; here's the OSS version.

- Canonical URL: https://webofmike.com/progressive-discovery/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-10-18
- Last modified: 2026-10-18
- Tags: MCP, AI Gateways, Platform Engineering, AI Agents
- Cite as: Mike Moore, "MCP Progressive Tool Discovery: Native vs. Hand-Rolled", Web of Mike (webofmike.com), 2026-10-18. https://webofmike.com/progressive-discovery/


The 2026-08-22 MCP roadmap names progressive tool discovery as a priority, in almost these words: a server should offer a small entry point and reveal more of the catalog, rather than sending every tool definition up front. I wanted to know what it would take to actually build that, not just argue for it, so I built both sides: a flat 50-tool MCP catalog and a progressive one, fronted by the same real [agentgateway](https://github.com/agentgateway/agentgateway) v1.6.0, and measured the difference. Code at [themsquared/progressive-tool-discovery](https://github.com/themsquared/progressive-tool-discovery).

The honest finding was not the number. It was that the primitive for this already exists in agentgateway, and it's Enterprise-only. So this post also shows what you get for free and what you have to build yourself.

## This is not Code Mode

I [wrote about MCP tool catalogs growing out of control a few weeks ago](/conways-law-mcp-servers/), and the pushback I got most was "doesn't Code Mode already fix this?" It fixes a different problem.

Code Mode takes N tools and replaces them with one tool: a script executor. The model writes JavaScript that calls the underlying tools as functions, the gateway runs it in a sandbox, and only the composed result comes back. That's a real win for tasks that chain several tool calls, because the model stops paying context for every intermediate result.

Progressive discovery is a different idea. The tools stay tools. What changes is how much of the catalog the agent sees before it has asked for anything. A flat catalog sends every tool's full definition on `tools/list`, every time, no matter how many of those tools the agent will actually call this session. Progressive discovery keeps the initial response small and lets the agent ask for more.

Both ideas reduce tokens. Only one of them is about hiding the catalog until it's needed, which is what the roadmap is actually asking for.

## What agentgateway gives you natively, and what it doesn't

agentgateway does have a real primitive for this, and I want to be specific about where, because the public docs make it easy to conflate the two tiers.

[Solo Enterprise's agentgateway docs](https://docs.solo.io/agentgateway/latest/mcp/tool-mode/) describe four `toolMode` values on an MCP backend: `Standard` (pass through everything), `Search` (two meta-tools, `get_tool` and `invoke_tool`, that disclose one tool at a time), `Code` (a single `run_code` tool, the Code Mode shape), and `CodeSearch` (both). The docs say it plainly: "Tool modes are how agentgateway implements progressive disclosure for MCP." Search mode is exactly the mechanism this post is about, built in, configured with one field.

That field is `entMcp.toolMode`, set on an `EnterpriseAgentgatewayBackend` resource. Both names tell you what tier it's in. I checked the open-source side to be sure: [agentgateway.dev's MCP connectivity docs](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/) list configuration modes, servers, auth, guardrails, target policies, observability, and spec compatibility. No tool-mode page. The OSS binary this repo runs doesn't have the feature.

What OSS agentgateway v1.6.0 does have is [MCP multiplexing and separate-path routing](https://agentgateway.dev/docs/standalone/latest/documentation/mcp/configuration-modes/): put several MCP servers behind one backend and their tools get merged into one `tools/list` (prefixed by target name to avoid collisions), or give each server its own route. That's federation, and it's the opposite direction from what I needed here. Multiplexing makes catalogs bigger and unified. Progressive discovery needs one catalog to stay small. So this demo uses separate-path routing: one route per catalog shape, not one merged endpoint.

I also checked [agentregistry v0.4.0](https://github.com/agentregistry-dev/agentregistry), since it's this campaign's other registry project and the obvious thing to ask about. It's a catalog of which MCP servers, agents, and skills exist across npm, PyPI, OCI, and raw endpoints, with `arctl` to deploy them. That's server-level discovery. Tool-level discovery, how one running server shapes its own `tools/list`, isn't its job, and its API has nothing resembling pagination or hierarchy for that. It's a good way to find the progressive-catalog server in the first place. It has no opinion on how that server behaves once you've found it, so I didn't force it into the architecture below.

## Building the entry point

Since the OSS gateway doesn't have `Search` mode, I built the mechanism it implements, by hand, in the MCP server itself. It's about 130 lines across two files.

The server exposes three tools from `tools/list`, always, no matter how big the real catalog behind it gets:

```python
ENTRY_POINT_TOOLS = [
    {"name": "list_categories", "description": "List the tool categories ..."},
    {"name": "expand_category", "description": "List the tools in one category ..."},
    {"name": "call_tool", "description": "Invoke a specific tool by name ..."},
]
```

`list_categories` returns a short summary: category names, descriptions, tool counts. `expand_category` takes a category name and returns the full tool definitions, schemas included, for just that category. `call_tool` dispatches to the real handler once the agent knows a tool's exact name and arguments. The agent never sees the other categories' tools unless it asks for them.

The flat server, for comparison, is the same catalog with none of that: `tools/list` returns all 50 tool definitions, 5 categories of 10, every time. Both servers share one `catalog_data.py` so the comparison is apples to apples, not two different catalogs with two different vocabularies.

Both are plain Python standard library HTTP servers, no MCP SDK dependency. That wasn't laziness: agentgateway's own Virtual MCP docs call out a real breakage where `mcp-server-time`'s SDK version pin fights `uvx`'s dependency resolution and the server fails to start. Skipping the SDK means skipping that whole class of problem for a demo this size, at the cost of not being a spec-complete MCP server (no SSE push, no session resumption). That's also why `config.yaml` runs agentgateway with `statefulMode: stateless` in front of both servers, so agentgateway doesn't try to pin a session to a backend neither server actually tracks.

`config.yaml` itself is agentgateway's routing-based configuration: one gateway on port 3000, two routes, each its own MCP backend with one target.

```yaml
routes:
  - name: flat
    matches:
      - path: { pathPrefix: /flat }
    backends:
      - mcp:
          statefulMode: stateless
          targets:
            - name: flat
              mcp: { host: http://flat-server:3005/mcp/ }
  - name: progressive
    matches:
      - path: { pathPrefix: /progressive }
    backends:
      - mcp:
          statefulMode: stateless
          targets:
            - name: progressive
              mcp: { host: http://progressive-server:3006/mcp/ }
```

Real agentgateway v1.6.0, unmodified. The entry-point behavior is entirely in the Python server, which is the honest shape of "here's what you hand-roll" when the gateway doesn't give you the feature.

## The numbers

I ran `measure.py` against the two routes through the actual gateway, not against the Python servers directly, so the byte counts include whatever agentgateway adds on the wire. Real output from a run:

```
agentgateway v1.6.0, two routes, same 50-tool catalog, two shapes.

                      tools/list bytes   tools exposed
flat                             16358              50
progressive                        922               3

tools/list reduction: 94.4% fewer bytes, 47 fewer tools in the initial exposure.

Scenario: the agent only ever needs the billing category (10 of 50 tools).

  flat tools/list alone:                        16358 bytes, 50 tools seen
  progressive tools/list:                         922 bytes, 3 tools seen
  + list_categories call:                         781 bytes
  + expand_category('billing') call:             3818 bytes, 10 tools revealed
  progressive total for this scenario:            5521 bytes

  net bytes saved vs. the flat catalog: 66.2%
```

Two things worth sitting with. First, the `tools/list` gap (94.4%) only grows as the real catalog grows, because the progressive entry point is fixed at 3 tools regardless of what's behind it. A 500-tool backend would widen this gap, not close it. Second, the realistic scenario (66.2%) is smaller than the headline number on purpose: it includes the cost of actually using a tool, not just listing the catalog. That's the honest number, and it's still a two-thirds reduction for an agent that only needed one of five categories.

These are bytes on the wire, not model tokens. I didn't run a tokenizer over them, so I'm not going to claim a token figure I didn't measure. What I can say is that both payloads are the same kind of content (tool names, prose descriptions, JSON schemas), so a tokenizer applied to both would move in the same direction as the byte counts, even if the exact percentage shifted. Anthropic's own numbers on what this does to model accuracy, not just token count, are in [the post this one follows up on](/conways-law-mcp-servers/).

## Running it yourself

Docker with Compose v2+, nothing else. Validated on Docker 29.8.1 / Compose v5.5.1, arm64 macOS, against `ghcr.io/agentgateway/agentgateway:v1.6.0`.

```bash
git clone https://github.com/themsquared/progressive-tool-discovery
cd progressive-tool-discovery
./demo.sh
```

`demo.sh` builds the two servers, starts agentgateway, waits for it to answer, runs the measurement script, and tears everything down. Well under 15 minutes including image pulls, and every number it prints comes from that run, not from this post.

## Where this leaves the roadmap item

The roadmap is right that catalogs shouldn't front-load everything. What this build adds is the part that's easy to skip past in an argument: agentgateway already has the feature the roadmap is describing, and it's behind a license tier. If you're on open-source agentgateway, the pattern isn't unavailable to you, it's just not a config field. It's about 130 lines in your MCP server, the same shape Enterprise's `Search` mode implements for you: a small fixed entry point, categories first, tool definitions on demand.

If your catalog is small and stable, none of this matters: standard mode is correct and simpler. If it's forty tools and growing, the entry point is the part to build first, whichever tier you're on. For the architectural case for why catalogs end up this size in the first place, and why Code Mode and tool search don't fix the governance problem underneath, see [Conway's Law Is Coming for Your MCP Servers](/conways-law-mcp-servers/). For another gateway-layer trick that shrinks what an agent has to read, [serving markdown instead of HTML at the gateway](/markdown-for-agents-at-the-gateway/) is the same instinct applied to a different payload.

Code: [themsquared/progressive-tool-discovery](https://github.com/themsquared/progressive-tool-discovery), Apache-2.0.

