If your protocol tells every new agent "call this tool first," that tool's response size is part of your interface contract. An orientation payload that a client will not inline is worse than a thin one: the agent either truncates it, spills it to a file it may not be able to re-read, or proceeds unoriented.
That is what happened here. A session-context tool — the documented first call, carrying tenant identity, persona, conventions and a live state snapshot — measured 111,619 characters on a modest tenant. Roughly 90,000 of those were a single field: the full per-tool catalogue, inlined on every call.
Why it grows without anyone deciding to grow it
Nothing about that field was wrong when it was added. A categorised map of every callable tool is genuinely useful orientation, and at forty tools it cost little. The catalogue then grew past two hundred tools, each with a name, category and one-line summary. The payload grew with it, linearly and invisibly, because no test asserted a size and no reviewer sees a response's byte count in a diff.
The failure only appears at the boundary — in a client's tool-result limit, not in the server. So the server's own tests stayed green while the tool became unusable in exactly the situation it was designed for: a context-free agent on its first call.
There was a second, sharper detail. The tool already had a verbose parameter, documented as "reserved for future use." Its lean mode had been designed and never built. Anyone hitting the size limit would read the parameter list, pass verbose: false or omit it, and receive the same enormous response — the documented remedy was inert. A reserved flag on a shipped interface is worse than no flag: it looks like the answer.
The shape that fixes it
Default to the shape of the thing and pointers to expand it; gate the exhaustive listing behind an explicit opt-in.
// default — orientation, not inventory
{
"tenant": { "id": "…", "business_name": "…", "timezone": "…" },
"user": { "persona": "operator" },
"conventions": { /* the handful of rules that prevent mistakes */ },
"operational_state": { "active_clients": 33, "jobs_this_week": 30 },
"tool_categories": { "clients": 13, "contractors": 12, "jobs": 4, "invoices": 6 },
"deep_dives": [ "search_tools(query) finds a tool by intent", "…" ]
}
tool_categories — category to count — tells the agent the toolkit's shape in a few dozen characters where the full map took ninety thousand. It answers the only question orientation needs to answer ("is there a jobs surface here, and roughly how deep?"), and the follow-up is a different call: intent search for a specific tool, or the flat catalogue for the authoritative list. Long prose fields get a cap and a pointer rather than truncation-by-accident.
verbose: true returns the full map, unchanged, for the caller that genuinely wants inventory — a documentation generator, say, rather than an operator's assistant.
Assert the budget
Size is a contract, so test it like one:
const res = await call({}); // default shape
expect(JSON.stringify(res).length).toBeLessThan(20_000);
expect(res).not.toHaveProperty("tool_map"); // the balloon stays out of the default
expect(res).toHaveProperty("tool_categories"); // …replaced by its shape
expect((await call({ verbose: true }))).toHaveProperty("tool_map"); // detail still reachable
Seed the fixture tenant with enough data that the assertion means something — a hundred-plus jobs and a full contractor roster — otherwise it passes on an empty tenant and tells you nothing.
Transferable rules
- A response size is an interface constraint. If a documented "call this first" tool can exceed a client's inline limit, it has a bug regardless of its content.
- Default to shape, opt in to inventory. Counts and pointers beat exhaustive lists for orientation; the exhaustive list stays one explicit call away.
- Never ship a reserved parameter. Either implement it or remove it — a flag that silently does nothing is a false remedy.
- Cap free-text fields with an explicit "truncated, ask for verbose" marker rather than letting a client cut them mid-sentence.
- Test the ceiling on realistic data. Byte counts do not appear in code review; they appear in assertions.
Related
- Tool discoverability for agents — the orient-then-search pattern this payload serves.
- Connecting Biloh over MCP — the connection and persona model.
- Making a multi-connector MCP setup safe to act on — the identity fields orientation must always carry.