Building MCP Servers for Claude Code: A Production Pattern for Custom Tooling
Deploy custom MCP servers that give Claude safe, structured access to your APIs and databases—a production-ready pattern from infrastructure first principles.
Building MCP Servers for Claude Code: A Production Pattern for Custom Tooling
If you've been using Claude Code for more than a few days, you've hit the same wall I did: the built-in tools are useful, but the moment you need Claude to interact with your own internal APIs, databases, or deployment pipelines, you're copy-pasting context into the chat. The Model Context Protocol (MCP) solves this. Building MCP servers for Claude Code is a pattern worth getting right from the start.
This tutorial is for engineers who want a production-ready shape for custom MCP tooling — not a toy example that collapses under real usage.
What MCP Actually Is (and Isn't)
MCP is an open protocol that exposes tools, resources, and prompts to an LLM host like Claude Code. Think of it as a structured RPC layer: Claude calls your server, your server does something useful, Claude gets back structured data. Anthropic launched MCP in November 2024 and the community has since built thousands of server implementations across every major programming language.
It is not a plugin marketplace or a cloud service. You run the server. You control the transport. That matters for security when your tools touch production credentials.
The three primitives
Understanding what each primitive is for prevents you building the wrong thing:
| Primitive | Purpose | Example |
|---|---|---|
| Tools | Callable functions that do something (side effects allowed) | create_jira_ticket, run_db_query |
| Resources | Read-only data the model can inspect | file://, db://schema, a REST API response |
| Prompts | Reusable prompt templates with parameters | A canned "summarise PR diff" prompt your whole team shares |
Most internal tooling starts with tools. Resources and prompts are worth adding once the core loop works.
The Architecture I Reach For
For anything beyond a quick prototype, I use a three-layer structure:
The MCP server sits as a thin orchestration layer. It validates inputs, calls your real services, and returns clean structured output. Keep business logic in your services, not in the MCP server. That separation saves you when testing the server without spinning up a full environment.
Stdio vs SSE transport — which to pick
For Claude Code specifically, stdio is almost always the right choice for local and per-project servers. The process is spawned by Claude Code directly, inherits a controlled environment, and dies with the session. There is no port to manage, no TLS to configure, and no firewall rule to punch.
SSE (Server-Sent Events) transport — which runs over HTTP — is the right choice when you need a shared server: one instance serving a whole team, or a server deployed behind your corporate auth layer. The trade-off is operational overhead: you now have a long-running process, a health check, and a URL to distribute.
If you are not sure, start with stdio. You can migrate to SSE later without changing any tool logic.
Bootstrapping the Server
The official @modelcontextprotocol/sdk for Node.js is the most actively maintained option (12.7k GitHub stars, 1,500+ commits as of mid-2025). Python's mcp package is a solid alternative if your team lives in that ecosystem.
Add a tsconfig.json targeting ESM (the SDK ships as ESM):
Create src/index.ts:
Zod schema validation on inputs is non-negotiable in production. Claude sends malformed arguments when inference goes sideways — validate everything at the boundary.
Registering With Claude Code
Claude Code looks for MCP server configuration in two places, applied in precedence order:
- User-level —
~/.claude/settings.json(available in every project) - Project-level —
.claude/settings.jsonin the project root (checked into source control, shared with your team)
For a team-shared internal tool, commit the project-level file. In your project root:
cwdmatters. If Claude Code is not finding your server binary, a missing or wrongcwdis the first thing to check. Relative paths inargsare resolved relative tocwd, not the project root.
Build your TypeScript (npx tsc), restart Claude Code, and your tools appear in the tool list immediately. No daemon restart, no configuration portal.
Verifying registration worked
Run this in a Claude Code session:
The /mcp command lists every registered server and its status. A green tick means the server process started and responded to the MCP handshake. A red cross means the process failed — check stderr output, which Claude Code surfaces inline.
Production Concerns You Can't Skip
1. Error handling with structure
Return errors as structured content, not thrown exceptions. Claude handles structured error text gracefully; uncaught exceptions crash the server process and leave Claude with a blank tool response.
A real failure scenario worth knowing: if your internal API returns a 503 and you throw an unhandled rejection, the MCP server process exits. Claude Code will show a generic "tool call failed" with no useful context. With isError: true, Claude Code displays your error message inline and Claude can reason about it: "the deployment status API is unavailable — I'll skip that step and tell the user."
2. Stdout is the protocol channel — log to stderr only
MCP over stdio means stdout is the wire protocol. A single stray console.log corrupts the JSON framing and produces the most confusing debugging session you will have all week:
Establish a logging convention at the top of your entry point:
Claude Code captures stderr from the MCP server process and surfaces it when you run /mcp, so your debug output is accessible without additional log infrastructure.
3. Secrets management
Never hardcode credentials. Three patterns that work in practice:
Environment variables via env block (simplest):
Suitable for local development. Check .claude/settings.json into .gitignore when it contains secrets, or use a separate settings.local.json that is gitignored.
Shell environment inheritance (team-friendly):
Omit the env block entirely. The MCP server process inherits the shell environment that launched Claude Code. If your team uses direnv or a .envrc file, secrets are available automatically and never touch version control.
Secrets manager at startup (production-grade):
Pull secrets once at startup, store in a module-level object, and re-fetch on a schedule if rotation is a concern. This pattern works well when your MCP server runs as a shared SSE process for a team.
4. Tool granularity and description quality
Resist building one giant run_anything tool. Claude's tool selection is driven almost entirely by the tool name and description. Narrow, well-named tools with precise descriptions outperform broad ones by a significant margin.
Compare these two descriptions for the same underlying operation:
The second description tells Claude when to use the tool, what it returns, and what it is good for. That context is the difference between a tool that gets used correctly and one that never fires.
5. Rate limiting and back-pressure
Claude is an eager agent. If a tool is available, it will call it — repeatedly, in parallel if the task warrants it. Build back-pressure in from the start:
Without this, a single Claude Code session exploring your codebase can saturate an internal API that was not designed for burst traffic.
Structuring a Maintainable Server
Treat your MCP server as a first-class service, not a throwaway script. The structure that scales well:
Each tool file exports a registration function:
This keeps code review diffs small and makes it obvious where to add a new tool.
Testing Without Claude Code
You do not need a live Claude Code session to test tool logic. Unit-test your tools in isolation against a stub API:
The key pattern: extract your handler function so it is directly importable and testable without instantiating an McpServer. The registration call in tools/deployment.ts just wires the handler into the server — the logic itself lives in a plain async function.
For end-to-end testing, the MCP SDK ships with an InMemoryTransport that lets you connect a test client and server without any socket or stdio:
This is the closest you get to Claude Code's real call path without running the full application.
Common Failure Modes
These are the debugging scenarios you will encounter. Knowing them upfront saves hours:
Server starts but tools don't appear
Check that cwd in your settings.json points to the right directory and that the compiled dist/index.js actually exists. Claude Code won't error loudly if the process exits immediately — run /mcp to see the server status.
SyntaxError: Unexpected token in Claude Code's tool output
A console.log leaked onto stdout. Search your codebase for any call that writes to process.stdout directly or via console.log. Replace with console.error or the log helper from the Logging section above.
Tool succeeds but Claude ignores the result
The response shape is wrong. The content array must contain objects with a type field ("text", "image", or "resource"). Returning a bare string or a non-conforming object silently produces an empty result.
Claude calls the tool in an infinite loop Your tool description is too vague, or the tool returns data that prompts re-invocation. Add a sentence to the description like: "Call this once per user request — do not call it repeatedly to paginate." Explicit instruction in the description is the fastest fix.
Environment variables missing at runtime
The env block in settings.json replaces the process environment rather than extending it on some platforms. If your tool relies on PATH or other system variables, pass them through explicitly or switch to shell environment inheritance (omit the env block entirely).
What This Unlocks
Once this pattern runs, Claude Code stops being a smart editor and becomes an agent that understands your stack. It can query internal APIs, check deployment state, search documentation, or scaffold code that conforms to your actual conventions — not generic ones.
The practical ceiling here is surprisingly high. Anthropic's own engineering team has demonstrated agents connected to hundreds of tools across dozens of MCP servers — at that scale, tool selection and context window management become the limiting factors, but for a typical internal toolset of 10–30 tools, the pattern above handles everything you need.
Start with one tool. Wire it to the real API. Get the error handling right. Then iterate.
Damian Hodgkiss
Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.