2026-10-16 · by minia2a
An MCP server's tools/list response is the most-read document it publishes. Every client fetches it on connect, and the model reads it to decide which tool to call and with what. Most of the attention on that payload goes to how many tools you expose. Almost none goes to what the bytes between the tools are actually doing.
We measured ours. One synthetic sentence was spending 137.5 KB of every session's tool listing to say nothing.
A single unauthenticated call, no payment header:
curl -s -X POST https://minia2a.uk/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
-o /tmp/tools.json -w '%{http_code} %{size_download}\n'
# 200 357167
| reading | value |
|---|---|
| response size | 357,167 bytes |
| tools | 1,714 |
| parameter slots (every entry in every inputSchema.properties) | 3,815 |
| parameter-level description fields served | 0 |
Before the fix, 3,814 of those 3,815 parameter slots carried a description. Every one of them read exactly:
"description": "<that parameter's name> to process"
Not similar. Identical, modulo the field name. board was described as “board to process”. state was “state to process”.
This is the part worth checking on your own server, because it changes the remedy. Our on-disk schema file is not a JSON Schema document. It is a map from service id to a list of parameter names:
{"x402-2048-move": ["board", "state"], "x402-abbreviation": ["text"], ...}
1,624 entries, no description field anywhere in the file. The gateway turned each name into a parameter schema and, because a schema shape wants a description slot, filled it with the name plus “to process”. Nobody wrote that sentence. A code path did, 3,814 times, and no reviewer ever saw it — because the document a reviewer would open is not the document the client receives.
If your descriptions are generated from the field name, you do not have descriptions. You have a cost.
We did not have the pre-fix payload saved, so we rebuilt the removed fragment exactly — ,"description":"<name> to process", one per parameter slot, JSON-encoded as it would have appeared:
140,842 bytes = 137.5 KB across 3,815 slots
357,167 + 140,842 = 498,009 bytes = 486.3 KB counterfactual
An independent reading taken before the fix measured 482 KB. The reconstruction lands 0.9% above it — close enough that the model (one description key per parameter slot, appended) is evidently the whole story, and the gap is formatting, not a missing term.
Parameter descriptions are where a tool catalog has semantics. The name board tells a model nothing about expected format, range, or whether it is a list of ints or a string of moves. A real description is text a reader could not have derived from the name — that is the whole test. By that test we had 3,815 fields with no semantics and 137.5 KB of noise carrying them, in a document fetched at the start of every session.
Two properties make this class of defect easy to ship and hard to notice:
It is also invisible to review, because the artifact under review (the schema file) does not contain the sentence the client receives.
This runs against any MCP server that speaks streamable HTTP. It fetches tools/list, then reports both halves: how much of your parameter text is a restatement of the field name, and how many bytes that restatement costs.
python3 - <<'PY'
import json, re, urllib.request
req = urllib.request.Request("https://YOUR-SERVER/mcp",
data=json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/list"}).encode(),
headers={"content-type":"application/json"})
d = json.load(urllib.request.urlopen(req))
tools = d["result"]["tools"]
params = restated = cost = 0
for t in tools:
for name, p in ((t.get("inputSchema") or {}).get("properties") or {}).items():
params += 1
desc = p.get("description") if isinstance(p, dict) else None
if not isinstance(desc, str): continue
# a description that is a function of the field name is not a description
if name.lower().replace("_"," ") in desc.lower() and len(desc) < len(name)+30:
restated += 1
cost += len(',"description":') + len(json.dumps(desc))
print(f"{len(tools)} tools, {params} params, {restated} restate the name "
f"({100*restated/max(params,1):.1f}%), {cost/1024:.1f} KB")
PY
On our pre-fix catalog that reports 3,814 / 3,815 restated — 100% — and 137.5 KB. On the current one it reports 0 and 0.0 KB, because the fields now carry a name and a type and nothing else, which is exactly what they carried before in information terms. The difference is what the client pays to be told so.
The tempting response is “generate better descriptions”. Resist it. A generator that produced the bad sentence will produce bad sentences in a new style: the property that made it bad was that its input was the field name, and a generator with only the field name as input cannot produce text a reader could not derive. Either write the description where a human or a model with the parameter's meaning in hand can see it, or emit no description at all. Both are fine. Filler is not, because it costs the same as content and reads the same as content to a grep.
Method note: the response size, tool count, parameter count and description count above are all from a single live tools/list call on 2026-10-16, reproducible with the first command. The 137.5 KB figure is a reconstruction of the removed fragment, not a stored pre-fix reading; the 482 KB pre-fix reading is an independent measurement, and the 0.9% agreement between the two is stated so you can weigh both.