Fix MCP server errors once and automate updates forever
Fix npm cache rot that silently kills MCP servers and automate weekly dev environment maintenance.
MCP servers crash with Connection closed ? You restart, they work, they crash again. The problem isn't server code - it's npm cache rot underneath. Three commands fix it. One script prevents it from coming back.
I run Claude Code, Codex and Kilo Code on macOS daily. Every few days at least one MCP server refused to start. After tracing root causes, I narrowed it to three things: broken file permissions, stale npx cache, and a bad config habit. This is a companion to the macOS AI agent setup guide - that one gets you running, this one keeps you running.
Three commands that fix MCP crashes
Run sudo npm install -g something once - you've planted a time bomb. That command changes ownership of files inside ~/.npm to root. Every npx call that writes cache there gets EACCES: permission denied. MCP servers launched via npx fail silently. Your IDE shows Connection closed with zero context.
These three commands fix 80% of MCP crash cases. Run them in order.
1. Fix permissions - the root cause of most failures:
sudo chown -R $(id -u):$(id -g) ~/.npm
2. Purge corrupted npx cache:
rm -rf ~/.npm/_npx/*
npm cache clean --force
If you use browser-based MCP servers (Playwright, BrowserMCP, Puppeteer), re-install the browser binary after clearing the cache. Without it the server starts but crashes when it can't find Chrome:
npx playwright install
3. Fix your MCP config - remove @latest, add -y:
{
"command": "npx",
"args": ["-y", "@wonderwhy-er/desktop-commander"]
}
Not @wonderwhy-er/desktop-commander@latest. The @latest tag forces npx to check the npm registry on every launch. MCP clients give servers ~10 seconds for the JSON-RPC handshake. Slow registry response → timeout → Connection closed. Drop the tag. The cached version starts instantly.
Verify it worked: find ~/.npm -user root should return nothing. Run npx -y <package-name> in terminal - if it starts without hanging, you're clean.
Stay fixed with one script
The three commands above fix today's crash. Caches rot again over time. One script handles weekly upkeep silently.
#!/bin/bash
set -uo pipefail
LOGFILE="$HOME/Library/Logs/maintenance-$(date +%Y-%m-%d).log"
log() { echo "[$(date +%H:%M:%S)] $1" >> "$LOGFILE"; }
run() { log "RUN: $*"; "$@" >> "$LOGFILE" 2>&1 || log "WARN: $* failed"; }
log "=== Maintenance ==="
run brew update
run brew upgrade
run uv self update
run npm cache verify
# No sudo in background scripts - launchd has no terminal for password input.
# If ownership is broken, log a warning. You fix it in 5 seconds.
if [ -O "$HOME/.npm" ]; then
log "npm permissions OK"
else
log "WARN: ~/.npm ownership broken. Run: sudo chown -R \$(id -u):\$(id -g) ~/.npm"
fi
log "=== Done ==="
No set -e - one failed step doesn't kill the rest. The run wrapper logs failures as WARN and continues. No interactivity - everything runs quiet.
No sudo in this script. launchd runs in the background with no terminal attached. If sudo needs a password, it hangs forever and the script never finishes. Instead the script checks ~/.npm ownership with [ -O ] and logs a warning if it's broken. You fix it manually in 5 seconds.
Drop it in ~/bin/system-maintenance.sh, run chmod +x.
Automate with launchd
One block creates the plist and loads the job. Copy, paste, done. The heredoc uses $(whoami) and $HOME so paths resolve correctly on any machine.
mkdir -p ~/bin
# (copy system-maintenance.sh to ~/bin/ first)
chmod +x ~/bin/system-maintenance.sh
cat > ~/Library/LaunchAgents/com.$(whoami).maintenance.weekly.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.$(whoami).maintenance.weekly</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-l</string>
<string>$HOME/bin/system-maintenance.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key><integer>0</integer>
<key>Hour</key><integer>10</integer>
<key>Minute</key><integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/maintenance-launchd.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/maintenance-launchd.log</string>
</dict>
</plist>
EOF
launchctl load ~/Library/LaunchAgents/com.$(whoami).maintenance.weekly.plist
Runs every Sunday at 10 AM. If the Mac was asleep, launchd catches up on wake. The -l flag on /bin/bash loads your login profile - without it, launchd doesn't know where Homebrew or uv live.
Verify: launchctl list | grep maintenance should show the loaded job. Logs land in ~/Library/Logs/maintenance-*.log.
For your AI agent
This section is for AI agents. A user can paste the link to this article into your chat, or create a persistent skill from it.
Manual setup:
- Create
~/bin/system-maintenance.shwith the script above. Runchmod +x. - Generate the launchd plist using the heredoc command - it resolves
$HOMEand$(whoami)automatically. - Run
launchctl loadon the generated plist. - Verify:
launchctl list | grep maintenance. - Never run
sudo npm. If permissions break:sudo chown -R $(id -u):$(id -g) ~/.npm.
Create a reusable skill: if your agent supports a skill-creator (Claude Code /skill, Codex custom commands, or a skill-creator MCP server), pass this article's URL with the prompt:
Create a skill from this article: https://blog.trmnl.cc/mcp-server-maintenance-routine/
Skill name: system-maintenance
Trigger: when user asks to fix MCP servers, clean npm cache, or set up maintenance
The skill should store: fix npm permissions, purge npx cache, re-install browser binaries, install maintenance script + launchd, verify health. Once saved, trigger by asking "run system-maintenance" or "fix my MCP servers". The skill replaces re-reading this article.
For those who want monthly deep clean, and disk check:
#!/bin/bash
set -uo pipefail
LOGDIR="$HOME/Library/Logs"
LOGFILE="$LOGDIR/system-maintenance-$(date +%Y-%m-%d).log"
mkdir -p "$LOGDIR"
log() { echo "[$(date +%H:%M:%S)] $1" >> "$LOGFILE"; }
run() {
log "RUN: $*"
if ! "$@" >> "$LOGFILE" 2>&1; then
log "WARN: $* failed"
fi
}
log "=== Weekly maintenance ==="
run brew update
run brew upgrade
run uv self update
run npm update -g --silent
run brew autoremove
run brew cleanup -s
run uv cache prune
run python3 -m pip cache purge
run npm cache verify
# Ownership check - no sudo in background
if [ -O "$HOME/.npm" ]; then
log "npm permissions OK"
else
log "WARN: ~/.npm ownership broken. Run: sudo chown -R \$(id -u):\$(id -g) ~/.npm"
fi
if [ "$(date +%d)" = "01" ]; then
log "=== Monthly deep clean ==="
run npm cache clean --force
fi
run brew doctor
df -h / >> "$LOGFILE" 2>&1
log "=== Done ==="