The cap was ours — and it is gone
MAX_TOOLS = 12 and MAX_PROMPTS = 2 were 3ngram’s own discipline. They entered at
the v1.0.0 launch commit, were not sourced to anything upstream, and were removed once
the property they proxied for could be measured. AGENTS.md hard rule 8 is now an
evidence test rather than a number, gated by the tool-selection eval slice
(eval/src/tool-selection.mjs, floors in eval/fixtures/floors.json).
The verification below is kept because it is why the number could go.
Verified two ways:
- The 2026-07-28 tools specification
defines no maximum or recommended tool count. What it constrains is tool name length
(1–128 characters), and it gives
tools/listpagination — which anticipates servers with more tools than fit in one response. - The pinned SDK enforces no count limit anywhere.
This mattered because “the protocol says 12” would have been a reason to stop designing.
“We chose 12 to force evidence” was a reason to produce the evidence and then
re-choose, which is what happened. The number was a forcing function attached to the
mcp-design.mdx rule, never a ceiling with independent authority.tools/list. Two well-separated tools cost less than one overloaded
one. That is what to optimise; the number was only ever a proxy.
So the proxy was replaced by the measurement. The tool-selection eval slice scores every
registered description against five agent utterances per tool and gates three numbers:
selection_accuracy_at_1 and selection_margin as floors, and max_description_overlap
— the largest pairwise cosine between two tool descriptions — as a ceiling. A twelfth
tool is not blocked; a twelfth tool that reads like an existing one is. That is the
constraint the count was always trying to express.
Choosing between a tool, a resource and a prompt
Two corrections to intuitions that have already caused mistakes here:
Resources are not a general pressure valve. They add no description to
tools/list,
so they cost nothing against selection accuracy — but a resource needs a URI. search
could never be one: a fused, filtered, ranked query has no address. Reach for a resource
when there is a name or id, not merely because the tool surface feels crowded.
Cacheability is a separate question from addressability. Per
MCP resources, a long TTL requires an immutable body. A
mutable thing can still be a resource; it just carries a short TTL and buys only the
description saving, not the caching win.
The budget today
Eleven tools registered, and no slot ledger any more — a twelfth tool needs a JTBD no existing tool covers, scenarios in the eval, and a run that holds the recorded floors and the overlap ceiling. Recorded at 11 tools:selection_accuracy_at_1 0.8545,
selection_margin 0.1097, max_description_overlap 0.6737 (briefing ~ handoff, the
closest pair on the current surface and the one to watch).
The get_facts chronological time-series read (range: {from?, to?}, MCP
design) is a concrete instance of the JTBD-first test working
against a candidate tool rather than for one: a get_series-shaped tool would have
covered the same job, but the job was already reachable by widening get_facts’s
existing query axis, so no new description entered tools/list at all — the cheapest
possible outcome under either regime.
Resolved 2026-08-08: the
manage_context reservation is released. It had held the
last slot since the v1.0.0 launch commit with no design document, no issue, and no
specification — four references, all of which merely reserved the name — while
threads had a complete design and needed it.The standoff was an artifact of treating 12 as though it came from the protocol. It
does not. The question was never who gets slot 12; it is whether each tool earns its
share of the description budget, because description overlap is what actually degrades
model selection accuracy. If manage_context becomes real, it competes on that footing
with a design attached.Proposals
1. search and handoff return resource links
get_memories exists for exactly one job — “the follow-up read for a truncated: true
search/handoff line” — and the protocol has a content type built for that job:
ResourceLinkSchema is part of ContentBlockSchema, so the pinned SDK can already emit
these in a tool result. A client could then fetch bodies itself, from a cache, with no
second tool call — and avoid the 2.11× text-mirror cost on the largest payload we serve
(see the output-size discipline note in MCP design).
Staged, with an evidence gate:
- Emit
resource_linkblocks alongside today’s structured hits. Purely additive, no client can regress. - Gather evidence that real clients follow them.
- Only then consider retiring
get_memories— and that becomes a genuine JTBD argument, not a budget trick.
2. Threads as an index resource plus one curation tool
Threads decompose along the read/write seam:threengram://thread/{name}— the index: member ids and minimal metadata. Short TTL, because membership mutates. Small by construction, so no firehose.threengram://memory/{id}— the bodies. Immutable, long TTL, already built.
configure_scope: scopes are
exclusive partitions, threads are overlapping collections, and one tool describing both
models confuses the agent reading the description. That argument stands.
What the resource does buy: threads never need a second tool for reading, and the
thread filter on search becomes a convenience rather than the only way in.
3. manage_context — done
Released, see above. Kept here so the reasoning survives the decision: a reservation with
no design cannot be weighed against a feature that has one.
4. Prompts are under-used
MAX_PROMPTS = 2 was self-imposed and is gone with its tool-side counterpart; the
protocol never set a limit. Prompts carry no tenant data and add no tool description, so
nothing but a JTBD stands between the surface and a third one.
The one clear gap is review_proposals has no prompt. It is the tool an agent is
least likely to invoke spontaneously — nothing in a session naturally suggests “check
what the consolidator proposed.” briefing and debrief orient session start and end; a
review prompt would orient the loop that keeps the corpus clean.
Note that SERVER_INSTRUCTIONS now carries cross-tool policy, so some of what a prompt
would have done reaches the model without one.
Sequencing
Resolved 2026-08-12: both caps are removed, and the eval slice replaced them. This row
said to re-set the cap last, and to record the reasoning as description overlap and
selection accuracy rather than a number inherited from launch day. What shipped does the
second half and skips the first: there is no new number.
MAX_TOOLS and MAX_PROMPTS are
deleted, and tool-selection moved from report-only to gated — floors on
selection_accuracy_at_1 and selection_margin, a ceiling on max_description_overlap,
baselined from the observed run at 11 tools.Re-setting the cap to 14 or 16 would have reproduced the original mistake at a higher
number: a figure with no measurement under it, which the next feature would argue about on
the same footing. A regression in routing now fails a required status check, and the
failure names the offending pair — which is the argument the count could never have.The ordering still held. The cap went last, after manage_context was settled and after
the slice had run report-only long enough to baseline from.What is deliberately not here
subscriptions/listen— held-open streams defeat the stateless deploy model. See the catalog-staleness note in MCP design.resources/listfor memories — corpus enumeration; the memory template returns nothing by design.- Completion for the memory
{id}— suggesting memory ids is the same enumeration through a different door. - Roots and Sampling — client capabilities a server cannot use, and Sampling is deprecated in this revision besides.
- Logging — deprecated by SEP-2577; OpenTelemetry covers it.