TLDR; MCP connects Claude Code to APIs, databases, and services a CLI can’t reach, but it costs more tokens than a shell command for the same job: 4 to 32 times more in the one published benchmark, covered below. Reach for MCP only when no CLI exists, auth is complex, or the response needs structure. Build a custom server when nothing off the shelf fits.
I added fifteen MCP servers in my first week with Claude Code. By the end of the month, I was running four. Not because the others didn’t work, most did, but because a shell command was cheaper, faster, and more reliable for the majority of what I’d connected.
MCP is the most discussed feature in the Claude Code ecosystem right now. Every week there’s a new server for some API, a new marketplace listing, a new “connect everything” thread on social media. The enthusiasm is warranted, and MCP extends what Claude can do. But it’s also created a pattern I see constantly: people reaching for MCP servers when a CLI tool would serve them better, burning context tokens on tool schemas they don’t need, and wondering why their sessions feel sluggish.
This guide covers the practical reality of MCP: what the protocol actually does, when it earns its keep versus when a curl pipe is the better choice, and how to build your own server when nothing off the shelf fits your workflow. The skills, hooks, agents, and commands guide covers the broader extensibility model. This one goes deep on the protocol layer.
What MCP actually is
Model Context Protocol is a standard for connecting Claude Code to external tools and data sources. It’s how Claude talks to databases, APIs, browsers, and custom services that live outside your terminal.
The architecture is simple. An MCP server is a process that declares a set of tools, each with a name, a description, and a JSON schema for its parameters. When Claude Code starts, it discovers available servers from your configuration, learns what tools each server offers, and can then call those tools during a conversation.
The communication uses JSON-RPC over one of two transports:
stdio: the server runs as a local child process. Claude Code starts it, sends JSON-RPC messages to stdin, reads responses from stdout. This is what you’ll use for most custom servers. Fast, no network involved, easy to debug.
Streamable HTTP: the server runs remotely and exposes HTTP endpoints. Used for shared team servers or cloud-hosted services. More infrastructure, but supports multi-user access and centralised deployment.
MCP defines three capability types, tools, resources, and prompts, but tools are what matter in practice. A tool is a function Claude can call: “search this database,” “fetch this API,” “run this query.” Resources and prompts exist in the spec but most servers focus on tools, and Claude Code’s integration is strongest at the tool level.
MCP doesn’t give Claude any abilities it couldn’t theoretically achieve through Bash commands. It provides structure: typed parameters, validated schemas, predictable response formats. Claude doesn’t have to guess at curl flags or parse unstructured output. The server handles the messy integration, and Claude gets clean data back.
When to use MCP vs CLI tools
This is the decision most people get wrong, and the cost difference is significant.
One Scalekit benchmark from March 2026 ran 75 trials across pure-CLI, CLI-plus-skills, and MCP arms. In that setup, CLI workflows consumed roughly 1,300–9,400 tokens per task with 100% completion, while the MCP arm consumed 32,000–82,000 tokens per task and completed 72% of runs. The reported multiplier ranged from 4x to 32x, with every MCP failure a TCP-level connection timeout. That is evidence about the tested tools and configuration, not a universal MCP tax. An Apideck vendor example likewise reports 55,000+ tokens for about 40 eagerly exposed tool schemas versus an approximately 80-token CLI prompt. Current clients can defer schemas through tool search, so measure the servers and client version you actually use.
The token overhead isn’t surprising when you understand what’s happening. Each MCP server registers its tools with full JSON schemas: parameter names, types, descriptions, nested objects. Even with Claude Code’s tool search (schemas load on demand rather than at startup), invoking MCP tools pulls those schemas into your context window. In Apideck’s 40-tool scenario, the definitions alone consumed over a quarter of a 200K context window before any work happened.
CLI tools carry almost none of this overhead. Claude already knows how to read --help output, compose Unix pipes, and parse structured formats like JSON. It was trained on millions of examples of command-line usage.
My decision rule is short. Use CLI when the tool exists and follows standard conventions: gh for GitHub, aws for AWS, gcloud for GCP, jq for JSON processing, curl for API calls. If it has a man page, Claude can use it.
Use MCP when one of these is true:
- No CLI exists and the integration is too small to justify writing one. Many SaaS APIs and internal company tools ship without a command-line interface.
- Structured authentication is required and you can’t move it out of the loop. OAuth flows, token refresh, multi-tenant auth: these are painful to manage through shell commands, though a purpose-built CLI can own the auth lifecycle too.
- The integration benefits from a long-lived, typed interaction model. A CLI can carry JSON, file paths, and even encoded binary data, but browser automation through Playwright also needs a persistent browser process and state across calls. MCP can expose that state as a coherent tool surface.
Before adding any MCP server, I ask: can I do this with a shell command and jq? If yes, I skip MCP. The GitHub MCP server gives Claude access to issues, PRs, and repos, but gh issue list --json title,state,assignees | jq does the same thing at a fraction of the token cost.
That question turned into a building habit. When an API mattered enough to my work, I stopped settling for the MCP server and wrote a small CLI instead: I now run seven of them in production, covering Google Search Console, DataForSEO, GA4, Google Maps data, CRM reads, PageSpeed, and Australia’s VET training register. Each stores auth in the OS keyring, prints tables on a TTY, and emits newline-delimited JSON when piped, so Claude drives them like any other shell command. The same logic led me to route common dev commands through a token-filtering proxy that trims git, test, and build output before it reaches the context window. None of that existed when I first filled up .mcp.json; the benchmark numbers above explain why it does now.
The MCP setup I use
My .mcp.json has been through three phases: fifteen servers, then four, and the four kept shrinking as the CLI work above matured. Two of the four were DataForSEO and Google Search Console. At the time, the reasoning felt solid: DataForSEO is a complex API with dozens of endpoints and no official CLI; GSC needs OAuth2 service-account auth that is fiddly from curl. Both true. But complex API and fiddly auth are arguments for building a tool, not for that tool being an MCP server. The dfs and gsc CLIs replaced both servers, and the pattern generalised into the seven CLIs I described earlier.
What remains as MCP is what a CLI can’t do:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp"]
},
"codex": {
"command": "codex",
"args": ["mcp-server"]
}
}
}
Playwright (@playwright/mcp, Microsoft’s official server): browser automation. Screenshots, DOM snapshots, form filling, click sequences. This requires a long-lived browser process holding state between calls; no one-shot shell command can do it.
Codex: a second model available as a tool, via the Codex CLI’s MCP server mode. I hand it diffs for cross-model code review; a different model catches different mistakes. The Codex server process holds the conversation; MCP gives it a clean tool surface.
If I hadn’t built the dfs CLI, the slot would be filled by DataForSEO’s own dataforseo-mcp-server, which since August 2026 also runs as a CLI. Gmail is a similar story: Google’s Gmail MCP server is a remote endpoint in the Workspace Developer Preview, not an npm package, and outside that program Claude reaches Gmail through the claude.ai connector.
The ones I removed outright: GitHub (replaced by gh CLI, massive token saving), Slack (replaced by curl to the webhook API), and a Notion MCP server that consumed 40,000+ tokens in schemas for features I used twice. Each removal freed context and improved session reliability.
Claude Code scopes MCP servers three ways via claude mcp add --scope. local is private to you in one project, project writes to the committed .mcp.json your team shares, and user applies across all your projects. I test new servers at local scope and promote them to project only once they’ve earned it.
Keeping MCP token costs down
Two things make a significant difference:
Tool search (built in): Claude Code loads MCP tool schemas on demand rather than dumping them all at session start. Anthropic calls this tool search; it’s enabled by default in current versions and controlled by the ENABLE_TOOL_SEARCH environment variable. Run /context to see which tools show as deferred versus loaded.
Context-mode (a separate plugin): an open-source community plugin (github.com/mksglu/context-mode) that indexes large MCP responses into a local search store instead of dumping raw data into your conversation. The project’s own benchmarks claim a 50–90% reduction in MCP token usage; treat that as a vendor figure rather than an independent measurement, but the mechanism is sound for servers that return large datasets.
You’ll see “3 to 5 servers maximum” repeated as received wisdom around the ecosystem. There’s no documented Anthropic recommendation behind it, and with tool search deferring schemas, Claude Code’s own docs are explicit that there’s no fixed per-server tool cap; the practical limit is your context budget at the moment tools actually get used. My own count settled at a handful, not because of a rule, but because each server I audited either earned its schemas or lost its slot to a shell command.
Building a custom MCP server from scratch
Off-the-shelf servers cover the common services. But every organisation has internal tools, proprietary APIs, and custom workflows that nobody’s published a server for. That’s when you build your own.
The threshold question: is the rework cost higher than the build cost? One team I studied shipped a custom MCP server for code-pattern enforcement and reported convention-violation review comments dropping to near zero after switching from CLAUDE.md instructions to programmatic matching against the codebase. The build pays for itself quickly when review-cycle time is the bottleneck.
But don’t build speculatively. If the problem can be solved with a CLAUDE.md instruction or a skill, do that first. MCP servers earn their keep when the integration requires code analysis, structured data access, or state management that text instructions can’t encode.
The walkthrough
A complete MCP server that wraps an internal REST API, in this case a deployment status service. The pattern adapts to any API. The tutorial targets SDK v1, which receives fixes for at least six months after the July 2026 v2 release.
Project setup:
mkdir deploy-status-mcp && cd deploy-status-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
The server (index.js):
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const API_BASE = process.env.DEPLOY_API_URL;
const API_TOKEN = process.env.DEPLOY_API_TOKEN;
const server = new McpServer({
name: "deploy-status",
version: "1.0.0",
});
server.registerTool(
"get_deployment_status",
{
description: "Get the current deployment status for a service",
inputSchema: {
service: z
.enum(["api", "web", "worker"])
.describe("Service name"),
},
},
async ({ service }) => {
const res = await fetch(`${API_BASE}/deployments/${encodeURIComponent(service)}/latest`, {
headers: { Authorization: `Bearer ${API_TOKEN}` },
});
if (!res.ok) {
return {
content: [{ type: "text", text: `Error: ${res.status} ${res.statusText}` }],
isError: true,
};
}
const data = await res.json();
return {
content: [{
type: "text",
text: JSON.stringify({
service: data.service,
version: data.version,
status: data.status,
deployed_at: data.deployed_at,
deployed_by: data.deployed_by,
}, null, 2),
}],
};
}
);
server.registerTool(
"list_recent_deployments",
{
description: "List recent deployments across all services",
inputSchema: {
limit: z.number().optional().default(10).describe("Number of deployments to return"),
},
},
async ({ limit }) => {
const res = await fetch(`${API_BASE}/deployments?limit=${limit}`, {
headers: { Authorization: `Bearer ${API_TOKEN}` },
});
if (!res.ok) {
return {
content: [{ type: "text", text: `Error: ${res.status} ${res.statusText}` }],
isError: true,
};
}
const data = await res.json();
return {
content: [{ type: "text", text: JSON.stringify(data.deployments, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Add "type": "module" to package.json for ES module support.
Configure in .mcp.json:
{
"mcpServers": {
"deploy-status": {
"command": "node",
"args": ["path/to/deploy-status-mcp/index.js"],
"env": {
"DEPLOY_API_URL": "https://internal-api.company.com",
"DEPLOY_API_TOKEN": "${DEPLOY_API_TOKEN}"
}
}
}
}
Two tools, clean error handling, structured responses. Claude can now ask “what’s the deployment status of the API service?” and get real data back.
Add more server.registerTool() calls for additional endpoints. (You’ll still see server.tool(name, description, schema, handler) in older tutorials; it’s deprecated in the current @modelcontextprotocol/sdk line, and the migration is a restructure, not a rename, because the description and schema move into a config object.) That package (v1, what’s used above) is now the legacy line: a v2 SDK ships as separate @modelcontextprotocol/server and @modelcontextprotocol/client packages with its own tutorials, so check which line a given example targets before copying its imports. Keep each tool focused on a single operation; Claude works better with many small tools than a few large ones with complex parameter schemas.
Debugging MCP servers
An MCP server that crashes on startup may appear only as missing or disconnected tooling in the conversation. Claude Code’s /mcp diagnostics and the server’s stderr expose the failure, but you still need to check them deliberately.
Log to stderr, not stdout. Stdout is the JSON-RPC transport. Anything you write there corrupts the protocol. Use console.error() for debug logging:
console.error(`[deploy-status] Fetching ${service} from ${API_BASE}`);
These messages stay out of the protocol stream and out of the conversation.
Use the MCP inspector. The official debugging tool lets you interact with your server directly:
npx @modelcontextprotocol/inspector node index.js
This opens a web interface where you can see your server’s tools, call them with test parameters, and inspect the responses. Fix problems here before connecting to Claude Code.
Common failure patterns:
| Symptom | Cause | Fix |
|---|---|---|
| Tools don’t appear | Server crashes on startup | Run manually with node index.js and check stderr |
| “Connection refused” | Server exited before Claude connected | Check environment variables are set in .mcp.json |
| Schema mismatch | Zod schema doesn’t match what Claude sends | Verify parameter types; use .optional() for non-required fields |
| Timeout | API response too slow | Add a timeout to your fetch calls; return partial data if possible |
| Auth errors | Token expired or missing | Check env var names match between .mcp.json and server code |
Run /mcp in Claude Code to see which servers are connected and which tools are available. If your server isn’t listed, it failed to start; run it manually to see the error.
Security considerations
Local stdio MCP servers run as processes under your user account unless you add an operating-system or container boundary. Remote servers instead operate under their own hosting environment and authentication scopes. In both cases, the permissions granted to the server define the attack surface.
Prompt injection through tool responses. When an MCP server returns data from an external source, a database, an API, user-generated content, that data enters Claude’s context as untrusted tool output. A malicious payload in a database field or API response could try to instruct Claude to take actions you didn’t intend. This is a documented attack class for systems that connect models to external data.
The mitigation: treat MCP responses the same way you’d treat user input in a web application. Validate types and expected fields, separate retrieved content from tool instructions, minimise the actions available to the model, and require confirmation for consequential operations. Do not rely on stripping text that merely looks like an instruction; an attacker can rephrase it. Be especially careful with servers that access user-generated content such as CMS data, support tickets, and email bodies.
Credential handling. Reference API keys from the process environment rather than storing their values in .mcp.json or server code. Never log credentials to stderr. Don’t include tokens in tool descriptions or parameter schemas; these are visible in Claude’s context.
Scope permissions narrowly. If your server only needs to read deployment status, don’t give it a token that can trigger deployments. Create service accounts with the minimum permissions required. This limits the blast radius if something goes wrong.
Audit your MCP connections. /mcp and claude mcp list show configured connections, but a local list is not a complete organisational audit trail. For team environments, maintain an explicit allowlist of approved MCP servers, centralise configuration where the product supports it, and review access periodically. For personal use, be deliberate about what you connect and regularly review your .mcp.json.
Every MCP server is an extension of Claude’s capabilities with your credentials. Add them the way you’d add OAuth scopes to a third-party application: only what’s needed, reviewed regularly, revoked when no longer used.
Start with CLI for everything. Notice the friction points where shell commands fall short. Build or install MCP servers specifically for those gaps. A couple of well-chosen servers beat the fifteen I started with, which bloated my context window and slowed every session. And when nothing off the shelf fits, the protocol is simple enough that a custom server takes an afternoon to build.
When an MCP server has to work for a whole team rather than one developer’s machine, it becomes Applied AI work, with the same review and access controls as any other system.
Related reading
- Skills, hooks, agents, and commands, the definitive guide, the four extensibility primitives and how they compose with MCP
- Token economics, what Claude Code actually costs, MCP’s context cost in the broader token picture
- The memory hierarchy, CLAUDE.md, memory vault, and persistent context, choosing the right persistence layer
- Session craft, when to start fresh, resume, or compact, managing context when MCP servers consume headroom
Sources
- Scalekit MCP vs CLI Benchmarks, 75 benchmark runs across CLI, CLI-plus-skills, and MCP arms (25 each), March 2026. Industry benchmark
- Apideck: Your MCP Server is Eating Your Context Window, The ~80-token CLI prompt and the 55,000+ token schema load in a 40-tool scenario. Technical analysis
- Model Context Protocol, Official Specification, Protocol specification revision 2026-07-28, SDK documentation, and server building guides. Documentation
- context-mode, Community plugin that indexes large MCP responses locally instead of loading them into context. Open-source project; savings figures are the project’s own
Common questions
What is an MCP server?
MCP, the Model Context Protocol, is a standard way to give an AI tool structured access to external systems: data sources, APIs and actions. An MCP server exposes those capabilities so the model can call them reliably, rather than you pasting context in by hand.
When should you build an MCP server instead of using a CLI tool?
Reach for a CLI tool first: it is simpler and cheaper when the model can just run a command. Build an MCP server when you need structured, typed access, a long-running process behind the tools, or capabilities a one-shot command cannot express, and nothing off the shelf fits.
What should you watch for when building an MCP server?
Security and debugging. The server can expose real systems, so scope permissions tightly and log calls; and because failures can be opaque, build in clear error output early. I treat the security boundary as a design decision, not an afterthought.