> ## Documentation Index
> Fetch the complete documentation index at: https://docs.3ngram.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP surface budget

> Why 3ngram's tool cap was its own and not the protocol's, why a measured eval replaced it, how to choose between a tool, a resource and a prompt, and what the surface should become next.

**Status: the cap question is settled and shipped; the proposals below are still design.**
This page existed to settle *how* the MCP surface should grow before the next feature
argued about it, because the budget was over-subscribed and the rule everyone was arguing
under turned out not to be a protocol rule at all. That argument is over: the number is
gone and the thing it stood in for is measured. What remains open is the proposal list.

## 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](https://modelcontextprotocol.io/specification/2026-07-28/server/tools)
  defines no maximum or recommended tool count. What it constrains is tool *name* length
  (1–128 characters), and it gives `tools/list` **pagination** — which anticipates
  servers with more tools than fit in one response.
* The pinned SDK enforces no count limit anywhere.

<Note>
  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.
</Note>

The real constraint is **model behaviour**, and it is about description quality rather
than count: tool-selection accuracy degrades as the list grows, and every description is
re-sent on every `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

|              | Use when                                                                             | Competes in `tools/list` |
| ------------ | ------------------------------------------------------------------------------------ | ------------------------ |
| **Tool**     | The agent must *do* something, or the read is a **query** (filters, ranking, fusion) | Yes                      |
| **Resource** | The thing is **addressable** by a stable identifier and is *read*                    | No                       |
| **Prompt**   | The agent needs *orienting* toward a workflow it would not start on its own          | No                       |

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](/concepts/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](/concepts/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.

<Note>
  **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](/concepts/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.
</Note>

## 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:

```json theme={null}
{ "type": "resource_link", "uri": "threengram://memory/{id}", "name": "…" }
```

`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](/concepts/mcp-design)).

<Warning>
  **This must be additive. Do not retire `get_memories` on the strength of it.**

  The specification places no `MUST` on clients to follow a resource link, and explicitly
  notes that linked resources need not appear in `resources/list`. A client is free to
  ignore the block entirely. Retiring the tool would then delete the truncated-read path
  for those clients and leave a search result that cannot be expanded at all.

  This is the same trap as the text mirror in `ok()`: the fallback exists for clients whose
  behaviour we cannot observe, and removing it converts an unknown into a silent failure.
</Warning>

**Staged, with an evidence gate:**

1. Emit `resource_link` blocks alongside today's structured hits. Purely additive, no
   client can regress.
2. Gather evidence that real clients follow them.
3. Only then consider retiring `get_memories` — and that becomes a genuine JTBD
   argument, not a budget trick.

Honest tradeoff at step 3: twenty bodies becomes twenty resource reads rather than one
batched call. Chattier on a cold cache, cheaper on every later session.

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

A client walks the index and fetches bodies that are likely already cached from any
earlier read. The expensive half is the half that caches well.

This does **not** remove the need for a curation tool — create, rename, archive, delete,
pin, unpin are writes, and MCP resources are read-only. [Threads](/concepts/threads)
already considered and rejected folding these into `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

| Step                                       | Unblocks                             | Gate                                                                                |
| ------------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------------- |
| ~~Settle `manage_context`~~                | ~~Threads~~                          | **Done** — reservation released                                                     |
| Resource links (additive)                  | Evidence for retiring `get_memories` | Client follow-through                                                               |
| Thread index resource                      | Threads' read path                   | Ships with threads                                                                  |
| ~~`review` prompt + raise the prompt cap~~ | Consolidation loop                   | **Done** for the cap — the `review` prompt is still open, and now needs only a JTBD |
| ~~Re-set the tool cap~~                    | —                                    | **Done** — replaced by a gated eval, not re-set to another number                   |

<Note>
  **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.
</Note>

## What is deliberately not here

* **`subscriptions/listen`** — held-open streams defeat the stateless deploy model. See
  the catalog-staleness note in [MCP design](/concepts/mcp-design).
* **`resources/list`** for 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.

Multi-round-trip confirmation and the Tasks extension are tracked separately and remain
the most valuable unbuilt protocol features.
