Skip to content

Keeping AI Coding Tool Configs in Sync: Building a Bidirectional MCP Configuration Manager

Published:  at 11:12 AM
Table of Contents

TLDR;

Keep MCP servers configuration between Claude Code and Kilo Code in sync using Claude Code Hooks and launchd (primary process management system on macOS)

Pain Point

Each AI coding tool stores its global MCP server configuration in a different location with a somewhat different format.

Real scenario would look like:

  1. Add it to Claude Code’s config
  2. Notice that you haven’t installed the MCP in Kilo while coding with Kilo
  3. Install
  4. Resume coding

Challenge

Let’s look at what we’re dealing with:

Claude Code

Kilo Code (VSCode Extension)

The Real Problem

Solution: Claude Code Hooks & Shell Scripting with Launchd

I built a bidirectional sync system that lives in my dotfiles. Here’s the architecture:

Key Components

The system consists of four main scripts:

  1. extract.sh - Extracts the mcpServers section from Claude’s massive config file
  2. merge.sh - Merges MCP servers back into Claude’s config (with automatic backups)
  3. sync.sh - Handles bidirectional sync between dotfiles and Kilo Code
  4. setup.sh - Installs the launchd agent for background automation

Plus a launchd agent that watches Kilo Code’s config file for changes.

How It Works

Scenario A: Claude Code → Kilo Code

  1. You end a Claude Code session
  2. SessionEnd hook triggers (configured in ~/.claude/settings.json)
  3. Extract script runs: Pulls mcpServers from ~/.claude.json → saves to ai-config-sync/config/mcp-servers.json
  4. Sync script runs: Copies mcp-servers.json → Kilo Code’s mcp_settings.json
  5. Done! Kilo Code now has your latest MCP servers

Scenario B: Kilo Code → Claude Code

  1. You add an MCP server in Kilo Code’s UI
  2. Kilo saves to mcp_settings.json
  3. Launchd detects the file change (watches the file 24/7)
  4. Sync script runs:
    • Copies Kilo’s mcp_settings.jsonai-config-sync/config/mcp-servers.json
    • Runs merge script to update ~/.claude.json
  5. Done! Claude Code now has your new server

Visual Flow

# Claude → Kilo (on session end)
~/.claude.json → [extract] → ai-config-sync/config/mcp-servers.json → [sync] → Kilo

# Kilo → Claude (on file change)
Kilo [sync] → ai-config-sync/config/mcp-servers.json → [merge] → ~/.claude.json

Directory Structure

The final structure looks like this:

~/.dotfiles/ai-config-sync/
├── config/
│   └── mcp-servers.json        # Source of truth
├── scripts/
│   ├── extract.sh              # Extract from ~/.claude.json
│   ├── merge.sh                # Merge back to ~/.claude.json
│   ├── sync.sh                 # Bidirectional sync
│   └── setup.sh                # Install launchd agent
├── launchd/
│   └── com.user.mcp-sync.plist # macOS background agent
├── logs/                       # Runtime logs
│   ├── sync.log
│   └── error.log
└── backups/                    # Config backups
    └── claude-*.json

Implementation Details

Why Launchd over Fswatch?

I initially considered fswatch, but launchd seemed like a better choice:

Featurelaunchdfswatch
Resource usage~2-5 MB (kernel-level)~10-20 MB (user process)
Auto-start✅ Yes (on login)❌ Need to configure
Survives reboot✅ Yes❌ No
Terminal required✅ No❌ Yes (or use nohup)
Native to macOS✅ Yes❌ Requires Homebrew

⚙️ Understanding launchd vs cron vs systemd

  • launchd (macOS): Modern, event-driven, can watch files/directories
  • cron (Unix): Time-based only, can’t watch files
  • systemd (Linux): Similar to launchd, path units can watch files

For this use case, launchd’s WatchPaths feature is perfect—it triggers immediately when Kilo’s config changes.

The launchd plist is simple:

<key>WatchPaths</key>
<array>
    <string>/Users/ryuton/Library/Application Support/.../mcp_settings.json</string>
</array>

<key>ProgramArguments</key>
<array>
    <string>/Users/ryuton/.dotfiles/ai-config-sync/scripts/sync.sh</string>
    <string>--to-claude</string>
</array>

<key>ThrottleInterval</key>
<integer>5</integer>

The ThrottleInterval prevents rapid-fire syncs if the file changes multiple times quickly.

JSON Validation

Before any sync operation, we validate JSON using jq:

if ! jq empty "$CLAUDE_CONFIG" 2>/dev/null; then
    log_error "Invalid JSON in Claude config"
    notify "MCP Sync Failed" "Invalid JSON in Claude config"
    exit 1
fi

This prevents corrupting configs with malformed JSON.

Backup Strategy

Every time we merge MCP servers back into ~/.claude.json, we create a timestamped backup:

BACKUP_FILE="$BACKUP_DIR/claude-$(date +%Y%m%d-%H%M%S).json"
cp "$CLAUDE_MAIN_CONFIG" "$BACKUP_FILE"

Backups are saved in ai-config-sync/backups/ (gitignored). If something goes wrong, recovery is one command away:

cp ~/.dotfiles/ai-config-sync/backups/claude-20251005-195038.json ~/.claude.json

Notifications

Cherry on top, the sync script sends macOS notifications so you know when syncs happen:

notify() {
    local title="$1"
    local message="$2"
    osascript -e "display notification \"$message\" with title \"$title\" sound name \"Glass\"" 2>/dev/null
}

Setting It Up

1. Copy The Scripts to Your dotfiles

# Clone the structure
mkdir -p ~/.dotfiles/ai-config-sync/{config,scripts,launchd,logs,backups}

# Add the scripts (see GitHub repo for full code)

2. Run The Setup script

~/.dotfiles/ai-config-sync/scripts/setup.sh

This will:

3. Configure Claude Code’s SessionEnd hook

Add to ~/.claude/settings.json:

{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.dotfiles/ai-config-sync/scripts/extract.sh && $HOME/.dotfiles/ai-config-sync/scripts/sync.sh --to-kilo"
          }
        ]
      }
    ]
  }
}

4. Add A Shell alias (optional but handy)

In your .zshrc

alias sync-mcp="$HOME/.dotfiles/ai-config-sync/scripts/sync.sh"

Resources