1use std::collections::{BTreeSet, HashMap};
2
3use crate::app_config::{AppType, McpApps, McpServer, MultiAppConfig};
4use crate::error::AppError;
5use crate::mcp;
6use crate::store::AppState;
7use serde_json::Value;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum McpLiveDriftKind {
11 InSync,
12 LiveOnly,
13 DbOnly,
14 Changed,
15 LiveInvalid,
16 Unknown,
17}
18
19#[derive(Debug, Clone)]
20pub struct McpLiveDriftEntry {
21 pub app: AppType,
22 pub id: String,
23 pub kind: McpLiveDriftKind,
24 pub db_spec: Option<Value>,
25 pub live_spec: Option<Value>,
26 pub message: Option<String>,
27}
28
29#[derive(Debug, Clone)]
30pub struct McpLiveDriftReport {
31 pub app: AppType,
32 pub entries: Vec<McpLiveDriftEntry>,
33}
34
35pub struct McpService;
37
38impl McpService {
39 pub fn get_all_servers(state: &AppState) -> Result<HashMap<String, McpServer>, AppError> {
41 let cfg = state.config.read()?;
42
43 if let Some(servers) = &cfg.mcp.servers {
45 return Ok(servers.clone());
46 }
47
48 Err(AppError::localized(
50 "mcp.old_structure",
51 "检测到旧版 MCP 结构,请重启应用完成迁移",
52 "Old MCP structure detected, please restart app to complete migration",
53 ))
54 }
55
56 pub fn get_live_drift(state: &AppState, app: AppType) -> Result<McpLiveDriftReport, AppError> {
57 let live_servers = match Self::read_live_mcp_servers(&app) {
58 Ok(servers) => servers,
59 Err(err) => {
60 return Ok(McpLiveDriftReport {
61 app: app.clone(),
62 entries: vec![McpLiveDriftEntry {
63 app,
64 id: String::new(),
65 kind: McpLiveDriftKind::LiveInvalid,
66 db_spec: None,
67 live_spec: None,
68 message: Some(err.to_string()),
69 }],
70 });
71 }
72 };
73
74 let db_servers = Self::get_all_servers(state)?;
75 let mut ids = BTreeSet::new();
76
77 for id in live_servers.keys() {
78 ids.insert(id.clone());
79 }
80
81 for (id, server) in &db_servers {
82 if server.apps.is_enabled_for(&app) || live_servers.contains_key(id) {
83 ids.insert(id.clone());
84 }
85 }
86
87 let mut entries = Vec::new();
88 for id in ids {
89 let db_server = db_servers
90 .get(&id)
91 .filter(|server| server.apps.is_enabled_for(&app));
92 let db_spec = db_server.map(|server| server.server.clone());
93 let live_spec = live_servers.get(&id).cloned();
94
95 let kind = match (&db_spec, &live_spec) {
96 (Some(db), Some(live)) => {
97 if normalize_json_value(db) == normalize_json_value(live) {
98 McpLiveDriftKind::InSync
99 } else {
100 McpLiveDriftKind::Changed
101 }
102 }
103 (Some(_), None) => McpLiveDriftKind::DbOnly,
104 (None, Some(_)) => McpLiveDriftKind::LiveOnly,
105 (None, None) => continue,
106 };
107
108 entries.push(McpLiveDriftEntry {
109 app: app.clone(),
110 id,
111 kind,
112 db_spec,
113 live_spec,
114 message: None,
115 });
116 }
117
118 Ok(McpLiveDriftReport { app, entries })
119 }
120
121 fn read_live_mcp_servers(app: &AppType) -> Result<HashMap<String, Value>, AppError> {
122 match app {
123 AppType::Codex => mcp::read_codex_live_mcp_servers_map(),
124 _ => Ok(HashMap::new()),
125 }
126 }
127
128 pub fn import_live_server(state: &AppState, app: AppType, id: &str) -> Result<(), AppError> {
129 let live_servers = Self::read_live_mcp_servers(&app)?;
130 let live_spec = live_servers.get(id).cloned().ok_or_else(|| {
131 AppError::McpValidation(format!(
132 "{} live MCP server '{}' not found",
133 app.as_str(),
134 id
135 ))
136 })?;
137
138 {
139 let mut cfg = state.config.write()?;
140 let servers = cfg.mcp.servers.get_or_insert_with(HashMap::new);
141
142 if let Some(existing) = servers.get_mut(id) {
143 existing.server = live_spec;
144 existing.apps.set_enabled_for(&app, true);
145 } else {
146 let mut apps = McpApps::default();
147 apps.set_enabled_for(&app, true);
148 servers.insert(
149 id.to_string(),
150 McpServer {
151 id: id.to_string(),
152 name: id.to_string(),
153 server: live_spec,
154 apps,
155 description: None,
156 homepage: None,
157 docs: None,
158 tags: Vec::new(),
159 },
160 );
161 }
162 }
163
164 state.save()?;
165 Ok(())
166 }
167
168 pub fn push_db_server_to_live(
169 state: &AppState,
170 app: AppType,
171 id: &str,
172 ) -> Result<(), AppError> {
173 let server = {
174 let cfg = state.config.read()?;
175 cfg.mcp
176 .servers
177 .as_ref()
178 .and_then(|servers| servers.get(id))
179 .cloned()
180 }
181 .ok_or_else(|| AppError::McpValidation(format!("MCP server '{id}' not found")))?;
182
183 if !server.apps.is_enabled_for(&app) {
184 return Err(AppError::McpValidation(format!(
185 "MCP server '{}' is not enabled for {}",
186 id,
187 app.as_str()
188 )));
189 }
190
191 Self::sync_server_to_app(state, &server, &app)
192 }
193
194 pub fn upsert_server(state: &AppState, server: McpServer) -> Result<(), AppError> {
196 let (server_id, apps_to_remove) = {
197 let mut cfg = state.config.write()?;
198
199 let servers = cfg.mcp.servers.get_or_insert_with(HashMap::new);
200 let server_id = server.id.clone();
201
202 let apps_to_remove = servers
203 .get(&server_id)
204 .map(|existing| {
205 existing
206 .apps
207 .enabled_apps()
208 .into_iter()
209 .filter(|app| !server.apps.is_enabled_for(app))
210 .collect::<Vec<_>>()
211 })
212 .unwrap_or_default();
213
214 servers.insert(server_id.clone(), server.clone());
216
217 (server_id, apps_to_remove)
218 };
219
220 state.save()?;
221
222 for app in apps_to_remove {
224 Self::remove_server_from_app(state, &server_id, &app)?;
225 }
226
227 Self::sync_server_to_apps(state, &server)?;
229
230 Ok(())
231 }
232
233 pub fn delete_server(state: &AppState, id: &str) -> Result<bool, AppError> {
235 let server = {
236 let mut cfg = state.config.write()?;
237
238 if let Some(servers) = &mut cfg.mcp.servers {
239 servers.remove(id)
240 } else {
241 None
242 }
243 };
244
245 if let Some(server) = server {
246 state.save()?;
247
248 Self::remove_server_from_all_apps(state, id, &server)?;
250 Ok(true)
251 } else {
252 Ok(false)
253 }
254 }
255
256 pub fn toggle_app(
258 state: &AppState,
259 server_id: &str,
260 app: AppType,
261 enabled: bool,
262 ) -> Result<(), AppError> {
263 let server = {
264 let mut cfg = state.config.write()?;
265
266 if let Some(servers) = &mut cfg.mcp.servers {
267 if let Some(server) = servers.get_mut(server_id) {
268 server.apps.set_enabled_for(&app, enabled);
269 Some(server.clone())
270 } else {
271 None
272 }
273 } else {
274 None
275 }
276 };
277
278 if let Some(server) = server {
279 state.save()?;
280
281 if enabled {
283 Self::sync_server_to_app(state, &server, &app)?;
284 } else {
285 Self::remove_server_from_app(state, server_id, &app)?;
286 }
287 }
288
289 Ok(())
290 }
291
292 fn sync_server_to_apps(state: &AppState, server: &McpServer) -> Result<(), AppError> {
294 let cfg = state.config.read()?;
295
296 for app in server.apps.enabled_apps() {
297 Self::sync_server_to_app_internal(&cfg, server, &app)?;
298 }
299
300 Ok(())
301 }
302
303 fn sync_server_to_app(
305 state: &AppState,
306 server: &McpServer,
307 app: &AppType,
308 ) -> Result<(), AppError> {
309 let cfg = state.config.read()?;
310 Self::sync_server_to_app_internal(&cfg, server, app)
311 }
312
313 fn sync_server_to_app_internal(
314 cfg: &MultiAppConfig,
315 server: &McpServer,
316 app: &AppType,
317 ) -> Result<(), AppError> {
318 match app {
319 AppType::Claude => {
320 mcp::sync_single_server_to_claude(cfg, &server.id, &server.server)?;
321 }
322 AppType::Codex => {
323 mcp::sync_single_server_to_codex(cfg, &server.id, &server.server)?;
324 }
325 AppType::Gemini => {
326 mcp::sync_single_server_to_gemini(cfg, &server.id, &server.server)?;
327 }
328 AppType::OpenCode => {
329 mcp::sync_single_server_to_opencode(cfg, &server.id, &server.server)?;
330 }
331 AppType::OpenClaw => {
332 mcp::sync_single_server_to_openclaw(cfg, &server.id, &server.server)?;
333 }
334 AppType::Hermes => {
335 mcp::sync_single_server_to_hermes(cfg, &server.id, &server.server)?;
336 }
337 }
338 Ok(())
339 }
340
341 fn remove_server_from_all_apps(
343 state: &AppState,
344 id: &str,
345 server: &McpServer,
346 ) -> Result<(), AppError> {
347 for app in server.apps.enabled_apps() {
349 Self::remove_server_from_app(state, id, &app)?;
350 }
351 Ok(())
352 }
353
354 fn remove_server_from_app(_state: &AppState, id: &str, app: &AppType) -> Result<(), AppError> {
355 match app {
356 AppType::Claude => mcp::remove_server_from_claude(id)?,
357 AppType::Codex => mcp::remove_server_from_codex(id)?,
358 AppType::Gemini => mcp::remove_server_from_gemini(id)?,
359 AppType::OpenCode => mcp::remove_server_from_opencode(id)?,
360 AppType::OpenClaw => mcp::remove_server_from_openclaw(id)?,
361 AppType::Hermes => mcp::remove_server_from_hermes(id)?,
362 }
363 Ok(())
364 }
365
366 pub fn sync_all_enabled(state: &AppState) -> Result<(), AppError> {
368 Self::sync_all_enabled_except(state, &[])
369 }
370
371 pub fn sync_all_enabled_except(
373 state: &AppState,
374 excluded_apps: &[AppType],
375 ) -> Result<(), AppError> {
376 let servers = Self::get_all_servers(state)?;
377
378 for app in AppType::all() {
379 if excluded_apps.contains(&app) {
380 continue;
381 }
382
383 for server in servers.values() {
384 if server.apps.is_enabled_for(&app) {
385 Self::sync_server_to_app(state, server, &app)?;
386 } else {
387 Self::remove_server_from_app(state, &server.id, &app)?;
388 }
389 }
390 }
391
392 Ok(())
393 }
394
395 #[deprecated(since = "3.7.0", note = "Use get_all_servers instead")]
401 pub fn get_servers(
402 state: &AppState,
403 app: AppType,
404 ) -> Result<HashMap<String, serde_json::Value>, AppError> {
405 let all_servers = Self::get_all_servers(state)?;
406 let mut result = HashMap::new();
407
408 for (id, server) in all_servers {
409 if server.apps.is_enabled_for(&app) {
410 result.insert(id, server.server);
411 }
412 }
413
414 Ok(result)
415 }
416
417 #[deprecated(since = "3.7.0", note = "Use toggle_app instead")]
419 pub fn set_enabled(
420 state: &AppState,
421 app: AppType,
422 id: &str,
423 enabled: bool,
424 ) -> Result<bool, AppError> {
425 Self::toggle_app(state, id, app, enabled)?;
426 Ok(true)
427 }
428
429 #[deprecated(since = "3.7.0", note = "Use sync_all_enabled instead")]
431 pub fn sync_enabled(state: &AppState, app: AppType) -> Result<(), AppError> {
432 let servers = Self::get_all_servers(state)?;
433
434 for server in servers.values() {
435 if server.apps.is_enabled_for(&app) {
436 Self::sync_server_to_app(state, server, &app)?;
437 }
438 }
439
440 Ok(())
441 }
442
443 pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
445 let mut cfg = state.config.write()?;
446 let count = mcp::import_from_claude(&mut cfg)?;
447 drop(cfg);
448 state.save()?;
449 Ok(count)
450 }
451
452 pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
454 let mut cfg = state.config.write()?;
455 let count = mcp::import_from_codex(&mut cfg)?;
456 drop(cfg);
457 state.save()?;
458 Ok(count)
459 }
460
461 pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
463 let mut cfg = state.config.write()?;
464 let count = mcp::import_from_gemini(&mut cfg)?;
465 drop(cfg);
466 state.save()?;
467 Ok(count)
468 }
469
470 pub fn import_from_opencode(state: &AppState) -> Result<usize, AppError> {
472 let mut cfg = state.config.write()?;
473 let count = mcp::import_from_opencode(&mut cfg)?;
474 drop(cfg);
475 state.save()?;
476 Ok(count)
477 }
478
479 pub fn import_from_openclaw(state: &AppState) -> Result<usize, AppError> {
481 let mut cfg = state.config.write()?;
482 let count = mcp::import_from_openclaw(&mut cfg)?;
483 drop(cfg);
484 state.save()?;
485 Ok(count)
486 }
487
488 pub fn import_from_hermes(state: &AppState) -> Result<usize, AppError> {
490 let mut cfg = state.config.write()?;
491 let count = mcp::import_from_hermes(&mut cfg)?;
492 drop(cfg);
493 state.save()?;
494 Ok(count)
495 }
496}
497
498fn normalize_json_value(value: &Value) -> Value {
499 match value {
500 Value::Array(items) => Value::Array(items.iter().map(normalize_json_value).collect()),
501 Value::Object(map) => {
502 let mut normalized = serde_json::Map::new();
503 let mut keys = map.keys().collect::<Vec<_>>();
504 keys.sort();
505 for key in keys {
506 if let Some(value) = map.get(key) {
507 normalized.insert(key.clone(), normalize_json_value(value));
508 }
509 }
510 Value::Object(normalized)
511 }
512 _ => value.clone(),
513 }
514}