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