MCP wrapper pattern: make README setups survive real machines
Claude Code connects to MCP servers — external tool providers that run as child processes. You configure them in ~/.claude/settings.json: a command to launch, environment variables to pass. The server starts on stdio, Claude Code sends JSON-RPC tool calls, and the server responds. Simple enough on paper.
I run Fastmail — have since before they had a mobile app. I use them as the mail broker for every vanity domain I own, and they invented JMAP, the protocol that replaced IMAP for anyone paying attention. I wrote a Terraform module that configures MX, SPF, DKIM, and DMARC for Fastmail across Route53 and Cloudflare (wrote about it here), and my vanity-dns repo consumes it. When I add a new domain to the stable, email is deliverable on day one — DNS is already configured before I tell Fastmail about it. Gmail MCPs handle my work Google Workspace accounts, but personal mail runs through Fastmail and its infrastructure is code I own. There’s a community MCP server for it. Getting it connected to Claude Code took longer than it should have.
I spent an hour debugging “Failed to get session: Unauthorized” from that MCP server with the correct token in the config file. The token worked fine with curl. The server started fine when I ran it manually. Claude Code’s tool calls returned 401 every time.
Three things were wrong, none of them obvious.
First, the npm cache had root-owned files from a previous sudo npm install. npx failed silently — no “permission denied,” just a server that never started. The only symptom was “Unauthorized” because no server means no auth.
Second, I’d put the API token in the config file’s env block and in a wrapper script’s env file. The wrapper sources its file with set -a, so its token wins. I rotated the token, updated the config, and the wrapper kept sending the old one. Two sources of truth, one of them a decoy.
Third, MCP servers on stdio have no log you can tail. The server is a black box once Claude Code spawns it. When auth fails, you can’t tell whether the token is wrong, the server didn’t start, or the env vars never arrived.
Seven lines that own the startup
A wrapper script that owns the entire startup:
#!/usr/bin/env bash
set -euo pipefail
set -a
source ~/.config/fastmail-mcp/env
set +a
mkdir -p /tmp/fastmail-mcp-npm-cache
export npm_config_cache=/tmp/fastmail-mcp-npm-cache
exec npx -y github:MadLlama25/fastmail-mcp fastmail-mcp
Then ~/.claude/settings.json becomes one line with no secrets:
"fastmail": {
"command": "/Users/fizz/.local/bin/fastmail-mcp-wrapper"
}
Credentials live in one place. npm cache is isolated. And you can run the wrapper in a terminal to see exactly what happens — something you can’t do when Claude Code spawns the process for you.
For each MCP server, ask: does it need secrets, does it depend on cache state, do I need deterministic startup across terminal sessions and GUI launches? If yes to any of those, wrap it. The upstream README gives you the baseline. The wrapper gives you operational reality.