Skip to main content

lean_ctx/setup/
mcp.rs

1//! Per-agent MCP configuration (configure/disable, target resolution).
2//!
3//! Split out of `setup/mod.rs`; `use super::*` re-imports the parent module’s
4//! aliases and sibling helpers. Public fns are re-exported via `pub(crate) use`.
5
6#[allow(clippy::wildcard_imports)]
7use super::*;
8
9/// Result of setting up a single agent with all steps.
10#[derive(Debug, Default)]
11pub struct AgentSetupResult {
12    pub mcp_ok: bool,
13    pub rules: crate::rules_inject::InjectResult,
14    pub skill_installed: bool,
15    pub errors: Vec<String>,
16}
17
18/// Complete per-agent setup: MCP config + global rules + skill + hook.
19/// Single source of truth — called by both `init --agent` and `setup`.
20pub fn setup_single_agent(
21    agent_name: &str,
22    global: bool,
23    mode: crate::hooks::HookMode,
24) -> AgentSetupResult {
25    let home = dirs::home_dir().unwrap_or_default();
26    let mut result = AgentSetupResult::default();
27
28    crate::hooks::install_agent_hook_with_mode(agent_name, global, mode);
29
30    match configure_agent_mcp(agent_name) {
31        Ok(()) => result.mcp_ok = true,
32        Err(e) => result.errors.push(format!("MCP config: {e}")),
33    }
34
35    result.rules = crate::rules_inject::inject_rules_for_agent(&home, agent_name);
36
37    if let Ok(path) = crate::rules_inject::install_skill_for_agent(&home, agent_name) {
38        result.skill_installed = path.exists();
39    }
40
41    result
42}
43
44pub fn configure_agent_mcp(agent: &str) -> Result<(), String> {
45    let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
46    let binary = resolve_portable_binary();
47
48    let targets = agent_mcp_targets(agent, &home)?;
49
50    let mut errors = Vec::new();
51    for t in &targets {
52        if let Err(e) = crate::core::editor_registry::write_config_with_options(
53            t,
54            &binary,
55            WriteOptions {
56                overwrite_invalid: true,
57            },
58        ) {
59            eprintln!(
60                "\x1b[33m⚠\x1b[0m  Could not configure {}: {}",
61                t.config_path.display(),
62                e
63            );
64            errors.push(e);
65        }
66    }
67
68    if agent == "kiro" {
69        install_kiro_steering(&home);
70    }
71
72    if agent == "vscode" || agent == "copilot" {
73        if let Err(e) = crate::core::editor_registry::plan_mode::write_vscode_plan_settings() {
74            eprintln!("\x1b[33m⚠\x1b[0m  VS Code plan mode: {e}");
75        }
76    }
77    if agent == "claude" || agent == "claude-code" {
78        if let Err(e) =
79            crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions()
80        {
81            eprintln!("\x1b[33m⚠\x1b[0m  Claude Code plan mode: {e}");
82        }
83    }
84
85    if errors.is_empty() {
86        Ok(())
87    } else {
88        Err(format!(
89            "{} config(s) could not be written. See warnings above.",
90            errors.len()
91        ))
92    }
93}
94
95pub(crate) fn agent_mcp_targets(
96    agent: &str,
97    home: &std::path::Path,
98) -> Result<Vec<EditorTarget>, String> {
99    let mut targets = Vec::<EditorTarget>::new();
100
101    let push = |targets: &mut Vec<EditorTarget>,
102                name: &'static str,
103                config_path: PathBuf,
104                config_type: ConfigType| {
105        targets.push(EditorTarget {
106            name,
107            agent_key: agent.to_string(),
108            detect_path: PathBuf::from("/nonexistent"), // not used in direct agent config
109            config_path,
110            config_type,
111        });
112    };
113
114    let pi_cfg = home.join(".pi").join("agent").join("mcp.json");
115
116    match agent {
117        "cursor" => push(
118            &mut targets,
119            "Cursor",
120            home.join(".cursor/mcp.json"),
121            ConfigType::McpJson,
122        ),
123        "claude" | "claude-code" => push(
124            &mut targets,
125            "Claude Code",
126            crate::core::editor_registry::claude_mcp_json_path(home),
127            ConfigType::McpJson,
128        ),
129        "augment" => {
130            push(
131                &mut targets,
132                "Augment CLI",
133                crate::core::editor_registry::augment_cli_settings_path(home),
134                ConfigType::McpJson,
135            );
136            push(
137                &mut targets,
138                "Augment (VS Code)",
139                crate::core::editor_registry::augment_vscode_mcp_path(home),
140                ConfigType::AugmentVsCode,
141            );
142        }
143        "windsurf" => push(
144            &mut targets,
145            "Windsurf",
146            home.join(".codeium/windsurf/mcp_config.json"),
147            ConfigType::McpJson,
148        ),
149        "codex" => {
150            let codex_dir =
151                crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
152            push(
153                &mut targets,
154                "Codex CLI",
155                codex_dir.join("config.toml"),
156                ConfigType::Codex,
157            );
158        }
159        "gemini" => {
160            push(
161                &mut targets,
162                "Gemini CLI",
163                home.join(".gemini/settings.json"),
164                ConfigType::GeminiSettings,
165            );
166            push(
167                &mut targets,
168                "Antigravity IDE",
169                home.join(".gemini/antigravity/mcp_config.json"),
170                ConfigType::McpJson,
171            );
172            push(
173                &mut targets,
174                "Antigravity CLI",
175                home.join(".gemini/antigravity-cli/mcp_config.json"),
176                ConfigType::McpJson,
177            );
178        }
179        "antigravity" => push(
180            &mut targets,
181            "Antigravity IDE",
182            home.join(".gemini/antigravity/mcp_config.json"),
183            ConfigType::McpJson,
184        ),
185        "antigravity-cli" => push(
186            &mut targets,
187            "Antigravity CLI",
188            home.join(".gemini/antigravity-cli/mcp_config.json"),
189            ConfigType::McpJson,
190        ),
191        "copilot" => push(
192            &mut targets,
193            "Copilot CLI",
194            home.join(".copilot/mcp-config.json"),
195            ConfigType::CopilotCli,
196        ),
197        "crush" => push(
198            &mut targets,
199            "Crush",
200            home.join(".config/crush/crush.json"),
201            ConfigType::Crush,
202        ),
203        "pi" => push(&mut targets, "Pi Coding Agent", pi_cfg, ConfigType::McpJson),
204        "qoder" => {
205            for path in crate::core::editor_registry::qoder_all_mcp_paths(home) {
206                push(&mut targets, "Qoder", path, ConfigType::QoderSettings);
207            }
208        }
209        "qoderwork" => push(
210            &mut targets,
211            "QoderWork",
212            crate::core::editor_registry::qoderwork_mcp_path(home),
213            ConfigType::McpJson,
214        ),
215        "cline" => push(
216            &mut targets,
217            "Cline",
218            crate::core::editor_registry::cline_mcp_path(),
219            ConfigType::McpJson,
220        ),
221        "roo" => push(
222            &mut targets,
223            "Roo Code",
224            crate::core::editor_registry::roo_mcp_path(),
225            ConfigType::McpJson,
226        ),
227        "kiro" => push(
228            &mut targets,
229            "AWS Kiro",
230            home.join(".kiro/settings/mcp.json"),
231            ConfigType::McpJson,
232        ),
233        "verdent" => push(
234            &mut targets,
235            "Verdent",
236            home.join(".verdent/mcp.json"),
237            ConfigType::McpJson,
238        ),
239        "jetbrains" | "amp" | "openclaw" => {
240            // Handled by dedicated install hooks (servers[] array / amp.mcpServers / mcp.servers)
241        }
242        "qwen" => push(
243            &mut targets,
244            "Qwen Code",
245            home.join(".qwen/settings.json"),
246            ConfigType::McpJson,
247        ),
248        "trae" => push(
249            &mut targets,
250            "Trae",
251            home.join(".trae/mcp.json"),
252            ConfigType::McpJson,
253        ),
254        "amazonq" => push(
255            &mut targets,
256            "Amazon Q Developer",
257            home.join(".aws/amazonq/default.json"),
258            ConfigType::McpJson,
259        ),
260        "opencode" => {
261            #[cfg(windows)]
262            let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
263                std::path::PathBuf::from(appdata)
264                    .join("opencode")
265                    .join("opencode.json")
266            } else {
267                home.join(".config/opencode/opencode.json")
268            };
269            #[cfg(not(windows))]
270            let opencode_path = home.join(".config/opencode/opencode.json");
271            push(
272                &mut targets,
273                "OpenCode",
274                opencode_path,
275                ConfigType::OpenCode,
276            );
277        }
278        "hermes" => push(
279            &mut targets,
280            "Hermes Agent",
281            home.join(".hermes/config.yaml"),
282            ConfigType::HermesYaml,
283        ),
284        "vscode" => push(
285            &mut targets,
286            "VS Code",
287            crate::core::editor_registry::vscode_mcp_path(),
288            ConfigType::VsCodeMcp,
289        ),
290        "zed" => push(
291            &mut targets,
292            "Zed",
293            crate::core::editor_registry::zed_settings_path(home),
294            ConfigType::Zed,
295        ),
296        "aider" => push(
297            &mut targets,
298            "Aider",
299            home.join(".aider/mcp.json"),
300            ConfigType::McpJson,
301        ),
302        "continue" => push(
303            &mut targets,
304            "Continue",
305            home.join(".continue/mcp.json"),
306            ConfigType::McpJson,
307        ),
308        "neovim" => push(
309            &mut targets,
310            "Neovim (mcphub.nvim)",
311            home.join(".config/mcphub/servers.json"),
312            ConfigType::McpJson,
313        ),
314        "emacs" => push(
315            &mut targets,
316            "Emacs (mcp.el)",
317            home.join(".emacs.d/mcp.json"),
318            ConfigType::McpJson,
319        ),
320        "sublime" => push(
321            &mut targets,
322            "Sublime Text",
323            home.join(".config/sublime-text/mcp.json"),
324            ConfigType::McpJson,
325        ),
326        _ => {
327            return Err(format!("Unknown agent '{agent}'"));
328        }
329    }
330
331    Ok(targets)
332}
333
334pub fn disable_agent_mcp(agent: &str, overwrite_invalid: bool) -> Result<(), String> {
335    let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
336
337    let mut targets = Vec::<EditorTarget>::new();
338
339    let push = |targets: &mut Vec<EditorTarget>,
340                name: &'static str,
341                config_path: PathBuf,
342                config_type: ConfigType| {
343        targets.push(EditorTarget {
344            name,
345            agent_key: agent.to_string(),
346            detect_path: PathBuf::from("/nonexistent"),
347            config_path,
348            config_type,
349        });
350    };
351
352    let pi_cfg = home.join(".pi").join("agent").join("mcp.json");
353
354    match agent {
355        "cursor" => push(
356            &mut targets,
357            "Cursor",
358            home.join(".cursor/mcp.json"),
359            ConfigType::McpJson,
360        ),
361        "claude" | "claude-code" => push(
362            &mut targets,
363            "Claude Code",
364            crate::core::editor_registry::claude_mcp_json_path(&home),
365            ConfigType::McpJson,
366        ),
367        "augment" => {
368            push(
369                &mut targets,
370                "Augment CLI",
371                crate::core::editor_registry::augment_cli_settings_path(&home),
372                ConfigType::McpJson,
373            );
374            push(
375                &mut targets,
376                "Augment (VS Code)",
377                crate::core::editor_registry::augment_vscode_mcp_path(&home),
378                ConfigType::AugmentVsCode,
379            );
380        }
381        "windsurf" => push(
382            &mut targets,
383            "Windsurf",
384            home.join(".codeium/windsurf/mcp_config.json"),
385            ConfigType::McpJson,
386        ),
387        "codex" => {
388            let codex_dir =
389                crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
390            push(
391                &mut targets,
392                "Codex CLI",
393                codex_dir.join("config.toml"),
394                ConfigType::Codex,
395            );
396        }
397        "gemini" => {
398            push(
399                &mut targets,
400                "Gemini CLI",
401                home.join(".gemini/settings.json"),
402                ConfigType::GeminiSettings,
403            );
404            push(
405                &mut targets,
406                "Antigravity IDE",
407                home.join(".gemini/antigravity/mcp_config.json"),
408                ConfigType::McpJson,
409            );
410            push(
411                &mut targets,
412                "Antigravity CLI",
413                home.join(".gemini/antigravity-cli/mcp_config.json"),
414                ConfigType::McpJson,
415            );
416        }
417        "antigravity" => push(
418            &mut targets,
419            "Antigravity IDE",
420            home.join(".gemini/antigravity/mcp_config.json"),
421            ConfigType::McpJson,
422        ),
423        "antigravity-cli" => push(
424            &mut targets,
425            "Antigravity CLI",
426            home.join(".gemini/antigravity-cli/mcp_config.json"),
427            ConfigType::McpJson,
428        ),
429        "copilot" => push(
430            &mut targets,
431            "Copilot CLI",
432            home.join(".copilot/mcp-config.json"),
433            ConfigType::CopilotCli,
434        ),
435        "crush" => push(
436            &mut targets,
437            "Crush",
438            home.join(".config/crush/crush.json"),
439            ConfigType::Crush,
440        ),
441        "pi" => push(&mut targets, "Pi Coding Agent", pi_cfg, ConfigType::McpJson),
442        "qoder" => {
443            for path in crate::core::editor_registry::qoder_all_mcp_paths(&home) {
444                push(&mut targets, "Qoder", path, ConfigType::QoderSettings);
445            }
446        }
447        "qoderwork" => push(
448            &mut targets,
449            "QoderWork",
450            crate::core::editor_registry::qoderwork_mcp_path(&home),
451            ConfigType::McpJson,
452        ),
453        "cline" => push(
454            &mut targets,
455            "Cline",
456            crate::core::editor_registry::cline_mcp_path(),
457            ConfigType::McpJson,
458        ),
459        "roo" => push(
460            &mut targets,
461            "Roo Code",
462            crate::core::editor_registry::roo_mcp_path(),
463            ConfigType::McpJson,
464        ),
465        "kiro" => push(
466            &mut targets,
467            "AWS Kiro",
468            home.join(".kiro/settings/mcp.json"),
469            ConfigType::McpJson,
470        ),
471        "verdent" => push(
472            &mut targets,
473            "Verdent",
474            home.join(".verdent/mcp.json"),
475            ConfigType::McpJson,
476        ),
477        "jetbrains" | "amp" | "openclaw" => {
478            // Not supported for disable via this helper.
479        }
480        "qwen" => push(
481            &mut targets,
482            "Qwen Code",
483            home.join(".qwen/settings.json"),
484            ConfigType::McpJson,
485        ),
486        "trae" => push(
487            &mut targets,
488            "Trae",
489            home.join(".trae/mcp.json"),
490            ConfigType::McpJson,
491        ),
492        "amazonq" => push(
493            &mut targets,
494            "Amazon Q Developer",
495            home.join(".aws/amazonq/default.json"),
496            ConfigType::McpJson,
497        ),
498        "opencode" => {
499            #[cfg(windows)]
500            let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
501                std::path::PathBuf::from(appdata)
502                    .join("opencode")
503                    .join("opencode.json")
504            } else {
505                home.join(".config/opencode/opencode.json")
506            };
507            #[cfg(not(windows))]
508            let opencode_path = home.join(".config/opencode/opencode.json");
509            push(
510                &mut targets,
511                "OpenCode",
512                opencode_path,
513                ConfigType::OpenCode,
514            );
515        }
516        "hermes" => push(
517            &mut targets,
518            "Hermes Agent",
519            home.join(".hermes/config.yaml"),
520            ConfigType::HermesYaml,
521        ),
522        "vscode" => push(
523            &mut targets,
524            "VS Code",
525            crate::core::editor_registry::vscode_mcp_path(),
526            ConfigType::VsCodeMcp,
527        ),
528        "zed" => push(
529            &mut targets,
530            "Zed",
531            crate::core::editor_registry::zed_settings_path(&home),
532            ConfigType::Zed,
533        ),
534        "aider" => push(
535            &mut targets,
536            "Aider",
537            home.join(".aider/mcp.json"),
538            ConfigType::McpJson,
539        ),
540        "continue" => push(
541            &mut targets,
542            "Continue",
543            home.join(".continue/mcp.json"),
544            ConfigType::McpJson,
545        ),
546        "neovim" => push(
547            &mut targets,
548            "Neovim (mcphub.nvim)",
549            home.join(".config/mcphub/servers.json"),
550            ConfigType::McpJson,
551        ),
552        "emacs" => push(
553            &mut targets,
554            "Emacs (mcp.el)",
555            home.join(".emacs.d/mcp.json"),
556            ConfigType::McpJson,
557        ),
558        "sublime" => push(
559            &mut targets,
560            "Sublime Text",
561            home.join(".config/sublime-text/mcp.json"),
562            ConfigType::McpJson,
563        ),
564        _ => {
565            return Err(format!("Unknown agent '{agent}'"));
566        }
567    }
568
569    for t in &targets {
570        crate::core::editor_registry::remove_lean_ctx_server(
571            t,
572            WriteOptions { overwrite_invalid },
573        )?;
574    }
575
576    Ok(())
577}