From af91b89cff13fcafd29d52be71ed03b82705bbf8 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 6 Jul 2026 09:57:17 -0600 Subject: [PATCH] feat: merge MCP config when installing bundled mcp config --- src/config/mod.rs | 7 ++++--- src/config/request_context.rs | 19 ++++++++++++------ src/function/mod.rs | 38 ++++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 4d9eb06..3a0e05a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -445,9 +445,10 @@ fn confirm_asset_overwrite(category: AssetCategory, label: &str, target: &Path) } let body = match category { AssetCategory::McpConfig => format!( - "This replaces your MCP server configuration at {} with this \ - build's bundled template. Your configured MCP servers (and any \ - custom secret references they contain) will be lost.", + "This merges the bundled MCP server template into your configuration \ + at {}. New servers from the bundled template will be added; any \ + MCP servers you have already configured (including custom secret \ + references) are left untouched.", target.display() ), _ => format!( diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 320e644..187cef7 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -5175,19 +5175,26 @@ mod tests { #[test] #[serial] - fn install_mcp_config_overwrites_existing() { + fn install_mcp_config_merges_existing() { let _guard = TestConfigDirGuard::new(); Functions::install_mcp_config().unwrap(); let mcp = paths::mcp_config_file(); assert!(mcp.exists(), "install_mcp_config should create mcp.json"); - write(&mcp, "USER_MCP_CONFIG").unwrap(); + let custom_json = + r#"{"mcpServers":{"my-custom-server":{"type":"stdio","command":"custom-cmd"}}}"#; + write(&mcp, custom_json).unwrap(); Functions::install_mcp_config().unwrap(); - assert_ne!( - read_to_string(&mcp).unwrap(), - "USER_MCP_CONFIG", - "install_mcp_config must overwrite the existing mcp.json" + + let result = read_to_string(&mcp).unwrap(); + assert!( + result.contains("my-custom-server"), + "install_mcp_config must preserve user-added MCP servers" + ); + assert!( + result.contains("github"), + "install_mcp_config must add new bundled servers" ); } } diff --git a/src/function/mod.rs b/src/function/mod.rs index 4fe1ce5..3d79356 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -14,7 +14,7 @@ use crate::config::ensure_parent_exists; use crate::config::paths; use crate::mcp::{ MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX, - MCP_SEARCH_META_FUNCTION_NAME_PREFIX, + MCP_SEARCH_META_FUNCTION_NAME_PREFIX, McpServersConfig, }; use crate::parsers::{bash, python, typescript}; use anyhow::{Context, Result, anyhow, bail}; @@ -266,14 +266,42 @@ impl Functions { let file_path = paths::mcp_config_file(); let embedded = FunctionAssets::get("mcp.json") .ok_or_else(|| anyhow!("Failed to load embedded mcp.json"))?; - let content = unsafe { std::str::from_utf8_unchecked(&embedded.data) }; + let bundled_content = unsafe { std::str::from_utf8_unchecked(&embedded.data) }; + let bundled: McpServersConfig = + serde_json::from_str(bundled_content).context("failed to parse embedded mcp.json")?; ensure_parent_exists(&file_path)?; - info!("Reinstalling MCP config file: {}", file_path.display()); + let mut merged = if file_path.exists() { + let existing = + fs::read_to_string(&file_path).context("failed to read existing mcp.json")?; + serde_json::from_str::(&existing) + .context("failed to parse existing mcp.json")? + } else { + McpServersConfig { + mcp_servers: IndexMap::new(), + } + }; - let mut config_file = File::create(&file_path)?; - config_file.write_all(content.as_bytes())?; + let mut added = Vec::new(); + for (name, server) in bundled.mcp_servers { + if !merged.mcp_servers.contains_key(&name) { + merged.mcp_servers.insert(name.clone(), server); + added.push(name); + } + } + + info!("Merging bundled MCP config into: {}", file_path.display()); + + let serialized = + serde_json::to_string_pretty(&merged).context("failed to serialize merged mcp.json")?; + let tmp = file_path.with_extension("json.tmp"); + fs::write(&tmp, &serialized).context("failed to write temporary mcp.json")?; + fs::rename(&tmp, &file_path).context("failed to finalize mcp.json")?; + + if !added.is_empty() { + println!(" + new MCP servers: {}", added.join(", ")); + } Ok(()) }