diff --git a/src-tauri/crates/agent-core/src/specialization/external_import/commands.rs b/src-tauri/crates/agent-core/src/specialization/external_import/commands.rs index 951746be01..8a8b505c8c 100644 --- a/src-tauri/crates/agent-core/src/specialization/external_import/commands.rs +++ b/src-tauri/crates/agent-core/src/specialization/external_import/commands.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use super::detect::detect_all; +use super::mcp_config::load_external_mcp_config; use super::types::{ frontmatter_declares_readonly, readonly_excluded_tool_names, DetectedItem, ImportItemReport, ImportReport, ImportSelection, ImportStatus, ItemKind, SourceScope, @@ -368,50 +369,6 @@ fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), String> { // MCP import // ============================================================ -fn load_external_mcp_config(path: &Path) -> Result { - let raw = std::fs::read_to_string(path) - .map_err(|err| format!("Failed to read MCP config {}: {}", path.display(), err))?; - let mut value: serde_json::Value = serde_json::from_str(&raw) - .map_err(|err| format!("Failed to parse MCP config {}: {}", path.display(), err))?; - let Some(servers) = value - .get_mut("mcpServers") - .and_then(|entry| entry.as_object_mut()) - else { - return Ok(McpConfigFile::default()); - }; - - for server in servers.values_mut() { - let Some(server_obj) = server.as_object_mut() else { - continue; - }; - if !server_obj.contains_key("type") { - let inferred = if server_obj.contains_key("url") { - "streamableHttp" - } else { - "stdio" - }; - server_obj.insert( - "type".to_string(), - serde_json::Value::String(inferred.to_string()), - ); - } - if server_obj.get("type").and_then(|entry| entry.as_str()) == Some("http") { - server_obj.insert( - "type".to_string(), - serde_json::Value::String("streamableHttp".to_string()), - ); - } - } - - serde_json::from_value(value).map_err(|err| { - format!( - "Failed to parse MCP server entries {}: {}", - path.display(), - err - ) - }) -} - fn apply_mcp_import( selection: &ImportSelection, target_repo_path: Option<&Path>, diff --git a/src-tauri/crates/agent-core/src/specialization/external_import/detect/mcp.rs b/src-tauri/crates/agent-core/src/specialization/external_import/detect/mcp.rs index af036377f4..b0e187f1e6 100644 --- a/src-tauri/crates/agent-core/src/specialization/external_import/detect/mcp.rs +++ b/src-tauri/crates/agent-core/src/specialization/external_import/detect/mcp.rs @@ -6,9 +6,10 @@ use std::path::Path; +use super::super::mcp_config::load_external_mcp_config; use super::super::types::{DetectedItem, ItemKind, ItemPreview, SourceAgent, SourceScope}; use super::helpers::{home_dir, orgii_mcp_exists, path_has_denied_ancestor, MAX_ITEMS_PER_BATCH}; -use crate::specialization::mcp::config::{McpConfigFile, McpTransportType}; +use crate::specialization::mcp::config::McpTransportType; pub(super) fn detect_mcp_servers(repo_path: Option<&Path>) -> Vec { let mut out = Vec::new(); @@ -70,50 +71,6 @@ pub(super) fn detect_mcp_servers(repo_path: Option<&Path>) -> Vec out } -fn load_external_mcp_config(path: &Path) -> Result { - let raw = std::fs::read_to_string(path) - .map_err(|err| format!("Failed to read MCP config {}: {}", path.display(), err))?; - let mut value: serde_json::Value = serde_json::from_str(&raw) - .map_err(|err| format!("Failed to parse MCP config {}: {}", path.display(), err))?; - let Some(servers) = value - .get_mut("mcpServers") - .and_then(|entry| entry.as_object_mut()) - else { - return Ok(McpConfigFile::default()); - }; - - for server in servers.values_mut() { - let Some(server_obj) = server.as_object_mut() else { - continue; - }; - if !server_obj.contains_key("type") { - let inferred = if server_obj.contains_key("url") { - "streamableHttp" - } else { - "stdio" - }; - server_obj.insert( - "type".to_string(), - serde_json::Value::String(inferred.to_string()), - ); - } - if server_obj.get("type").and_then(|entry| entry.as_str()) == Some("http") { - server_obj.insert( - "type".to_string(), - serde_json::Value::String("streamableHttp".to_string()), - ); - } - } - - serde_json::from_value(value).map_err(|err| { - format!( - "Failed to parse MCP server entries {}: {}", - path.display(), - err - ) - }) -} - fn scan_mcp_config_file( path: &Path, source_agent: SourceAgent, diff --git a/src-tauri/crates/agent-core/src/specialization/external_import/mcp_config.rs b/src-tauri/crates/agent-core/src/specialization/external_import/mcp_config.rs new file mode 100644 index 0000000000..feb08b7542 --- /dev/null +++ b/src-tauri/crates/agent-core/src/specialization/external_import/mcp_config.rs @@ -0,0 +1,105 @@ +use std::path::Path; + +use crate::specialization::mcp::config::McpConfigFile; + +/// Load an MCP config authored by another agent and normalize the transport +/// spellings that ORGII accepts before deserializing it into the canonical +/// config model. +pub(super) fn load_external_mcp_config(path: &Path) -> Result { + let raw = std::fs::read_to_string(path) + .map_err(|err| format!("Failed to read MCP config {}: {}", path.display(), err))?; + let mut value: serde_json::Value = serde_json::from_str(&raw) + .map_err(|err| format!("Failed to parse MCP config {}: {}", path.display(), err))?; + let Some(servers) = value + .get_mut("mcpServers") + .and_then(|entry| entry.as_object_mut()) + else { + return Ok(McpConfigFile::default()); + }; + + for server in servers.values_mut() { + let Some(server_obj) = server.as_object_mut() else { + continue; + }; + if !server_obj.contains_key("type") { + let inferred = if server_obj.contains_key("url") { + "streamableHttp" + } else { + "stdio" + }; + server_obj.insert( + "type".to_string(), + serde_json::Value::String(inferred.to_string()), + ); + } + if server_obj.get("type").and_then(|entry| entry.as_str()) == Some("http") { + server_obj.insert( + "type".to_string(), + serde_json::Value::String("streamableHttp".to_string()), + ); + } + } + + serde_json::from_value(value).map_err(|err| { + format!( + "Failed to parse MCP server entries {}: {}", + path.display(), + err + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::specialization::mcp::config::McpTransportType; + use tempfile::TempDir; + + #[test] + fn normalizes_external_transport_variants() { + let temp = TempDir::new().expect("create temp dir"); + let path = temp.path().join("mcp.json"); + std::fs::write( + &path, + r#"{ + "mcpServers": { + "implicit-stdio": { "command": "server" }, + "implicit-http": { "url": "https://example.com/mcp" }, + "legacy-http": { "type": "http", "url": "https://example.com/legacy" }, + "explicit-sse": { "type": "sse", "url": "https://example.com/sse" } + } + }"#, + ) + .expect("write config"); + + let config = load_external_mcp_config(&path).expect("load config"); + + assert_eq!( + config.mcp_servers["implicit-stdio"].transport_type, + McpTransportType::Stdio + ); + assert_eq!( + config.mcp_servers["implicit-http"].transport_type, + McpTransportType::StreamableHttp + ); + assert_eq!( + config.mcp_servers["legacy-http"].transport_type, + McpTransportType::StreamableHttp + ); + assert_eq!( + config.mcp_servers["explicit-sse"].transport_type, + McpTransportType::Sse + ); + } + + #[test] + fn treats_missing_mcp_servers_as_empty() { + let temp = TempDir::new().expect("create temp dir"); + let path = temp.path().join("mcp.json"); + std::fs::write(&path, r#"{ "other": true }"#).expect("write config"); + + let config = load_external_mcp_config(&path).expect("load config"); + + assert!(config.mcp_servers.is_empty()); + } +} diff --git a/src-tauri/crates/agent-core/src/specialization/external_import/mod.rs b/src-tauri/crates/agent-core/src/specialization/external_import/mod.rs index 9f847a9dd4..63d62b149d 100644 --- a/src-tauri/crates/agent-core/src/specialization/external_import/mod.rs +++ b/src-tauri/crates/agent-core/src/specialization/external_import/mod.rs @@ -20,6 +20,7 @@ pub mod commands; pub mod detect; +mod mcp_config; pub mod types; // Wildcard re-export needed: `#[tauri::command]` generates hidden