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