fizz.today

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.

The fix

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.

The rule

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.

#mcp #claude-code #platformengineering #devtools