# mcp-sync
**Sync canonical MCP configuration to multiple AI coding assistants**
`mcp-sync` reads a single `mcp.yaml` file and syncs the server definitions to configuration files for:
- **Antigravity** (VS Code extension)
- **Claude Code** (CLI)
- **Codex** (OpenAI CLI)
- **OpenCode**
- **Custom targets** (dynamic libraries)
## Installation
```bash
cargo install --path .
```
## Quick Start
```bash
# One-time sync to all targets
mcp-sync sync
# Watch for changes and re-sync
mcp-sync watch
# Sync only global configs
mcp-sync sync --scope global
# Dry run (show what would change)
mcp-sync sync --dry-run --verbose
# Clean up servers not in canonical file
mcp-sync sync --clean
```
## Canonical Format (`mcp.yaml`)
```yaml
version: 1
# Optional: plugins for transforming config
plugins:
- name: env-expander
config:
prefix: "${"
suffix: "}"
servers:
# stdio server (local process)
my-server:
command: npx
args: ["-y", "@modelcontextprotocol/server"]
env:
API_KEY: "${API_KEY}"
cwd: /path/to/workdir
enabled: true # default: true
# http server (remote)
remote-server:
kind: http
url: https://mcp.example.com/v1
headers:
Authorization: "Bearer ${TOKEN}"
```
## CLI Options
| `--canon <PATH or URL>` | Path or URL to canonical mcp.yaml (default: `mcp.yaml`) |
| `--scope <SCOPE>` | Sync scope: `global`, `project`, or `both` (default: `both`) |
| `--project-root <PATH>` | Project root (default: auto-detect git root) |
| `--clean` | Remove servers not present in canonical file |
| `--dry-run` | Print changes without writing files |
| `--log-level <LEVEL>` | Log level: trace, debug, info, warn, error (default: info) |
| `--plugin <PATH>` | Load additional plugin library (can be repeated) |
| `--target <PATH>` | Load custom target library (can be repeated) |
| `--only-target <NAMES>` | Sync only to specific targets (comma-separated or `all`) |
## Commands
### `sync` (default)
One-time sync to all targets.
### `watch`
Watch mcp.yaml for changes and re-sync automatically.
### `init`
Interactive wizard to create mcp.yaml:
```bash
mcp-sync init --output mcp.yaml
```
### `validate`
Validate mcp.yaml syntax and schema:
```bash
mcp-sync validate --canon mcp.yaml
```
### `diff`
Show differences between canon and current target configs:
```bash
mcp-sync diff --canon mcp.yaml
```
### `completions`
Generate shell completions:
```bash
# Bash
mcp-sync completions bash >> ~/.bashrc
# Zsh
mcp-sync completions zsh >> ~/.zshrc
# Fish
mcp-sync completions fish > ~/.config/fish/completions/mcp-sync.fish
```
## Remote Canon
Load mcp.yaml from a URL:
```bash
mcp-sync sync --canon https://example.com/team/mcp.yaml
```
## Examples
### Sync with Custom Target
```bash
# All built-in targets + custom target
mcp-sync sync --target ./my-target.dylib
# Multiple custom targets
mcp-sync sync --target ./target1.dylib --target ./target2.dylib
# Only Claude + custom target
mcp-sync sync --only-target claude --target ./my-target.dylib
```
### Sync with Custom Plugin
```bash
# Built-in env-expander + custom plugin
mcp-sync sync --plugin ./secret-injector.dylib
```
### Watch Mode with Custom Config
```bash
# Watch and sync on changes
mcp-sync watch --canon ~/my-mcp.yaml --verbose
```
### CI/CD Integration
```bash
# Sync global configs, fail on error
mcp-sync sync --scope global --canon mcp.yaml
```
## Target File Locations
### Global (User-level)
| Antigravity | `~/Library/Application Support/Antigravity/User/mcp.json` | `~/.config/Antigravity/User/mcp.json` | `%APPDATA%\Antigravity\User\mcp.json` |
| Claude | `~/.claude.json` | `~/.claude.json` | `%USERPROFILE%\.claude.json` |
| Codex | `~/.codex/config.toml` | `~/.codex/config.toml` | `%APPDATA%\codex\config.toml` |
| OpenCode | `~/.config/opencode/opencode.json` | `~/.config/opencode/opencode.json` | `%APPDATA%\opencode\opencode.json` |
### Project-level
| Antigravity | `.vscode/mcp.json` |
| Claude | `.mcp.json` |
| Codex | `.codex/config.toml` |
| OpenCode | `opencode.json` |
## Plugin System
Plugins can transform configurations during sync.
### Built-in: `env-expander`
Expands environment variables in config values:
```yaml
plugins:
- name: env-expander
config:
prefix: "${"
suffix: "}"
```
### Custom Plugins
Create a dynamic library that exports `create_plugin`:
```rust
use mcp_sync::{Canon, Plugin};
use anyhow::Result;
use serde_json::Value as JsonValue;
pub struct MyPlugin { /* ... */ }
impl Plugin for MyPlugin {
fn name(&self) -> &str { "my-plugin" }
fn on_load(&mut self, config: &JsonValue) -> Result<()> {
Ok(())
}
fn transform_canon(&self, canon: &mut Canon) -> Result<()> {
// Modify config before syncing
Ok(())
}
fn transform_output(&self, target: &str, value: &mut JsonValue) -> Result<()> {
// Modify target-specific output
Ok(())
}
}
#[unsafe(no_mangle)]
pub extern "C" fn create_plugin() -> *mut dyn Plugin {
Box::into_raw(Box::new(MyPlugin::new()))
}
```
## Custom Targets
Create custom sync targets for unsupported tools.
### Implementation
```rust
use mcp_sync::{Canon, SyncOptions, Target};
use anyhow::Result;
use std::path::{Path, PathBuf};
pub struct MyTarget;
impl Target for MyTarget {
fn name(&self) -> &'static str { "MyTarget" }
fn global_path(&self) -> Result<PathBuf> {
Ok(PathBuf::from("/path/to/global/config.json"))
}
fn project_path(&self, root: &Path) -> PathBuf {
root.join(".my-target-config.json")
}
fn sync(&self, path: &Path, canon: &Canon, opts: &SyncOptions) -> Result<()> {
// Write config in your tool's format
Ok(())
}
}
#[unsafe(no_mangle)]
pub extern "C" fn create_target() -> *mut dyn Target {
Box::into_raw(Box::new(MyTarget))
}
```
### Build
```bash
# Cargo.toml
[lib]
crate-type = ["cdylib"]
[dependencies]
mcp-sync = { path = "..." }
anyhow = "1"
```
```bash
cargo build --release
# Output: target/release/libmy_target.dylib
```
### Use
```bash
mcp-sync sync --target ./target/release/libmy_target.dylib
```
## Backups
Before modifying any file, `mcp-sync` creates a timestamped backup:
```
config.json → config.json.bak.20260105-230041
```
## License
MIT