MCP Servers: What They Actually Cost and When to Use Them
MCP isn't a magic connection. It's schema injection into your context. Understanding that mechanism changes every installation decision you make.
Prerequisites
- → Claude Code installed and running
- → Basic familiarity with Claude Code configuration files
What you'll set up
- ✓ Know exactly what MCP costs in tokens at startup and per call
- ✓ A decision rule for MCP vs. CLI that holds over time
- ✓ Four MCP servers to install, in priority order
- ✓ `.mcp.json` and user config set with the right scope and transport
- ✓ Three production pitfalls identified before you hit them
You install your first MCP server, point it at a GitHub token, and the next session feels slightly different (Claude can list pull requests without you copying URLs). Then you check your token usage and notice the session cost noticeably more than usual, before you asked a single question.
Most people at this point assume MCP has some overhead, shrug, and keep adding servers. By the fifth server, they’re wondering why long sessions feel sluggish and why Claude occasionally invents tool parameters that don’t exist. The answer to both is the same: MCP is not a background connection. It’s schema injection into your context window, and every server you add pays a tax on every request.
Understanding that mechanism doesn’t just explain the cost. It changes which servers you install and which ones you skip.
What MCP actually does
When Claude Code starts a session, it contacts each configured MCP server and pulls the tool definitions: names, descriptions, parameter schemas. Those definitions get injected into the context (system prompt or early messages) so Claude can reason about them the same way it reasons about native tools like Read, Write, or Bash. The protocol is JSON-RPC 2.0 over either stdio (local process) or HTTP/SSE (remote).
The counter-intuitive part is what this does to prompt caching. Your CLAUDE.md sits in a stable prompt prefix and gets cached at roughly 10% of the input token price from the second request onward (the context engineering guide covers what that file should contain; this guide covers what competes with it). MCP schemas are less stable: servers can connect and disconnect between turns, and schemas that load or change mid-session don’t get the benefit of that cached prefix. On recent Claude Code versions, tools are deferred by default and discovered on demand, which removes most of this cost up front. The arithmetic below is still worth understanding, both because deferred loading isn’t available everywhere (some models and gateways fall back to eager loading) and because it explains why the defaults changed.
Where schemas load eagerly, the distinction matters more than it first appears. A 300-token tool description re-sent uncached across a 50-request session costs 15,000 extra tokens compared to a cached equivalent. Across five servers with 20 tools each, that arithmetic adds up fast. My own gsc-mcp exposes 43 Search Console tools, exactly the kind of surface that makes deferred loading non-negotiable rather than optional.
The token math before and after v2.1.7
Before Claude Code v2.1.7, all MCP tool schemas were injected at session startup, fully. Five servers with 20 tools at roughly 300 tokens per description: around 30,000 tokens riding along in every request. In the worst case, when the prefix cache keeps getting invalidated by servers connecting and disconnecting mid-session, that’s 1.5 million uncached tokens over a 50-request session; in the stable case the schemas are at least cache-read at about a tenth of the input price, cheaper but not free. Neither number appears anywhere in the UI, which is why it surprises people.
Since v2.1.7, Claude Code uses lazy loading. Only tool names are injected at startup. Full schemas load on the first actual call to each tool. In the worst case (every tool used at least once), this cuts the startup injection from roughly 30,000 tokens to a few thousand for the names alone, with each full schema paid only when its tool first fires. Recent versions push the same mechanism further with tool search: entire MCP servers, not just individual tool schemas, can be deferred and discovered on demand, and an alwaysLoad flag opts specific servers back into startup loading when you want them warm from the first message.
What lazy loading doesn’t change: a schema that loads mid-session lands outside the cached prefix at the moment it loads, and once loaded it occupies context for the rest of the session. A GitHub MCP server with 20 tools and detailed parameter descriptions still injects that text the first time its tools fire, and carries it from then on. The cost is far lower than the old load-everything-at-startup behavior, but it’s not zero.

Lazy loading changes when the schema tax is paid. It doesn’t change how many endpoints get a schema at all, and for a large API that’s still the bigger number. Cloudflare’s Code Mode MCP attacks that instead: rather than exposing one schema per endpoint, it exposes exactly two meta-tools, search() and execute(), backed by a typed SDK, and the model writes JavaScript against that SDK inside a sandboxed V8 worker rather than calling endpoints directly. Against Cloudflare’s own API surface, more than 2,500 endpoints, that turns roughly 1.17 million tokens of schema-loading cost into about 1,000, a 99.9% cut, running in production rather than as a benchmark. It’s a structural answer to the same problem lazy loading only defers: the number of tools a model has to reason about on every turn, not just when their schemas load.
MCP vs. CLI: the decision rule
The question worth asking before installing any server is not “can MCP do this?” but “what does the CLI already do here, and how much context does it consume?”
MCP wins in three situations. First, when authentication is complex: OAuth flows, token refresh, service accounts: the kind of auth that requires persistent credential state a shell skill can’t handle cleanly. Second, when you’re on a team and want centralized observability: an HTTP remote MCP server can log every tool call for the whole team, which stdio can’t do. Third, when there genuinely is no CLI equivalent, which is rarer than you’d expect.
CLI wins in most other situations. A frontier model like Opus or Sonnet 4.6 drives aws-cli, glab, or gh well without a schema. It reads help text and infers flags. Thomas Walter reached the same verdict at Dev With AI while building debug-that, a CLI that gives Claude a real debugger: “Les CLI sont composables… c’est moins consommateur de token.” Composable, and cheaper in tokens. For a solo developer working in a budget-conscious context, the difference is concrete: gh pr list --json number,title,state returns exactly what you need in around 200 tokens. The GitHub MCP server injects roughly 20 tool schemas to accomplish the same thing, and those schemas occupy the window for the rest of the session, cache-read at best, full price whenever the prefix cache breaks. You can push that CLI output lower still with rtk, a proxy that compresses command results by 60 to 90 percent before they reach the context.
Over 30 GitHub interactions in a session, the CLI approach saves somewhere between 30,000 and 60,000 tokens: the low end if the MCP schemas stay in the cached prefix the whole session, the high end if the connection drops and reconnects even once, forcing a full-price reload. That’s not a performance edge case. It’s the base cost of convenience.
A starter stack, in priority order
Not every server passes that cost-benefit test. Four do, reliably.
Playwright (Microsoft) belongs at the top because there’s no CLI equivalent for interactive, stateful browser control inside a conversation. Playwright does have a CLI (npx playwright screenshot, the test runner itself), but that’s a fire-and-forget command, not a session Claude can drive turn by turn: navigate, read the console, inspect a request, click something based on what it just saw, all inside the same back-and-forth. The schema cost is justified because that interactive loop is otherwise unavailable.
Semgrep also has a solid CLI (semgrep --config), so the honest case for the MCP server isn’t capability, it’s workflow. An MCP-connected Claude can run a scan on its own initiative mid-session, during a code review or before a commit, without you remembering to invoke it, and the findings come back as structured data Claude consumes directly rather than output it has to parse. If security review is part of your regular workflow and you want it to happen automatically rather than by discipline, this earns its overhead. If you’re fine running semgrep --config yourself when it matters, the CLI is the leaner choice.
Context7 pulls up-to-date official documentation for frameworks and libraries, tied to the specific version you’re using. No CLI does this. The alternative is web search, which is slower and frequently surfaces outdated Stack Overflow answers. For any project with a fast-moving dependency stack, Context7 eliminates a whole category of “wait, is this the current API?” interruptions.
Linear makes sense on teams, less so solo. The value isn’t ticket access (you can query the Linear API directly). The value is that an MCP-connected Claude can cross-reference a ticket while reading code, without you copying context manually. For team leads who want Claude aware of sprint state during code review, this pays off. For a solo developer with no team observability needs, it’s unnecessary overhead.
What doesn’t justify the cost: most simple REST wrappers, Slack (the direct API is sufficient for solo use), and any server you installed to try once and never removed.
Configuration: scope and transport
Two decisions to make for each server before writing any config.
The first is scope. A .mcp.json file in the project root is versioned, committed, and shared with the team. User-scope servers live in ~/.claude.json (written for you by claude mcp add <name> --scope user) and apply across all your projects. The rule is simple: if the team needs the server, it goes in the project. If it’s a personal workflow tool, it stays global. Mixing these up means either exposing personal tokens in version control or failing to give teammates the servers they need.
The second is transport. Stdio runs a local process; data never leaves the machine, there’s no network call, and startup is fast. HTTP/SSE remote servers are centralized: multiple people share the same server instance, which enables logging, rate limiting, and team-level observability. Solo developer: stdio unless there’s a specific reason not to. Team with shared tooling or monitoring requirements: HTTP remote.
A minimal project .mcp.json for Playwright and Context7:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
},
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@latest"]
}
}
}
Stdio is implied whenever command is present; a remote server declares "type": "http" (or "sse") with a url instead. A user-scope entry in ~/.claude.json for a personal Linear integration looks the same:
{
"mcpServers": {
"linear": {
"command": "npx",
"args": ["-y", "linear-mcp-server"],
"env": {
"LINEAR_API_KEY": "${LINEAR_API_KEY}"
}
}
}
}
To turn off a project server without deleting its shared config, list it in .claude/settings.local.json, which stays out of version control:
{
"disabledMcpjsonServers": ["github"]
}
Note the LINEAR_API_KEY reference: use environment variable substitution rather than hardcoding secrets. .mcp.json is committed by design and ~/.claude.json gets backed up, and a token in plaintext in either is a token at risk.
Three production pitfalls
Silent crash. An MCP server process crashes after startup, but Claude keeps going. From Claude’s perspective, the tools exist (their names were injected). When it tries to call one, nothing comes back. Claude will sometimes invent a plausible response rather than surface a clear error. The symptom looks like tool calls that complete without doing anything. Diagnosis: type /mcp in the chat to see the current connection status of each server. If a server shows as disconnected, restart it explicitly before trusting any output from tools in that namespace.
Tool hallucinations. With lazy loading, Claude knows the tool name at startup but not the full parameter schema until the first call. If Claude generates a call before the schema has loaded, it sometimes invents parameters based on what seems plausible from the name and description alone. The failure mode is a type error or “unknown parameter” response from the server. The fix is to run one real call to each tool early in the session to hydrate the schemas, rather than assuming they’re available from session start. The same rule shows up from the reliability angle, not just the token-cost one. An IFTTD podcast episode, a Devoxx talk, and a Dev With AI talk each reach it on their own: limit the number of exposed tools, because every extra one raises the hallucination risk, and prefer a targeted server exposing only the functions you actually allow over a generic full-access one.
Invisible budget drain. Five servers running simultaneously with verbose descriptions accumulate cost even under lazy loading, because any session that exercises those tools pays the description cost on each call. The disabledMcpjsonServers list in .claude/settings.local.json lets you turn off project servers without removing their shared config. Disable anything you’re not actively using in the current session. If you’re working on a frontend task, the Linear and Semgrep servers have no reason to be contributing to your context.
Security: five minutes before installing
Any MCP server executes on your machine with your credentials in scope. The schema injection is the visible cost. The hidden risk is what the server process itself can do.
The minimum check before installing anything: read the launch script and verify what network access and filesystem permissions it requests. Pin a specific version instead of using @latest so you’re not silently picking up a future change in behavior. Never put secrets directly in MCP config files: use environment variable references and set the actual values in your shell profile or a local .env that stays out of version control.
Never assume a permission restriction constrains intent, only the literal action. A French podcast on production AI security documented the failure mode directly: block an agent from deleting a file through its MCP tool, and it may simply empty the file instead, because from its perspective that satisfies the same goal through an unblocked path. Any write-capable MCP server needs client-side guardrails that anticipate the goal, not just the specific verb.
For a more thorough vetting process before adding a new server to a team environment, the attack surface guide covers the full checklist: package provenance, permission auditing, and sandboxing options for servers that need elevated access.
What to take from this
The practical upshot fits in three sentences. Before adding a server, ask whether the CLI can handle it with a frontier model driving it; if yes, skip the server. If you do add one, use stdio for personal workflows and HTTP remote only when team observability is the actual goal. Disable servers you’re not using in the current session, and check /mcp status the first time something behaves oddly.
The developers who get the most out of MCP are not the ones with the most servers configured. They’re the ones who drew the line carefully at what genuinely needs a schema.