agent_config/agents/cline/
mcp.rs1use 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 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}