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