I had never removed an MCP server until that night.
I was working through some routine maintenance on my Lab sites with Gemini CLI. I asked it to tidy up a directory, and the function call that came back carried arguments I didn't recognize — parameters belonging to a tool on an entirely different server. The API returned a 400, and the only thing left on screen was a complaint about a property that had nothing to do with what I'd asked for.
I opened my settings file and stopped for a moment. Every time I'd found a promising server, I had added it to mcpServers. There was no record of me ever taking one out. I had a habit for adding and no criterion for subtracting.
Here is the part I want to lead with: the number that matters is not the one the platform enforces. Quality slips long before you reach it.
512 Is Not Where Things Start Breaking
The Gemini API caps how many function declarations a single request may carry. Cross it and the request is rejected before the model ever runs.
[API Error: [{
"error": {
"code": 400,
"message": "The GenerateContentRequest proto is invalid:\n * tools[0].function_declarations: [FIELD_INVALID] At most 512 function declarations can be specified.",
"status": "INVALID_ARGUMENT"
}
}]]This is reported as gemini-cli issue #19083, hit by someone running more than twenty MCP servers at once. The failure is immediate rather than partial, which makes it one of the friendlier ways to break.
The trouble starts earlier. In a case involving a single server exposing 188 tools, the developer called a login tool explicitly and still got a 400 — parameters from unrelated tools had leaked into the request. That is nowhere near 512. I suspect it's the same thing I ran into.
To the model, every tool description is a similar-shaped candidate. The more candidates there are, the blurrier the choice of which one to call and what to pass it becomes. The platform sets the hard ceiling; the workable one is mine to set. The official MCP server configuration docs explain connection and filtering carefully, but they never suggest how many tools is too many. That blank is yours to fill in, as I read it.
Count What You Have Connected
Before removing anything, I needed to know the current number. /mcp lists tool names per server, but it won't give you a total. So I wrote a small script that walks the stdio servers in my settings file and counts.
#!/usr/bin/env python3
"""Connect to each stdio MCP server in ~/.gemini/settings.json and count its tools.
Usage: python3 count_mcp_tools.py
Output: server name / tool count / tools whose qualified name is too long / total
"""
import json
import os
import subprocess
from pathlib import Path
SETTINGS_PATH = Path.home() / ".gemini" / "settings.json"
PROTOCOL_VERSION = "2025-06-18" # the server answers with the version it speaks
def rpc(proc, payload):
"""Write one JSON-RPC line, then read until the matching id comes back."""
proc.stdin.write(json.dumps(payload) + "\n")
proc.stdin.flush()
if "id" not in payload:
return None
while True:
line = proc.stdout.readline()
if not line:
raise RuntimeError("the server exited before answering")
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue # some servers print logs to stdout, so skip unparsable lines
if msg.get("id") == payload["id"]:
return msg
def list_tools(conf):
env = dict(os.environ)
env.update({k: os.path.expandvars(v) for k, v in conf.get("env", {}).items()})
proc = subprocess.Popen(
[conf["command"]] + conf.get("args", []),
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
cwd=conf.get("cwd"), env=env, text=True, bufsize=1,
)
try:
rpc(proc, {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "tool-counter", "version": "0.1.0"},
}})
rpc(proc, {"jsonrpc": "2.0", "method": "notifications/initialized"})
res = rpc(proc, {"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
return [t["name"] for t in res.get("result", {}).get("tools", [])]
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
def main():
servers = json.loads(SETTINGS_PATH.read_text()).get("mcpServers", {})
total = 0
for name, conf in sorted(servers.items()):
if "command" not in conf:
print(f"{name:22} skipped, connects over a URL")
continue
try:
tools = list_tools(conf)
except Exception as err:
print(f"{name:22} could not read: {err}")
continue
total += len(tools)
# the name that actually ships is mcp_{server}_{tool}, truncated past 63 chars
long_names = [t for t in tools if len(f"mcp_{name}_{t}") > 63]
note = f" over 63 chars: {len(long_names)}" if long_names else ""
print(f"{name:22} {len(tools):4} tools{note}")
print(f"{'TOTAL':22} {total:4} tools (API ceiling 512)")
if __name__ == "__main__":
main()Send initialize, follow it with the notifications/initialized notification, and only then call tools/list — the stdio handshake expects that order. A couple of my servers write startup logs to stdout, so anything that won't parse as JSON gets skipped quietly. Being strict there turns a counting script into one that dies on an exception.
The 63-character check is in there because tool names don't ship as written. Gemini CLI assigns a fully qualified name of the form mcp_{serverName}_{toolName} to avoid collisions, and anything longer than 63 characters gets its middle replaced. When similar tools sit side by side, truncation makes them harder still to tell apart.
My own total came nowhere close to 512. It was, however, considerably larger than the number I would have guessed.
includeTools Is Where You Write Down What Stays
Gemini CLI gives you two per-server filters. includeTools is an allowlist: only the tools you name stay available. excludeTools is a blocklist: the tools you name disappear. Write neither, and everything the server exposes is enabled.
{
"mcpServers": {
"site-ops": {
"command": "npx",
"args": ["-y", "@example/site-ops-mcp"],
"includeTools": ["read_file", "write_file", "list_dir"],
"timeout": 30000
}
}
}When both are present, excludeTools wins — a tool named in both lists is removed. The same logic holds when you override a server that arrived with an extension: blocklists are unioned, allowlists are intersected. An extension can't quietly restore a tool you left out of your own allowlist. Your local settings hold the veto.
To park a whole server without deleting its configuration, use the top-level mcp block.
{
"mcp": {
"excluded": ["experimental-server"]
}
}One naming trap is worth knowing. Don't put underscores in server names. The parser splits a qualified name on the first underscore after the mcp_ prefix, so a name like my_server makes it misread where the server ends and the tool begins. It doesn't raise an error; your policy rules just stop matching. Write my-server instead.
Sort Them Into Always On, On Demand, and Off
I sorted the tools I'd counted into three piles. The question I asked wasn't whether a tool was useful — it was whether this week would have gone badly without it.
| Pile | Test | How it's configured |
|---|---|---|
| Always on | Called almost daily; work stalls without it | Named in includeTools |
| On demand | A few times a month; I can name the occasion | Disabled by default, re-enabled per session |
| Off | I can't recall why I added it, and I could do it by hand | Moved to mcp.excluded |
That third test sped things up more than I expected. "I can't recall why I added it" described a surprising share of my configuration. Those servers were interesting on the day I tried them, and I never called them again.
The sorting works inside a single server too. Keeping the read-oriented tools in includeTools and dropping the write-oriented ones shrinks the candidate list and shrinks the blast radius if something is called by mistake. As an indie developer, the broader a tool's permissions, the more I want it folded away by default.
Filter too hard, though, and you'll lose time wondering where a tool went. So I keep a list of what I removed next to the settings file. Putting something away is not the same as throwing it out, and the list is how I keep that distinction honest.
Leave Yourself a Way Back
A sorting scheme you can't reverse in the moment turns into "just enable everything again" within a week. Gemini CLI handles servers from the command line.
# see what is currently connected
gemini mcp list
# keep it off by default
gemini mcp disable experimental-server
# bring it back for this session only
gemini mcp enable experimental-server --sessionMid-conversation, /mcp shows status and /mcp enable <name> restores a server on the spot. A disabled server still appears in the listing; it simply doesn't connect. I'd rather have that than delete the entry, because the listing keeps me from forgetting the thing exists.
If a server won't connect at all, the count isn't your problem — look at authentication and paths first. I wrote that up in Gemini CLI Won't Start or Authenticate? Where to Look First. If you're still setting up connections in the first place, Gemini CLI with MCP Servers is the better starting point.
When you write servers yourself, the number of tools you expose becomes a design decision. Splitting unrelated capabilities across small servers and connecting only the side you need has cut down my mis-calls more than any prompt change did. Building Custom MCP Servers for Gemini API covers how to build them.
If You Do One Thing
Run the counting script tonight and write down the total. Not to compare it against 512, but to compare it against the number you thought you had. For me, the size of that gap is what made removing things feel worth the afternoon.
Thank you for reading. Adding tools is the fun part, but subtracting them has been just as useful to me.