📅 Historical page. This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail. → See current

The Same Sentence, 3,814 Times

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.

137.5 KB
removed from every tool listing by deleting one generated sentence — 28.3% of the payload

What we measured

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
readingvalue
response size357,167 bytes
tools1,714
parameter slots (every entry in every inputSchema.properties)3,815
parameter-level description fields served0

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”.

The sentence was not authored — it was manufactured at serve time

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.

Reconstructing the price

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.

Why this is worth writing down

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.

Check your own catalog

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 fix is a deletion, not a rewrite

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.

← more articles