Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -368,50 +369,6 @@ fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), String> {
// MCP import
// ============================================================

fn load_external_mcp_config(path: &Path) -> Result<McpConfigFile, String> {
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>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DetectedItem> {
let mut out = Vec::new();
Expand Down Expand Up @@ -70,50 +71,6 @@ pub(super) fn detect_mcp_servers(repo_path: Option<&Path>) -> Vec<DetectedItem>
out
}

fn load_external_mcp_config(path: &Path) -> Result<McpConfigFile, String> {
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<McpConfigFile, String> {
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
pub mod commands;
pub mod detect;
mod mcp_config;
pub mod types;

// Wildcard re-export needed: `#[tauri::command]` generates hidden
Expand Down
Loading