mcp_sync/targets/
claude.rs

1//! Claude Code CLI target implementation.
2
3use crate::{
4    canon::{canon_names, Canon},
5    target::{SyncOptions, Target},
6    utils::{home, json_get_obj_mut, log_verbose, read_json_file, write_json_file},
7};
8use anyhow::{anyhow, Result};
9use serde_json::Value as JsonValue;
10use std::path::{Path, PathBuf};
11
12/// Claude Code CLI target.
13///
14/// File format: `{ "mcpServers": { name: {...} } }`
15pub struct ClaudeTarget;
16
17impl Target for ClaudeTarget {
18    fn name(&self) -> &'static str {
19        "Claude"
20    }
21
22    fn global_path(&self) -> Result<PathBuf> {
23        let home = home()?;
24        
25        // Claude uses ~/.claude.json on all platforms
26        Ok(home.join(".claude.json"))
27    }
28
29    fn project_path(&self, project_root: &Path) -> PathBuf {
30        project_root.join(".mcp.json")
31    }
32
33    fn sync(&self, path: &Path, canon: &Canon, opts: &SyncOptions) -> Result<()> {
34        let mut root = read_json_file(path)?;
35        let mcp = json_get_obj_mut(&mut root, "mcpServers")?;
36
37        for (name, s) in &canon.servers {
38            let cfg = if s.kind() == "http" {
39                let url = s.url.clone().ok_or_else(|| anyhow!("server {}: http missing url", name))?;
40                let mut obj = serde_json::Map::new();
41                obj.insert("type".into(), JsonValue::String("http".into()));
42                obj.insert("url".into(), JsonValue::String(url));
43                if let Some(h) = &s.headers {
44                    obj.insert("headers".into(), serde_json::to_value(h)?);
45                }
46                JsonValue::Object(obj)
47            } else {
48                let cmd = s.command.clone().ok_or_else(|| anyhow!("server {}: stdio missing command", name))?;
49                let mut obj = serde_json::Map::new();
50                obj.insert("command".into(), JsonValue::String(cmd));
51                obj.insert(
52                    "args".into(),
53                    JsonValue::Array(
54                        s.args.clone().unwrap_or_default().into_iter().map(JsonValue::String).collect(),
55                    ),
56                );
57                if let Some(env) = &s.env {
58                    obj.insert("env".into(), serde_json::to_value(env)?);
59                }
60                JsonValue::Object(obj)
61            };
62            mcp.insert(name.clone(), cfg);
63        }
64
65        if opts.clean {
66            let canon_set = canon_names(canon);
67            let keys: Vec<String> = mcp.keys().cloned().collect();
68            for k in keys {
69                if !canon_set.contains(&k) {
70                    mcp.remove(&k);
71                }
72            }
73        }
74
75        let label = if path.starts_with(home().unwrap_or_default()) {
76            "global"
77        } else {
78            "project"
79        };
80        log_verbose(opts.verbose, format!("{} {} -> {:?}", self.name(), label, path));
81        write_json_file(path, &root, opts.dry_run)
82    }
83}