Book a call
LESSON14mVERIFIED 2026-08-05 · CLAUDE CODE 2.1.222 · GITHUB COPILOT CLI 1.0.78

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:

BASH
node scripts/mcp-weigh.js npx -y @modelcontextprotocol/server-filesystem /tmp/scratch
TEXT
Secure 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_directories

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

ServerToolsSchema
@modelcontextprotocol/server-filesystem 0.2.01412,958 chars
@playwright/mcp 1.62.0-alpha-17836235050002418,441 chars
@modelcontextprotocol/server-everything 2.0.0137,639 chars
@modelcontextprotocol/server-sequential-thinking 0.2.014,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:

AttachedToolsSchemaResident inputvs. nothing
nothing25,953
filesystem1412,958 chars26,096+143
playwright2418,441 chars26,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 --help to 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.

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

  2. Am I trying to make it know something rather than do something? That's a skill.

  3. Is the answer the same whenever you ask? It's a document. Put it in the repo.

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

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

THE FILEscripts/mcp-weigh.js
JAVASCRIPT
#!/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);
j / k to move between lessons