Book a call
LESSON14mVERIFIED 2026-08-03 · CLAUDE CODE 2.1.193 · CODEX CLI 0.146.0 · GITHUB COPILOT CLI 1.0.77 · ANTIGRAVITY CLI 1.1.10

Too many tools: what a crowded tool list costs you

One flag took the same trivial prompt from 26.6k input tokens to 66.5k. What a crowded tool list costs every turn — and why denying a tool doesn't remove it.

Nobody installs "too many tools"

12, 30, 90, etc days after you start using MCP you add another server to your setup, just because you need it for a ticket from Jira, then another one to avoid setting up the database connection every time from scratch, and then the company-wide one that your colleagues install in their projects. All of these are reasonable steps on a given day but none of them tells you how many tools you have now.

What actually happens is that nobody really decides to have sixty tools. The number you accumulate goes up as long as you keep working, and the first sign that it's too much is that your sessions feel less "sharp" than they used to (like when you try to remember something — you know it's there but can't seem to put your finger on it). Say that out loud and you usually hear "it's normal, it's MCP that eats context", which gets treated as complaining. It isn't. It's a claim about a number, the number is measurable, and on our machine the measurement took one flag.

Measuring with a single flag

GitHub Copilot CLI ships with GitHub's own MCP server built in, and its help tells you that not all of that server's tools are enabled by default — plus a flag that turns the rest on. Same vendor, same server, same prompt, and the only thing that differs is the number of tools on the list.

So we ran this, three ways:

TEXT
Reply with exactly: ok

Four tokens came back every time. Then we ran the whole set again from a different working directory, because one table of absolute numbers is worth very little:

RunTool listInput, scratch dirInput, inside a repo
--disable-builtin-mcpsno built-in MCP servers24.4k45.5k
(default)the CLI's curated GitHub subset26.6k47.7k
--enable-all-github-mcp-toolsthe full GitHub toolset66.5k88.3k

Read the columns first and the rows second:

  • Moving the same command into a real repository added about 21k tokens before any tool was involved at all, so if you were hoping to compare your floor against ours, don't.

  • The cost of the flag barely moved: 39.9k in the scratch directory, 40.6k in the repo. The floor is local. The tool bill is the tool bill.

And you don't pay it once. Tool definitions go up the wire on every request, so this is 40k off the top of every turn of every session for the rest of the day — the same tax, paid again, whether or not you go anywhere near GitHub. In the repo run, where all three arms were billed cold, the identical four-token answer went from 11.9 AI credits on the default subset to 22.1 with everything switched on.

So GitHub decided to ship the subset as the default — they looked at their server in their own client and figured that showing the entire toolset is a bit much, and provided a flag to override it. Overriding it is the expensive direction.

What a tool actually costs

A tool doesn't cost just its name. It costs its interface — the name, the description the author wrote for the model, and the entire JSON schema for every parameter (including the optional ones that nobody ever uses). We queried two servers over the wire for their tool lists and measured the replies:

ServerToolstools/list JSON
CodeScene code-health MCP 1.4.12539,057 chars
MCP "everything" reference server 2.0.0137,653 chars

That's 1,562 characters per tool on the real one, with its biggest single definition (rules_config_set_threshold) being 2,486 characters on its own. The reference server averages 589.

The gap is the honest lesson here. A reference server exists to show you what the protocol looks like, so its definitions stay brief; a server built to do an actual job carries the business with it, so its tools are verbose. Estimate off the reference figure and you'll be out by a factor of 2.6.

Cursor documents the same thing in tokens instead of characters:

Tool definitions are included in the input context (usually a few hundred tokens per tool)

Which lands in the same place our character counts do, and is worth having as a vendor's own arithmetic rather than ours.

And Anthropic's engineering write-up says the scaled-up version:

In cases where agents are connected to thousands of tools, they'll need to process hundreds of thousands of tokens before reading a request.

The example they work through goes from 150,000 tokens to 2,000 once the definitions stop being loaded upfront.

The selection cost

Tokens are the cost you can see. The other one is that every tool you add is another candidate the model has to rule out, and that gets harder in a way that isn't linear.

The measured version we know of is the 2025 RAG-MCP paper, where they set up a single capable server and then added N−1 distractors from a registry of over 4,400 public servers, with N ranging from 1 to 11,100. Their own reading of the results, by position in the pool:

  • below 30, "success rates above 90% when the candidate pool is minimal"

  • in the range 31–70, "clusters of purple emerge intermittently, reflecting lower accuracy as semantic overlap among MCP descriptions increases"

  • beyond about 100, failure dominates

On their headline benchmark the unaided baseline picked the right server 13.62% of the time. Read that with the caveats attached: a single base model (qwen-max-0125), a single web-search benchmark, and pools far bigger than anything you have.

What transfers is not the percentage, it's the shape of the curve — and the mechanism the authors reach for when they explain the middle of it, which is overlap rather than count. Sixty tools that do sixty different things are a much easier list than twelve where three of them sort of read a file.

Which matches what the failure looks like in practice. When you have too many tools the model doesn't usually abstain — it chooses the one next to it:

  • The search tool from the wrong server

  • A generic HTTP call instead of the specific API

  • The tool whose description happened to contain your noun

And then you get an answer that sounds right, is confidently wrong about which system it came from, and has nothing in the transcript flagging it.

Denial versus removal

The most instructive mistake is a frequent one, because there are two knobs that both sound like turning a tool off:

  • The first one decides whether the agent can use the tool without asking you (Copilot: --allow-tool, --deny-tool; Antigravity: mcp(server/tool) permission rules)

  • The second one decides whether the model is told about it at all (Copilot: --available-tools, --excluded-tools — described in its own reference as restricting "the set of tools that the AI model is aware of, and can therefore choose from"; Antigravity: disabledTools — documented as "Tool names to withhold from the model.")

On most harnesses only the second one is a context lever. Claude Code is the exception, and the variant below says which you're on.

Approval control is real control — GitHub's own wording for --deny-tool is that the model "cannot use that tool at all, even if it would be the best choice" — but look at what that sentence concedes. The model is still choosing it. The definition is still in the window, still costing its schema on every turn, still crowding the list the model has to discriminate against. What you bought was a refusal at the point of use, not a shorter list.

So if you built a wall of deny rules to tidy things up and the floor didn't move, that's why. Worth checking which knob you're holding before you conclude that trimming doesn't work.

Deferred loading, adopted and withdrawn

The obvious thing would be to not send the definitions until they're needed — and the field hasn't agreed on it:

  • Claude Code enables it by default. Its docs say tool search "keeps MCP context usage low by deferring tool definitions until Claude needs them. Only tool names and server instructions load at session start, so adding more MCP servers has minimal impact on your context window." Tested against that 39,057-character server from the table above, adding it to a headless run moved total input by 4 tokens, and that held on every repeat except the first cold start of a session.

  • Codex went the opposite way. Its own feature-flag list still carries tool_search and tool_search_always_defer_mcp_tools, both marked removed. It built the deferral and then pulled it. On 0.146.0 the only lever is filtering.

But deferral doesn't solve everything:

  • It changes when you pay rather than whether you pay at all — discovery is a round trip the model spends before it can act

  • And it does nothing about selection. What loads at session start is names plus whatever the server put in its instructions, so the model is discriminating on less, not more. Twelve overlapping tools are harder to tell apart that way, not easier. It reduces the bill, not the overlap problem.

Trimming, ordered by payoff

  1. Count first. You need to know how many tools you have before you can decide the number is too high. Expect less help than you'd think — two of the six harnesses covered here will enumerate a server's tools, the rest only report which servers are connected, and the variant below says which you're on. The universal fallback is asking the server yourself: a tools/list call over stdio, which costs nothing.

  2. Scope servers to the project they're for. The biggest source of bloat we've seen is installing a server in your user account and then loading it in every repo forever, just because you needed it for a ticket once. All six of these harnesses read a project-scoped config alongside the global one, and project scope is usually what you want.

  3. Choose a subset over the full set. If a server offers toolsets — GitHub's is the obvious one — choose the toolset. The vendor already had a think about it and shipped their answer as the default.

  4. Remove the "I needed this for an afternoon" server. It's still there. Months later.

  5. Ask whether it should have been a tool at all. Sometimes what you actually need is a CLI. If a server wraps a CLI your agent can already run, now you have schemas taking up space in the window to get to a thing you could reach with a shell command. That question has its own lesson, and it's probably the cheapest one to ask first.

IN YOUR HARNESS

Seeing it and trimming it in Claude Code

Count it: claude mcp list. Its own help line: "List configured MCP servers. Unapproved .mcp.json servers are shown as ⏸ Pending approval and not connected to; approved servers are health-checked."

For every server it prints a single line with its name and connection status. No tool counts. Ours came back as seven servers, six connected and one needing authentication, and that's the honest limit of the view — for an actual number you have to ask the server yourself over stdio.

Turn a server off without deleting it: disabledMcpjsonServers in settings, documented as "List of specific MCP servers from .mcp.json files to reject". Copy that key rather than typing it — the casing is Mcpjson, not the McpJson your fingers will want.

JSON
{
  "disabledMcpjsonServers": ["heavy-server"]
}

Take tools out of context: usually denying sth doesn't remove it, but MCP tools here work slightly differently — permission deny rules are what removes a tool from the context, and this is the one place where the deny-versus-remove distinction from the shared half doesn't apply. The docs are explicit that "mcp__*" "matches every MCP tool across all servers", and that "a tool matched by a bare-name glob deny rule is removed from Claude's context, the same as a bare tool name". So a deny rule here is a context lever, not just a prompt.

The thing that changes the arithmetic: tool search is on by default. "MCP tools are deferred rather than loaded into context upfront, and Claude uses a search tool to discover relevant ones when a task needs them." We pointed it at the 25-tool server whose tools/list is 39,057 characters, and adding it to a headless run moved total input by 4 tokens.

You can turn the dial with ENABLE_TOOL_SEARCH: false loads everything upfront the old way, auto loads schemas upfront when they fit within 10% of the context window and defers the overflow, and auto:N sets your own percentage.

BASH
# see the old behaviour, for comparison
ENABLE_TOOL_SEARCH=false claude

Gotcha: deferral needs a model that supports tool_reference blocks — Sonnet 4.5, Haiku 4.5, Opus 4.5 and later. And the docs are explicit that it turns itself off when ANTHROPIC_BASE_URL points at a non-first-party host, "since most proxies don't forward tool_reference blocks". So the same config behind a corporate gateway can carry a very different tool bill from the one you measured at home. If that's your setup, measure it there rather than assuming the default applies.

Seeing it and trimming it in Codex CLI

Count it: "In the codex TUI, use /mcp to see your active MCP servers." From the shell, codex mcp list prints one row per server — for a stdio server that's Name, Command, Args, Env, Cwd, Status and Auth — and no column for tools anywhere. On 0.146.0 we couldn't get a tool count out of either list or get.

Turn a server off without deleting it: set enabled to false in ~/.codex/config.toml, which the docs describe as "Set false to disable a server without deleting it." We set it, and the row came back reading disabled under Status.

Filter individual tools: this is the strongest thing Codex has here, and you can only reach it from the config file — there's no such flag on codex mcp add.

TOML
[mcp_servers.demo]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-everything"]
enabled = true
enabled_tools = ["echo", "add"]
disabled_tools = ["add"]

As you can see we set up a demo server with two tools, echo and add, and then excluded one of them. enabled_tools is the allow list, disabled_tools is "applied after enabled_tools" — so deny wins on a collision, which is why add ends up excluded. Reading it back is the only way to confirm a filter actually took:

BASH
codex mcp get demo --json

That echoes the resolved config with enabled_tools and disabled_tools filled in. Just keep in mind that the same JSON output carries any bearer token in cleartext, so run it somewhere appropriate.

Gotcha: for a moment Codex had deferral too, and then removed it. The flags are still listed by codex features list:

TEXT
$ codex features list | grep tool_search
tool_search                          removed            false
tool_search_always_defer_mcp_tools   removed            true

Which means every tool you configure here is in the window from the first turn. Filtering isn't a tidiness exercise on this harness — it's the only lever there is.

Seeing it and trimming it in GitHub Copilot CLI

Count it: in the session, /mcp show"This displays all configured MCP servers and their current status" — and /mcp show SERVER-NAME for one server, which is the form that lists that server's tools. From the shell, copilot mcp list groups servers by origin: User, Workspace, Plugin, Builtin. For the token side, the footer at the end of a non-interactive run prints your input total without being asked, which is where the numbers in the table above came from.

Know which knob you're holding. This is the harness where the distinction matters most, because it ships both pairs:

BASH
# approval only — the tool is still loaded, still costs you, still selectable
copilot --deny-tool='MyMCP(denied_tool)'

# availability — the model never sees these
copilot --excluded-tools='playwright'
copilot --available-tools='shell,write'

--available-tools and --excluded-tools are the ones the reference describes as restricting "the set of tools that the AI model is aware of, and can therefore choose from, when it determines how to complete a task". The docs show that pair with plain comma-separated names, and the parenthesised Server(tool) form on the approval pair — their own examples there are MyMCP(create_issue) and plain MyMCP, and "The argument is optional—omitting it matches all tools of that kind." Parentheses, not colons.

Turn a server off: --disable-mcp-server <server-name>, repeatable, or --disable-builtin-mcps for all of the built-ins in one go. Worth knowing which built-ins you have before you reach for the blunt one, because the docs and the binary disagree: GitHub's documentation lists four (github-mcp-server, playwright, fetch, time), while 1.0.77's own help says "Disable all built-in MCP servers (currently: github-mcp-server)". Believe your binary.

Persist it: per-server tools arrays in ~/.copilot/mcp-config.json"Enter * to include all tools, or provide a comma-separated list of tool names (no quotes needed). The default is *."

Gotcha: --enable-all-github-mcp-tools is the flag that produced the 66.5k row in the shared table, and despite the name it isn't routine maintenance. The CLI's default subset exists because GitHub decided the full list was too much for its own client, and this flag is a one-word way to overrule that on every turn of every session. If you need more than the subset, --add-github-mcp-toolset takes one toolset at a time — we didn't measure a single toolset, so weigh it yourself, but structurally it can only be a slice of the 40k rather than all of it.

Seeing it and trimming it in Cursor

Count it: two subcommands do the inventory here. cursor-agent mcp list shows the configured servers with their load status, and cursor-agent mcp list-tools <identifier> is the one that enumerates a specific server's tools and their argument names. Servers are defined either in .cursor/mcp.json for project-specific tools or ~/.cursor/mcp.json for tools available everywhere — and that split is the whole game, because the second file loads in every repo you open.

Turn a server off: in the app the docs describe a per-server toggle that lets you "Toggle servers on/off without removing them", and they say what it buys you: "Disabled servers won't load or appear in chat. This is useful for troubleshooting or reducing tool clutter." From the CLI it's cursor-agent mcp disable <identifier>"Disable an MCP server (it will not be loaded or prompted for approval)" — with enable as its opposite, which adds the server to the local approved list.

Gotcha, and we hit it: the first thing we noticed exploring this ourselves is that you can't see what tools a server has before approving it. We dropped a server into a scratch directory's .cursor/mcp.json and asked:

TEXT
$ cursor-agent mcp list
codescene: not loaded (needs approval)

$ cursor-agent mcp list-tools codescene
Failed to list tools: Failed to load MCP 'codescene': MCP server "codescene" has not been approved

Which is the wrong way round for the decision you're making. How many tools it adds and how big they are is exactly what you want to know before approving it, and the approval is the gate on the answer. The way around it is to talk to the server over stdio directly rather than through the harness.

One more thing that experiment turned up: the same config file inside a git repository gave us No MCP servers configured instead. Cursor resolves .cursor/mcp.json from the workspace root, not from wherever you happen to be standing, so a config sitting in a subdirectory is simply not seen.

Second gotcha, about a number you'll find online: a cap of roughly 40 tools sent to the model gets quoted around the internet as if it were official. It traces back to Cursor's own forum rather than to Cursor. We went through the documentation and there is no maximum in it, no threshold, no truncation warning, no number of any kind. What the docs do say about a crowded list is the per-tool token cost from the shared half, plus the phrase "reducing tool clutter". So treat 40 as a community report rather than a specification — and if a tool of yours is being ignored, go and list what actually loaded instead of assuming you crossed a line.

Seeing it and trimming it in Antigravity CLI

Count it: type /mcp in the session and an MCP Manager overlay appears, described in the docs as letting you "View live status rings for active, disconnected, or loading servers." Note what that sentence promises: connection status per server, not a tool count. There's no documented number here, and there's no agy mcp subcommand either — on 1.1.10 the CLI's own surface has no MCP command at all, so anything beyond the overlay means editing the file.

The file: "The configuration file is located globally at ~/.gemini/config/mcp_config.json (or locally in your workspace under .agents/mcp_config.json)." The local path is the one worth using.

Turn a server off: disabled"Temporarily disable a server without removing its configuration."

Take tools out of context: disabledTools, documented as "Tool names to withhold from the model." That's the availability knob:

JSON
{
  "mcpServers": {
    "internal": {
      "serverUrl": "https://mcp.example.internal/mcp",
      "disabled": false,
      "disabledTools": ["dangerous_bulk_delete", "legacy_export"]
    }
  }
}

Gotcha, twice over. First, permission rules of the form mcp(server/tool) in ~/.gemini/antigravity-cli/settings.json are execution approval, not withholding — nothing in the docs says a denied tool is kept out of the model's view, and only disabledTools is described that way. Second, if you've read that Antigravity supports includeTools and excludeTools, check the source: those are Gemini CLI's keys, documented in Gemini CLI's own settings file. Two Google products sharing one ~/.gemini directory with different vocabularies, and Antigravity's documentation doesn't mention either key. Search your own config for the pair before you conclude a filter is in place.

Seeing it and trimming it in Kimi Code CLI

The first thing is to work out which of the two programs you're running, because everything below depends on it. The rebuilt TypeScript Kimi Code CLI keeps its config in ~/.kimi-code/. The legacy Python Kimi CLI uses ~/.kimi/, and Moonshot's README says that one "will be gradually wound down". Both call themselves Kimi Code in their prose now, so the directory is the reliable tell — that, the repo name, and whether kimi mcp exists as a subcommand at all.

Count it: /mcp"List MCP servers and their connection status in the current session". /mcp-config handles configuration and OAuth login.

Turn a server off: set enabled to false in mcp.json — user-level at ~/.kimi-code/mcp.json, project-level at .kimi-code/mcp.json in the working directory.

Filter individual tools: per-server enabledTools (allowlist) and disabledTools (blocklist):

JSON
{
  "mcpServers": {
    "github": {
      "command": "github-mcp-server",
      "enabled": true,
      "enabledTools": ["search_issues", "get_pull_request"]
    }
  }
}

There's also a global switch in config.toml, which is the blunt instrument for a whole server's worth of tools at once:

TOML
[tools]
disabled = ["mcp__github__*"]

Gotcha: the legacy Python build supports none of that. No enabled, no enabledTools or disabledTools, no [tools] table — its entire [mcp] section is a timeout, and the only way to shed a server's tools is kimi mcp remove NAME. So if you're on the old one and a key you copied from the current docs appears to do nothing, that's the reason, and the fix is the migration rather than the key.

One disclosure: none of this variant was executed. The rebuilt CLI isn't installed on the machine we wrote this on — only the legacy 1.48.0 Python build is, which is what the version stamp on this lesson reflects — so everything above comes from Moonshot's documentation rather than from a run. Four of the other five variants were exercised against the installed binary; Antigravity, like this one, is documentation apart from its version and its missing subcommand. Check that ~/.kimi-code/ exists before trusting any of the paths here.

What changes after you've counted once

It's not about having a perfect config. It's about converting the feeling that the model is worse in this repo into a number you can go and look up, which a fair share of the time is the whole explanation.

And the habit worth keeping is smaller than an audit: at install time, ask yourself whether this server is for the project or for you, and take the subset if you're offered one. Both are free on the day, and both are the ones nobody goes back to decide later.

j / k to move between lessons