# One tools/list call cost 529 KB

> Source: <https://dev.to/minia2a/one-toolslist-call-cost-529-kb-22c2>
> Published: 2026-11-11 07:52:29+00:00

November 2026

MCP clients do not ship a catalogue to the model once and cache it. They ask the server for `tools/list`, and whatever comes back is what the model gets to see — every session, as part of its context. For a server with a dozen tools that is a few kilobytes and nobody thinks about it. For a server that maps a marketplace — one tool per service — it is a different object entirely.

On 11 November 2026 we measured ours, on the live endpoint, with the request a stock client sends:

```
POST /mcp  {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

→ 200, 529,287 bytes, 1,859 tools, no nextCursor
```

529 KB, at roughly 4 bytes per token, is about **132,000 tokens** — before the system prompt, before the user's question, before a single tool is called. That is larger than the context window of a good number of the models these clients are wired to. On 22 October the same call was 439,656 bytes over 1,711 tools. It grew 90 KB in 20 days, because the catalogue grew by 148 services.

Nothing crashes. That is the problem. A client with a smaller budget truncates the list, or leaves it out, or the model sees the first few hundred tools and treats the rest as absent. The failure mode is not an error message — it is an agent that connected successfully, saw a partial catalogue, and called something else. From the server side that looks like "nobody uses this", and it is indistinguishable from having no demand.

The same thing happens at a much smaller scale whenever a tool's `inputSchema` is verbose. We once shipped an object-level description on every tool; it added **212 KB** to the payload — one sentence repeated 1,610 times. It was reverted the same day. Tool-list cost is the sum of per-tool costs, and per-tool costs are usually decided by whoever wrote the last schema.

Our `tools/list` has accepted `limit`, `cursor`, `category` and `query` since 26 October, with the cursor bound to the filters that produced it (reusing a cursor after changing the filter is refused, not silently applied to another result set). Measured the same day:

| params | bytes | tools returned | ≈ tokens | 
|---|---|---|---|
| `{}` | 529,287 | 1,859 | ≈ 132,000 | 
| `{"limit":200}` | 53,008 | 202 | ≈ 13,000 | 
| `{"limit":100}` | 27,977 | 102 | ≈ 7,000 | 
| `{"limit":50}` | 14,427 | 52 | ≈ 3,600 | 
| `{"limit":20}` | 6,306 | 22 | ≈ 1,600 | 

So the fix looked like it already existed. Except that the feature was strictly opt-in: a request without `params` returned the whole list, byte for byte as before, because that was the compatibility promise made when paging landed. The real decision — *what does a client get when it asks for nothing?* — was deferred, and "deferred" meant "the 529 KB answer".

MCP hosts do not let a user say "please pass `limit: 100`". The client builds that request. A server that pages only on request is a server that pages only for callers who already knew to ask.

We are changing the default so that no `limit` — including no `params` at all — returns the first page of **100** service tools plus `nextCursor`, and the rest come from following the cursor. Explicit `limit` is unchanged, up to the existing 500 ceiling. Three properties keep this from being a downgrade:

`trial_count DESC, id` since an earlier fix, so the ordering is already a popularity ranking.`tools/call` resolves a name against the active catalogue; it never checked whether that name appeared in the list you were served. A model can search, get an id from `service_discovery` (still the first tool in the list), and call it.
We measured how concentrated usage actually is, across the 1,675 services that have ever been called (50,393 usage rows):

| first page | usage rows covered | share | 
|---|---|---|
| 50 tools | 42,580 | 84.5% | 
| 100 tools | 45,060 | 89.4% | 
| 200 tools | 46,894 | 93.1% | 

Going from 50 to 100 buys 4.9 points for about 14 KB; going from 100 to 200 buys 3.7 points for another 28 KB. 100 is where the curve bends. The row counts are not independent callers — bots and our own probes inflate them — but the decision needs the relative shape, and the shape is stable.

The honest cost of this change: a client that never follows `nextCursor` will now see 100 tools instead of 1,859. That is deliberate. Today that same client sees a 529 KB list that, in practice, it truncates anyway — except now the truncation is at a boundary we chose and the client can see, with a cursor that says there is more. We would rather be the server that says "here is page one" than the server that hands over a payload larger than the context and lets the client decide what to drop.

`tools/list` bytes, not your tool count.
The change is written, reviewed against the deployed source, and covered by a test that seeds more services than the default page holds — so a future edit that raises the default, or removes it, fails instead of silently restoring the 529 KB answer. It is queued behind the gateway binary deploy; this post will be updated with the live before/after when it ships.

*Originally published at [minia2a.uk](https://minia2a.uk/blog/mcp-tools-list-context-budget-november-2026).*
