feat: merge MCP config when installing bundled mcp config
This commit is contained in:
+4
-3
@@ -445,9 +445,10 @@ fn confirm_asset_overwrite(category: AssetCategory, label: &str, target: &Path)
|
|||||||
}
|
}
|
||||||
let body = match category {
|
let body = match category {
|
||||||
AssetCategory::McpConfig => format!(
|
AssetCategory::McpConfig => format!(
|
||||||
"This replaces your MCP server configuration at {} with this \
|
"This merges the bundled MCP server template into your configuration \
|
||||||
build's bundled template. Your configured MCP servers (and any \
|
at {}. New servers from the bundled template will be added; any \
|
||||||
custom secret references they contain) will be lost.",
|
MCP servers you have already configured (including custom secret \
|
||||||
|
references) are left untouched.",
|
||||||
target.display()
|
target.display()
|
||||||
),
|
),
|
||||||
_ => format!(
|
_ => format!(
|
||||||
|
|||||||
@@ -5175,19 +5175,26 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn install_mcp_config_overwrites_existing() {
|
fn install_mcp_config_merges_existing() {
|
||||||
let _guard = TestConfigDirGuard::new();
|
let _guard = TestConfigDirGuard::new();
|
||||||
|
|
||||||
Functions::install_mcp_config().unwrap();
|
Functions::install_mcp_config().unwrap();
|
||||||
let mcp = paths::mcp_config_file();
|
let mcp = paths::mcp_config_file();
|
||||||
assert!(mcp.exists(), "install_mcp_config should create mcp.json");
|
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();
|
Functions::install_mcp_config().unwrap();
|
||||||
assert_ne!(
|
|
||||||
read_to_string(&mcp).unwrap(),
|
let result = read_to_string(&mcp).unwrap();
|
||||||
"USER_MCP_CONFIG",
|
assert!(
|
||||||
"install_mcp_config must overwrite the existing mcp.json"
|
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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-5
@@ -14,7 +14,7 @@ use crate::config::ensure_parent_exists;
|
|||||||
use crate::config::paths;
|
use crate::config::paths;
|
||||||
use crate::mcp::{
|
use crate::mcp::{
|
||||||
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
|
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 crate::parsers::{bash, python, typescript};
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
@@ -266,14 +266,42 @@ impl Functions {
|
|||||||
let file_path = paths::mcp_config_file();
|
let file_path = paths::mcp_config_file();
|
||||||
let embedded = FunctionAssets::get("mcp.json")
|
let embedded = FunctionAssets::get("mcp.json")
|
||||||
.ok_or_else(|| anyhow!("Failed to load embedded 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)?;
|
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::<McpServersConfig>(&existing)
|
||||||
|
.context("failed to parse existing mcp.json")?
|
||||||
|
} else {
|
||||||
|
McpServersConfig {
|
||||||
|
mcp_servers: IndexMap::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut config_file = File::create(&file_path)?;
|
let mut added = Vec::new();
|
||||||
config_file.write_all(content.as_bytes())?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user