When not to reach for an MCP
Four things people install an MCP server for that don't need one, what an idle server costs once schemas are deferred, and the five questions that settle it.
The objection, taken seriously
The criticism of MCP is partly fair, we agree. It's an API wrapper with extra steps, it does eat the context window, and plenty of the servers people install do something the agent could already manage with a shell command. It's just that one part of that has changed without most of us noticing, and it changed to a different extent across different tools.
The conclusion people draw from it is still wrong, though, because there is a shape of problem for which a server is the only clean answer and nothing else comes close. So this isn't about debunking MCP. It's about the four things people reach for a server to do that don't need one, what the reach actually costs now that some harnesses defer the schemas, and what the cheaper answer looks like. The protocol itself, and what's in the spec, has its own lesson at the start of this chapter.
All figures below were gathered on 5 August 2026, against the versions stamped on this lesson.
A server is text before it is anything else
Ultimately, for the model to know a tool exists, what it's for and what arguments it takes, there has to be a piece of text saying so — and that text comes from the server, and something has to carry it.
Any server you can start locally will tell you what it publishes before you install it, and without
spending a model call. It's a short stdio handshake — initialize, then tools/list — and the
artifact at the bottom of this lesson does exactly that:
node scripts/mcp-weigh.js npx -y @modelcontextprotocol/server-filesystem /tmp/scratchSecure MCP Filesystem Server running on stdio
secure-filesystem-server 0.2.0
protocol 2025-06-18
Client does not support MCP Roots, using allowed directories set from server args: [ ... ]
14 tools, 12,958 chars of schema
1262 read_media_file
1139 read_text_file
1071 edit_file
1021 search_files
988 read_multiple_files
...
770 read_file
713 list_allowed_directoriesTwo of those lines are the server talking rather than us — it writes to stderr, so where they land moves around between runs.
Four public servers, measured the day this was written:
| Server | Tools | Schema |
|---|---|---|
@modelcontextprotocol/server-filesystem 0.2.0 | 14 | 12,958 chars |
@playwright/mcp 1.62.0-alpha-1783623505000 | 24 | 18,441 chars |
@modelcontextprotocol/server-everything 2.0.0 | 13 | 7,639 chars |
@modelcontextprotocol/server-sequential-thinking 0.2.0 | 1 | 4,585 chars |
Worth noticing that this is a sum of the individual tool schemas, which comes out slightly under the
length of the whole tools/list reply — which matters if you're comparing it against a number
somebody tallied the other way.
The first row is the one to sit with. Thirteen thousand characters and fourteen tools — read_file,
write_file, edit_file, list_directory, search_files, move_file, directory_tree and seven
more — to give a coding agent access to the filesystem. Claude Code's extension guide opens by
saying that its built-in tools, which it lists as covering "file operations, search, execution, and
web access", already "cover most coding tasks".
Then look at the opposite extreme: 4,585 characters for one tool, and 2,781 of those are the description alone, against 1,084 for the input schema it's supposedly describing. Three fifths of what that server asks your model to carry isn't a parameter contract. There's a cheaper place to keep that much prose.
What an idle server actually costs
So what's the price of a server nobody touches? Anthropic itemised the bill in November 2025 for a five-server setup:
GitHub — 35 tools, ~26K tokens
Slack — 11 tools, ~21K
Sentry — 5 tools, ~3K
Grafana — 5 tools, ~3K
Splunk — 2 tools, ~2K
"That's 58 tools consuming approximately 55K tokens before the conversation even starts." Keep adding servers — they price Jira at around 17K on its own — and you're "quickly approaching 100K+ token overhead". About their own setups: "we've seen tool definitions consume 134K tokens before optimization."
That's the number the sceptics quote, and it was real. It's now conditional, and the condition is which harness you're on and which month it is.
So we ran the same server through two of them. Empty directory, prompt Reply with exactly: ok — nothing to read, nothing to search, no tool required.
Claude Code 2.1.222, on Haiku. This is resident input — the entirety of what the turn pays for, cached or not:
| Attached | Tools | Schema | Resident input | vs. nothing |
|---|---|---|---|---|
| nothing | — | — | 25,953 | — |
| filesystem | 14 | 12,958 chars | 26,096 | +143 |
| playwright | 24 | 18,441 chars | 26,198 | +245 |
The baseline and the filesystem run were each done twice and came back identical, so this isn't cache noise. That's 143 across 14 tools, 245 across 24, and 102 for the ten-tool gap between them: three independent ways of dividing it, all landing on about ten tokens per tool name. If the schemas themselves were resident you'd be looking at thousands. They aren't in the window.
And that's documented behaviour rather than luck. Claude Code's 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," and it's on by default. The server is live rather than missing, too: a second
run, with that one tool allowlisted, called list_allowed_directories and got an answer back.
GitHub Copilot CLI 1.0.78. Same server, same prompt, same empty directory, same afternoon. Copilot reports its own total and rounds it, so these are its figures rather than ours: 13.9k tokens up without the server, 15.0k with — about 1,100 tokens a turn for something nobody touched. Playwright instead of it: 16.1k, so about 2,200.
Which puts the same idle server at seven to nine times more per turn on one of these than on the other, depending which of the two you attach. Whether that gap is schemas or something else we didn't measure, and won't guess.
Two things follow. Deferral is real, so "MCP eats your context" has stopped being a general law — but deferred isn't free either, and ten tokens a tool name is per turn and doesn't go away.
And neither figure is a constant. Two harnesses measured on the same afternoon came out most of an order of magnitude apart, and the MCP page that documents any of this fences its behaviour off patch by patch — this feature from v2.1.187, that one from v2.1.212. Nobody sends you a note when your number moves.
What deferral buys you is not paying for a browser automation server across the four hundred turns where you were editing YAML. It doesn't make an unnecessary server free. It makes it quiet, which is worse for the habit and better for the bill. What a crowded list costs, as against one idle server, is the next lesson in this chapter.
Four things that don't need a server
It already has a command line
If the thing you want to reach has a CLI, so does your agent. gh, psql, aws, kubectl,
docker, stripe, terraform — every one of them a documented command-line interface the shell
tool can already drive, with no schema for anybody to carry.
That last part is the one people skip past. A CLI's documentation is retrievable on demand: the
agent runs gh pr --help in the one session out of forty where it needs to, reads fifty lines, and
the other thirty-nine pay nothing. Same economics as deferral, except nobody had to wait for a
vendor to build it.
There's a second reason, and it survives even where deferral is on: the shell composes.
gh pr list --json number,title | jq '...' is one call. The tool-shaped equivalent is a call, a
result landing in the window, a decision, another call. Every tool boundary in the middle of what
wanted to be a pipeline is somewhere for intermediate results to pile up on the way past.
It's knowledge, not an action
If what you want is for the agent to know something — how your deploy works, which of four database URLs is the real one, the house rules for a migration — then nothing is being executed. Some text needs to be in front of the model at the right moment.
Anthropic's own one-liner is the cleanest version of the split: "If you're explaining how to do something, that's a skill. If you need Claude to access something, that's MCP."
It's worth knowing why that's the whole test now, because the other half of the argument has expired. Anthropic's own comparison table, from December 2025, contrasts a skill that "loads on-demand, preserving context" with a server whose definitions are "loaded upfront" — and the +143 above is what happened to the second half of that sentence. Both mechanisms now keep something small and permanent in front of the model and defer the bulk: a skill's description against a tool's name, a skill's body against a tool's schema. Pick a skill because you're explaining rather than connecting, not because somebody told you it was cheaper. The full comparison has a lesson of its own in the previous chapter.
The sequential-thinking server is the clean example of getting this backwards: of its 4,585 characters, 2,781 are one tool's description — shipped as a tool because a tool was the mechanism within reach.
It's a document
An OpenAPI spec, a database schema, a service-to-owner list, your error codes — these change on a
release cadence rather than per request, and a server that answers get_schema() with the same
string every time is a file with a daemon in front of it.
Put it in the repo and point the memory file at the path. The agent reads it when it needs it, it's versioned next to the code that depends on it, and it survives your laptop.
The test is whether the answer depends on when you ask. If it doesn't, it's a document.
You needed it once
This is the one that actually accumulates, and it's the most common of the four. Something needs doing on a Tuesday, a server exists that does it, into the config it goes, job done. It's November. The server is still there, still starting with every session, still charging for its tool names on every turn.
Nobody removes these, because nothing hurt on the day. The cost lands on every turn of every future session, thin enough that it never becomes anyone's problem, and it surfaces as "the agent has got worse lately."
What a server is genuinely for
The good cases have a shape, and it isn't "we integrate with X." Claude Code's docs give the trigger in one line, and it's a better test than anything about architecture: connect a server "when you find yourself copying data into chat from another tool, like an issue tracker or a monitoring dashboard." If you're the transport layer, that's the job a server takes off you.
Live state behind something with no command line. An internal service whose only interface is HTTP and an auth flow. Ticket state, deploy state, what's in the queue right now — where the answer differs at 10am and 11am and there's no
--helpto read.A credential you'd rather not hand to the shell. A server that authenticates over OAuth holds its own token and the agent holds a tool call, so what lands in your window is
search_tickets(query)rather than a key on a curl line. Worth checking which kind you have, though: a server configured with a header still keeps that credential in your config.One integration, several harnesses. Four people on three different tools, and the server gets written once against the protocol while each harness points at it from its own config. This scales with team size and is close to worthless for one person on one tool.
Something your harness doesn't already have. Browser automation is the honest example — and check before you reach, because some harnesses now ship a browser server of their own. Playwright's 24 tools are a lot of window, but there's no shell one-liner for "click the button that appeared after the modal closed and tell me what the page says now." Where the capability doesn't otherwise exist, the schema is simply the price.
What deferral doesn't touch
A stdio server is somebody else's code, running on your machine, inside your session. It sees
the arguments the model passes and it decides what comes back. Claude Code's MCP page puts it
plainly: "Verify you trust each server before connecting it. Servers that fetch external content can
expose you to prompt injection risk." A server can also put text into the system prompt — Copilot
CLI carries a flag to "Include initialization instructions from all MCP servers in the system prompt
instead of only allowlisted servers", which tells you both that servers can do this and that
somebody decided an allowlist was warranted. Playwright's tool list is worth reading with that in
mind: one of the 24 is called browser_run_code_unsafe. The name is honest. Most authors won't
label it for you.
A crowded tool list is a worse tool list. Names colliding across servers, three plausible ways
to do one thing, a model choosing between them off a one-line description. Anthropic's
context-engineering guidance states the failure mode as a rule you can apply without measuring
anything: "If a human engineer can't definitively say which tool should be used in a given
situation, an AI agent can't be expected to do better." Vendors act like they believe it — GitHub
ships its own MCP server to its own CLI with a subset of the tools switched on, and copilot --help
documents --add-github-mcp-toolset as the way to enable more "instead of the default CLI subset".
And there's an operational tail no amount of deferral removes. Read Claude Code's MCP page as an ops document rather than a tutorial and you get:
OAuth flows
Reconnection with exponential backoff
Idle timeouts
A startup timeout
Main-conversation calls moving to a background task once they run past two minutes
An approval prompt for project-scoped servers
Tool descriptions truncated at 2KB
A default 25,000-token ceiling on what one tool call may return
None of that argues against servers. All of it is surface area you didn't have last week.
The test
Five questions in order. The first yes ends it.
Does the thing already have a CLI the agent can run? Use it. Write the two commands that matter into a skill or the memory file so it stops rediscovering them.
Am I trying to make it know something rather than do something? That's a skill.
Is the answer the same whenever you ask? It's a document. Put it in the repo.
Will I still be using this in a month? If not, run it for the afternoon and take it out again. A server installed for one task is a config entry you won't remember writing.
Does more than one person or more than one harness need this — or does it hold a credential I'd rather keep out of the shell? Now it's a server, and a good one.
The habit that keeps it honest
Once a quarter, open the config and read the server list out loud. For each one, say when it was last used. Anything you can't answer for comes out — and you can always put it back, because putting it back is one line.
Measure it while you're there. The artifact below costs nothing to run, and the per-turn figure — the one that actually bills you — is two headless runs, one with the server and one without. Our two harnesses disagreed by most of an order of magnitude on the same afternoon, and the vendor's own docs gate this behaviour by patch version, so the only figure worth trusting is the one you took yourself, on the version you're actually running.
The reason to do it on a schedule rather than when something goes wrong is that nothing will go wrong. There's no error and no failed run. There's a config file that grew, and sessions that start slightly heavier and choose slightly worse than they used to, and no single day on which either becomes visible.
scripts/mcp-weigh.js#!/usr/bin/env node
// mcp-weigh — ask an MCP server what it publishes, before you install it.
//
// node scripts/mcp-weigh.js npx -y @modelcontextprotocol/server-filesystem /tmp
// node scripts/mcp-weigh.js npx -y @playwright/mcp@latest
//
// Speaks stdio JSON-RPC directly: initialize -> tools/list. No SDK, no model
// call, nothing billed. Prints the tool count and the serialized size of the
// schemas the server wants your model to carry.
//
// Characters, not tokens — a token count depends on the tokenizer. Divide by
// three or four for a rough order of magnitude, and treat the ranking as the
// point rather than the absolute.
const { spawn } = require("node:child_process");
const [cmd, ...args] = process.argv.slice(2);
if (!cmd) {
console.error("usage: node scripts/mcp-weigh.js <command> [args...]");
process.exit(2);
}
const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
const send = (msg) => child.stdin.write(JSON.stringify(msg) + "\n");
send({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: { name: "mcp-weigh", version: "1" },
},
});
let buffer = "";
let serverLine = "";
child.stdout.on("data", (chunk) => {
buffer += chunk;
let cut;
while ((cut = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, cut).trim();
buffer = buffer.slice(cut + 1);
if (!line) continue;
let msg;
try {
msg = JSON.parse(line);
} catch {
continue; // not every line on stdout has to be JSON; skip what won't parse
}
if (msg.id === 1 && msg.result) {
const { serverInfo, protocolVersion } = msg.result;
serverLine = `${serverInfo?.name ?? "?"} ${serverInfo?.version ?? "?"}`;
console.log(serverLine);
console.log(`protocol ${protocolVersion}`);
send({ jsonrpc: "2.0", method: "notifications/initialized" });
send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
}
if (msg.id === 2 && msg.result) {
report(msg.result.tools ?? []);
child.kill();
process.exit(0);
}
}
});
function report(tools) {
const rows = tools
.map((tool) => [JSON.stringify(tool).length, tool.name])
.sort((a, b) => b[0] - a[0]);
const total = rows.reduce((sum, [chars]) => sum + chars, 0);
console.log(
`${rows.length} tools, ${total.toLocaleString("en-US")} chars of schema`,
);
console.log();
for (const [chars, name] of rows) {
console.log(` ${String(chars).padStart(6)} ${name}`);
}
}
setTimeout(() => {
console.error("timed out waiting for the server");
child.kill();
process.exit(1);
}, 60_000);