Skip to main content

agent_config/agents/cline/
mcp.rs

1//! Cline MCP surface. Global VS Code extension config at
2//! `Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`,
3//! keyed by server name under `mcpServers`.
4
5use std::path::PathBuf;
6
7use crate::agents::planning as agent_planning;
8use crate::error::AgentConfigError;
9use crate::integration::{InstallReport, McpSurface, UninstallReport};
10use crate::paths;
11use crate::plan::{InstallPlan, UninstallPlan};
12use crate::scope::{Scope, ScopeKind};
13use crate::spec::{HookSpec, McpSpec};
14use crate::status::StatusReport;
15use crate::util::{fs_atomic, mcp_json_object, ownership, safe_fs};
16
17use super::ClineAgent;
18
19impl ClineAgent {
20    pub(super) fn mcp_path(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
21        match scope {
22            Scope::Global => paths::cline_mcp_global_file(),
23            Scope::Local(_) => Err(AgentConfigError::UnsupportedScope {
24                id: "cline",
25                scope: ScopeKind::Local,
26            }),
27        }
28    }
29}
30
31impl McpSurface for ClineAgent {
32    fn id(&self) -> &'static str {
33        "cline"
34    }
35
36    fn supported_mcp_scopes(&self) -> &'static [ScopeKind] {
37        &[ScopeKind::Global]
38    }
39
40    fn mcp_status(
41        &self,
42        scope: &Scope,
43        name: &str,
44        expected_owner: &str,
45    ) -> Result<StatusReport, AgentConfigError> {
46        McpSpec::validate_name(name)?;
47        let cfg = Self::mcp_path(scope)?;
48        let presence = mcp_json_object::config_presence(&cfg, name)?;
49        let ledger = ownership::mcp_ledger_for(&cfg);
50        let recorded = ownership::owner_of(&ledger, name)?;
51
52        let primary_status = StatusReport::for_mcp(
53            name,
54            cfg.clone(),
55            ledger,
56            presence,
57            expected_owner,
58            recorded,
59        );
60
61        if matches!(
62            primary_status.status,
63            crate::status::InstallStatus::InstalledOwned { .. }
64                | crate::status::InstallStatus::InstalledOtherOwner { .. }
65        ) {
66            Ok(primary_status)
67        } else {
68            if let Ok(legacy_cfg) = paths::legacy_cline_mcp_global_file() {
69                if legacy_cfg.exists() {
70                    let legacy_presence = mcp_json_object::config_presence(&legacy_cfg, name)?;
71                    let legacy_ledger = ownership::mcp_ledger_for(&legacy_cfg);
72                    let legacy_recorded = ownership::owner_of(&legacy_ledger, name)?;
73                    let legacy_status = StatusReport::for_mcp(
74                        name,
75                        legacy_cfg,
76                        legacy_ledger,
77                        legacy_presence,
78                        expected_owner,
79                        legacy_recorded,
80                    );
81                    if matches!(
82                        legacy_status.status,
83                        crate::status::InstallStatus::InstalledOwned { .. }
84                            | crate::status::InstallStatus::InstalledOtherOwner { .. }
85                    ) {
86                        return Ok(legacy_status);
87                    }
88                }
89            }
90            Ok(primary_status)
91        }
92    }
93
94    fn plan_install_mcp(
95        &self,
96        scope: &Scope,
97        spec: &McpSpec,
98    ) -> Result<InstallPlan, AgentConfigError> {
99        agent_planning::mcp_json_object_install(
100            McpSurface::id(self),
101            scope,
102            spec,
103            Self::mcp_path(scope),
104        )
105    }
106
107    fn plan_uninstall_mcp(
108        &self,
109        scope: &Scope,
110        name: &str,
111        owner_tag: &str,
112    ) -> Result<UninstallPlan, AgentConfigError> {
113        let mut plan = agent_planning::mcp_json_object_uninstall(
114            McpSurface::id(self),
115            scope,
116            name,
117            owner_tag,
118            Self::mcp_path(scope),
119        )?;
120        if let Ok(legacy_cfg) = paths::legacy_cline_mcp_global_file() {
121            if legacy_cfg.exists() {
122                let legacy_plan = agent_planning::mcp_json_object_uninstall(
123                    McpSurface::id(self),
124                    scope,
125                    name,
126                    owner_tag,
127                    Ok(legacy_cfg),
128                )?;
129                plan.changes.extend(legacy_plan.changes);
130            }
131        }
132        Ok(plan)
133    }
134
135    fn install_mcp(
136        &self,
137        scope: &Scope,
138        spec: &McpSpec,
139    ) -> Result<InstallReport, AgentConfigError> {
140        spec.validate()?;
141        let cfg = Self::mcp_path(scope)?;
142        spec.validate_local_secret_policy(scope)?;
143        scope.ensure_contained(&cfg)?;
144
145        // Support legacy global settings migration
146        if !cfg.exists() {
147            if let Ok(legacy_cfg) = paths::legacy_cline_mcp_global_file() {
148                if legacy_cfg.exists() {
149                    let content = fs_atomic::read_capped_or_empty(&legacy_cfg)?;
150                    safe_fs::write(scope, &cfg, &content, true)?;
151                    let legacy_ledger = ownership::mcp_ledger_for(&legacy_cfg);
152                    if legacy_ledger.exists() {
153                        let new_ledger = ownership::mcp_ledger_for(&cfg);
154                        let ledger_content = fs_atomic::read_capped_or_empty(&legacy_ledger)?;
155                        safe_fs::write(scope, &new_ledger, &ledger_content, true)?;
156                    }
157                    let _ = safe_fs::remove_file(scope, &legacy_cfg);
158                    let legacy_ledger = ownership::mcp_ledger_for(&legacy_cfg);
159                    let _ = safe_fs::remove_file(scope, &legacy_ledger);
160                }
161            }
162        }
163
164        let ledger = ownership::mcp_ledger_for(&cfg);
165        mcp_json_object::install(&cfg, &ledger, spec)
166    }
167
168    fn uninstall_mcp(
169        &self,
170        scope: &Scope,
171        name: &str,
172        owner_tag: &str,
173    ) -> Result<UninstallReport, AgentConfigError> {
174        McpSpec::validate_name(name)?;
175        HookSpec::validate_tag(owner_tag)?;
176        let cfg = Self::mcp_path(scope)?;
177        scope.ensure_contained(&cfg)?;
178        let ledger = ownership::mcp_ledger_for(&cfg);
179        let mut report = mcp_json_object::uninstall(&cfg, &ledger, name, owner_tag, "mcp server")?;
180
181        if let Ok(legacy_cfg) = paths::legacy_cline_mcp_global_file() {
182            if legacy_cfg.exists() {
183                scope.ensure_contained(&legacy_cfg)?;
184                let legacy_ledger = ownership::mcp_ledger_for(&legacy_cfg);
185                let legacy_report = mcp_json_object::uninstall(
186                    &legacy_cfg,
187                    &legacy_ledger,
188                    name,
189                    owner_tag,
190                    "mcp server",
191                )?;
192                report.merge(legacy_report);
193            }
194        }
195        Ok(report)
196    }
197}