TLDR; To build an MCP server for Claude Code, write a small program that declares typed tools for an API, database or service Claude can’t reach well from the shell. Use a command-line tool when one exists: in one Scalekit benchmark, MCP used 4 to 32 times more tokens for the same job. Build one small tool per operation, log to stderr, test in the MCP Inspector, and give the server a token that can do only what its tools need.
When to build an MCP server
- An internal or proprietary system has no CLI (command-line tool) and no published server. Ready-made servers cover the common services; your deployment API or in-house tool needs its own.
- The work needs state between calls, or a login Claude can’t run from the shell. Browser automation keeps one browser open across calls. OAuth (the standard behind “Sign in with Google”), expiring access tokens and per-customer logins are painful through shell commands.
- Written instructions keep failing to enforce a rule the code could check. One team I studied swapped its CLAUDE.md instructions (the file Claude reads each session) for a custom MCP server that matches code against the codebase’s patterns. The team reported that review comments about broken conventions fell to near zero.
- Skip it when a shell command, an existing server, a CLAUDE.md instruction or a skill does the job.
How MCP servers work in Claude Code
MCP, the Model Context Protocol, is a standard for connecting Claude Code to tools outside your terminal. An MCP server is a program that offers Claude tools. Each tool has a name, a description and a JSON schema, which lists the inputs it accepts and their types. Claude Code finds the servers in your configuration at startup, learns their tools and calls them during a conversation.
They talk in JSON-RPC (small JSON requests and replies) over one of two transports. With stdio, Claude Code runs the server on your machine and talks to it through standard input and output; most custom servers use it. Streamable HTTP connects to a remote server, such as one a team shares.
Through the shell, Claude could do anything an MCP server does. MCP adds structure: typed parameters, validated schemas and predictable responses, so Claude doesn’t guess at curl flags or parse loose output.
That structure costs tokens. A token is a fragment of text, a word or part of one, and usage is billed per token. One Scalekit benchmark from March 2026 ran 75 trials across three setups: CLI only, CLI plus skills, and MCP. CLI-only task medians were roughly 1,300–9,400 tokens, and the CLI-only setup completed 100% of runs; MCP task medians were 32,000–82,000 tokens, and MCP completed 72%. The reported multiplier was 4x to 32x, and every MCP failure was a TCP-level connection timeout. That is evidence about the tested tools, not a universal MCP tax.
The cost comes from schemas. An Apideck vendor example reports 55,000+ tokens for about 40 tool schemas loaded up front: over a quarter of a 200K-token context window (the text Claude can see at once). The CLI prompt it compares them with takes about 80 tokens. Claude Code’s tool search, on by default, holds schemas back and loads a tool’s full schema only when Claude looks for that tool. Run /context to measure the servers you use.
MCP or a command-line tool
Before you add a server, ask whether a shell command and jq, a JSON filter, can do the job. gh issue list --json title,state,assignees | jq does what the GitHub MCP server’s issue tools do at a fraction of the token cost. If no CLI exists, choose between a server and a small CLI of your own. The CLI keeps its login in the keyring (the system’s password store) and prints JSON when another program reads its output. Build the server when the integration is too small to justify a CLI, the login can’t run from the shell, or the work needs a long-lived, typed interaction with state. The token economics guide puts MCP in the wider cost picture.
Can a shell command and jq do it?
yes
Use the CLIgh, aws, gcloud, jq, curl: anything with a help page
No CLI, and too small to justify writing one?
yes
Use MCPMany SaaS APIs and internal tools ship without one
A login Claude can’t run from the shell?
yes
Use MCPOAuth, expiring tokens, per-customer logins; a CLI of your own can handle these too
A long-lived, typed interaction with state?
yes
Use MCPA CLI carries JSON and files; a browser session kept open between calls may need MCP, though Playwright’s own CLI now keeps sessions too
Otherwise
Write a small CLILogin in the keyring, JSON when piped
How to build an MCP server for Claude Code
The example wraps an in-house deployment-status API; the pattern fits any web API. You need Node 22.19 or later, because the Inspector in step 4 requires it.
1. Set up the project on the right SDK line
mkdir deploy-status-mcp && cd deploy-status-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm pkg set type=module
The example uses version 1 of the TypeScript SDK (software development kit), @modelcontextprotocol/sdk. Version 1 gets fixes for at least six months after version 2’s July 2026 release. Version 2 ships as separate @modelcontextprotocol/server and @modelcontextprotocol/client packages, with its own tutorials. Check which version an example targets before you copy its imports.
zod checks what Claude sends against each tool’s declared inputs.
Check: npm ls @modelcontextprotocol/sdk shows a 1.x version, and package.json contains "type": "module".
2. Write one tool per operation
Keep each tool to one operation and a few inputs.
In index.js, set up the server, register each tool, then connect it over stdio:
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" });
// register each tool here (one is shown below)
await server.connect(new StdioServerTransport());
One tool definition:
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 { version, status, deployed_at, deployed_by } = await res.json();
return {
content: [{ type: "text", text: JSON.stringify({ service, version, status, deployed_at, deployed_by }) }],
};
}
);
The enum, a fixed list of allowed values, keeps Claude to the three service names. isError: true tells Claude the call failed and why. The reply carries only the fields Claude needs, so the API’s full response doesn’t fill the context window. API_BASE and API_TOKEN come from environment variables (process.env).
Older tutorials use server.tool(name, description, schema, handler), which is deprecated in the current @modelcontextprotocol/sdk line, so new code shouldn’t use it. To migrate, move the description and schema into a config object.
Check: each tool does one thing, and its description says in one line what that is.
3. Log to stderr, never stdout
Stdout (standard output) carries the JSON-RPC messages, so anything you print there corrupts the protocol. Log with console.error(), which writes to stderr (the error stream) and stays out of the conversation:
console.error(`[deploy-status] Fetching ${service} from ${API_BASE}`);
Check: node index.js 2>/dev/null starts and prints nothing (stop it with Ctrl+C).
4. Test the tools in the MCP Inspector
The official Inspector lets you call your tools before Claude Code is involved. Set DEPLOY_API_URL and DEPLOY_API_TOKEN in your shell first; take the token from your secret manager instead of typing it out. The Inspector passes the server only the variables you name with -e, so name both or every call fails:
npx @modelcontextprotocol/inspector \
-e DEPLOY_API_URL="$DEPLOY_API_URL" \
-e DEPLOY_API_TOKEN="$DEPLOY_API_TOKEN" \
node index.js
It opens a page in your browser where you list the tools, call them with test inputs and read the replies. Fix problems here: in Claude Code, a server that crashes on startup shows as a failed connection, and the full error is in its stderr.
Check: every tool is listed, a valid call returns data, and an invalid service name comes back as an error.
5. Add the server to Claude Code with claude mcp add
claude mcp add takes the server’s name, any options, then -- and the command that starts it. Pass an environment variable (a setting the server reads at startup) with -e:
claude mcp add deploy-status \
-e DEPLOY_API_URL=https://internal-api.company.com \
-- node path/to/deploy-status-mcp/index.js
Leave the token out of that command: -e writes its value into ~/.claude.json, your Claude Code configuration file. A server that needs a secret goes in .mcp.json (shown below), which reads the token from your environment. A remote server takes a URL in place of the command: claude mcp add --transport http <name> <url>.
--scope sets where a server is available. local, the default, is private to you in one project. project writes to .mcp.json, a file you commit to the repository so your team shares it. user applies across all your projects. Try a new server at local, or at project if it needs a secret, and move it to project once it proves useful.
At project scope, the entry in .mcp.json looks like this:
{
"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}"
}
}
}
}
${DEPLOY_API_TOKEN} references the token from your environment, so its value never sits in the file.
Check: /mcp in Claude Code lists deploy-status as connected with its tool. If it shows as failed, run node index.js by hand and read stderr.
6. Secure the server: limit what it can reach
A local stdio server runs with your user account’s access unless you add an operating-system or container boundary. A remote server has whatever access its host and its login give it. Either way, the permissions you grant are what an attacker can reach.
- Scope the token narrowly. A token’s scope is what it may do. If the server only reads deployment status, don’t give it a token that can trigger deployments. Give it a service account with only those permissions.
- Keep credentials out of logs and out of Claude’s view. Never log them to stderr, and never put tokens in tool descriptions or parameter schemas: Claude sees both. The data privacy guide covers gating the client data a server returns before the model sees it.
- Treat tool responses as untrusted input. Data from a database, an API or user-generated content can carry a prompt injection: text that tries to make Claude do something you didn’t intend. Check types and expected fields, keep fetched content apart from tool instructions, give Claude as few actions as the job allows, and have a person confirm anything with real consequences. Filtering out text that looks like an instruction fails: an attacker can rephrase it. Take most care with website content, support tickets and email bodies.
- On a team, keep an allowlist.
/mcpandclaude mcp listshow only one machine’s servers; that is no audit trail. Keep a list of approved servers, manage configuration centrally where the product allows, and review access on a schedule.
Check: a write or deploy call made with the server’s token is refused.
Pitfalls and how to debug an MCP server
| Symptom | Cause | Fix |
|---|---|---|
| Tools don’t appear | Server crashes on startup | Run node index.js by hand and read stderr |
✘ Failed to connect in claude mcp list |
Server exited at startup, often on a missing environment variable | Check that the environment variables are set in .mcp.json |
| Protocol errors or a dropped connection | Debug output written to stdout, the protocol channel | Log with console.error() only |
| 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 that the variable names match in .mcp.json and the server code |
| The context window fills after a few calls | Tools return whole API payloads | Return only the fields Claude needs; for large datasets, the community plugin context-mode indexes responses in a local search store |
In practice
I added fifteen MCP servers in my first week with Claude Code. By the end of the month I was running four. Most of the others worked, but a shell command did the bulk of their jobs cheaper, faster and more reliably. I removed GitHub (replaced by the gh CLI), Slack (replaced by curl to the webhook API) and a Notion MCP server that used 40,000+ tokens in schemas for features I used twice.
Two of the four were DataForSEO and Google Search Console (GSC). DataForSEO is a complex API with dozens of endpoints and no official CLI; GSC needs an OAuth2 service-account login that is fiddly from curl. Plain shell commands couldn’t handle either, so each needed a tool built for it. I built CLIs: dfs and gsc replaced both servers.
I now run seven CLIs in production, for Google Search Console, DataForSEO, GA4, Google Maps data, CRM reads, PageSpeed, and Australia’s VET training register. Each keeps its login in the keyring, prints tables in a terminal and JSON lines when piped, so Claude drives them like any other shell command.
Of the four, only Playwright is still an MCP server in my setup:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp"]
}
}
}
Playwright (@playwright/mcp, Microsoft’s official server) runs browser automation: screenshots, page snapshots, form filling and click sequences. It keeps a browser and its state alive between calls, which no one-shot shell command can do.
The fourth was Codex, a model from another maker that I used as a second code reviewer. Its MCP server stopped connecting, and the Codex CLI I run now has no server mode, so I call it from the shell like my other CLIs.
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
- Claude Code skills, hooks, subagents and commands: when to use each, the four extensibility primitives and how they compose with MCP
- Claude Code pricing: what plans, tokens and models cost, MCP’s context cost in the broader token picture
- Claude Code memory: CLAUDE.md, auto-memory and persistent context, choosing the right persistence layer
- Session craft, when to start fresh, resume, or compact, managing context when MCP servers fill it
Sources
- context-mode, Community plugin that indexes large MCP responses locally instead of loading them into context. Open-source project
- 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
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, and you don’t paste context in by hand.
When should you build an MCP server instead of using a CLI tool?
Use a command-line tool first. In one published benchmark (Scalekit, March 2026), MCP used 4 to 32 times more tokens than a shell command for the same job. Consider an MCP server when no command-line tool or ready-made server fits. Build one if the integration is too small to justify writing your own CLI, needs state kept between calls, needs a login Claude can’t run from the shell, or benefits from strictly checked inputs.
What should you watch for when building an MCP server?
Security and debugging. Give the server a token that can do only what its tools need, treat what the tools return as untrusted text, and keep credentials out of logs and tool descriptions. Log to stderr (the error stream), never stdout (standard output, which carries the protocol messages). Test the tools in the MCP Inspector before connecting Claude Code. When a server fails to start, Claude Code reports a failed connection, sometimes with an error message; the full error is in the server’s stderr.
How do you build an MCP server for Claude Code?
Install the MCP software development kit (SDK), write one small tool per operation, log to stderr, test the tools in the MCP Inspector, then register the server with claude mcp add or a .mcp.json file.