feat: make MCP config file follow Claude directory override
When users set a custom Claude configuration directory, the MCP config file (.claude.json) is now placed alongside the overridden directory instead of the default ~/.claude.json location. Changes: - Add path derivation logic to generate MCP path from override directory (e.g., /custom/.claude → /custom/.claude.json) - Implement automatic migration from legacy path on first access - Add comprehensive unit tests covering 4 edge cases - Update UI descriptions to clarify MCP file placement - Fix documentation: correct MCP config path from ~/.claude/mcp.json to ~/.claude.json Technical details: - New function: derive_mcp_path_from_override() extracts directory name and creates sibling .json file - Migration copies ~/.claude.json to new location if override is set - All MCP operations (read/write/sync) now use the derived path via user_config_path() unified entry point Breaking changes: None (backward compatible with default behavior)
This commit is contained in:
@@ -4,7 +4,7 @@ use std::env;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::config::atomic_write;
|
use crate::config::{atomic_write, get_claude_mcp_path, get_default_claude_mcp_path};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -15,10 +15,49 @@ pub struct McpStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn user_config_path() -> PathBuf {
|
fn user_config_path() -> PathBuf {
|
||||||
// 用户级 MCP 配置文件:~/.claude.json
|
ensure_mcp_override_migrated();
|
||||||
dirs::home_dir()
|
get_claude_mcp_path()
|
||||||
.expect("无法获取用户主目录")
|
}
|
||||||
.join(".claude.json")
|
|
||||||
|
fn ensure_mcp_override_migrated() {
|
||||||
|
if crate::settings::get_claude_override_dir().is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_path = get_claude_mcp_path();
|
||||||
|
if new_path.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let legacy_path = get_default_claude_mcp_path();
|
||||||
|
if !legacy_path.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = new_path.parent() {
|
||||||
|
if let Err(err) = fs::create_dir_all(parent) {
|
||||||
|
log::warn!("创建 MCP 目录失败: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::copy(&legacy_path, &new_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(
|
||||||
|
"已根据覆盖目录复制 MCP 配置: {} -> {}",
|
||||||
|
legacy_path.display(),
|
||||||
|
new_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"复制 MCP 配置失败: {} -> {}: {}",
|
||||||
|
legacy_path.display(),
|
||||||
|
new_path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_json_value(path: &Path) -> Result<Value, String> {
|
fn read_json_value(path: &Path) -> Result<Value, String> {
|
||||||
|
|||||||
@@ -15,6 +15,36 @@ pub fn get_claude_config_dir() -> PathBuf {
|
|||||||
.join(".claude")
|
.join(".claude")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 默认 Claude MCP 配置文件路径 (~/.claude.json)
|
||||||
|
pub fn get_default_claude_mcp_path() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.expect("无法获取用户主目录")
|
||||||
|
.join(".claude.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_mcp_path_from_override(dir: &Path) -> Option<PathBuf> {
|
||||||
|
let file_name = dir
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().to_string())?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if file_name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let parent = dir.parent().unwrap_or_else(|| Path::new(""));
|
||||||
|
Some(parent.join(format!("{}.json", file_name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 Claude MCP 配置文件路径,若设置了目录覆盖则与覆盖目录同级
|
||||||
|
pub fn get_claude_mcp_path() -> PathBuf {
|
||||||
|
if let Some(custom_dir) = crate::settings::get_claude_override_dir() {
|
||||||
|
if let Some(path) = derive_mcp_path_from_override(&custom_dir) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get_default_claude_mcp_path()
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取 Claude Code 主配置文件路径
|
/// 获取 Claude Code 主配置文件路径
|
||||||
pub fn get_claude_settings_path() -> PathBuf {
|
pub fn get_claude_settings_path() -> PathBuf {
|
||||||
let dir = get_claude_config_dir();
|
let dir = get_claude_config_dir();
|
||||||
@@ -219,6 +249,41 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_mcp_path_from_override_preserves_folder_name() {
|
||||||
|
let override_dir = PathBuf::from("/tmp/profile/.claude");
|
||||||
|
let derived = derive_mcp_path_from_override(&override_dir)
|
||||||
|
.expect("should derive path for nested dir");
|
||||||
|
assert_eq!(derived, PathBuf::from("/tmp/profile/.claude.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_mcp_path_from_override_handles_non_hidden_folder() {
|
||||||
|
let override_dir = PathBuf::from("/data/claude-config");
|
||||||
|
let derived = derive_mcp_path_from_override(&override_dir)
|
||||||
|
.expect("should derive path for standard dir");
|
||||||
|
assert_eq!(derived, PathBuf::from("/data/claude-config.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_mcp_path_from_override_supports_relative_rootless_dir() {
|
||||||
|
let override_dir = PathBuf::from("claude");
|
||||||
|
let derived = derive_mcp_path_from_override(&override_dir)
|
||||||
|
.expect("should derive path for single segment");
|
||||||
|
assert_eq!(derived, PathBuf::from("claude.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_mcp_path_from_root_like_dir_returns_none() {
|
||||||
|
let override_dir = PathBuf::from("/");
|
||||||
|
assert!(derive_mcp_path_from_override(&override_dir).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 复制文件
|
/// 复制文件
|
||||||
pub fn copy_file(from: &Path, to: &Path) -> Result<(), String> {
|
pub fn copy_file(from: &Path, to: &Path) -> Result<(), String> {
|
||||||
fs::copy(from, to).map_err(|e| format!("复制文件失败: {}", e))?;
|
fs::copy(from, to).map_err(|e| format!("复制文件失败: {}", e))?;
|
||||||
|
|||||||
@@ -143,7 +143,7 @@
|
|||||||
"appConfigDirDescription": "Customize the storage location for CC-Switch configuration files (config.json, etc.)",
|
"appConfigDirDescription": "Customize the storage location for CC-Switch configuration files (config.json, etc.)",
|
||||||
"browsePlaceholderApp": "e.g., C:\\Users\\Administrator\\.cc-switch",
|
"browsePlaceholderApp": "e.g., C:\\Users\\Administrator\\.cc-switch",
|
||||||
"claudeConfigDir": "Claude Code Configuration Directory",
|
"claudeConfigDir": "Claude Code Configuration Directory",
|
||||||
"claudeConfigDirDescription": "Override Claude configuration directory (settings.json).",
|
"claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.",
|
||||||
"codexConfigDir": "Codex Configuration Directory",
|
"codexConfigDir": "Codex Configuration Directory",
|
||||||
"codexConfigDirDescription": "Override Codex configuration directory.",
|
"codexConfigDirDescription": "Override Codex configuration directory.",
|
||||||
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
|
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
|
||||||
|
|||||||
@@ -143,7 +143,7 @@
|
|||||||
"appConfigDirDescription": "自定义 CC-Switch 的配置存储位置(config.json 等文件)",
|
"appConfigDirDescription": "自定义 CC-Switch 的配置存储位置(config.json 等文件)",
|
||||||
"browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch",
|
"browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch",
|
||||||
"claudeConfigDir": "Claude Code 配置目录",
|
"claudeConfigDir": "Claude Code 配置目录",
|
||||||
"claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json)。",
|
"claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。",
|
||||||
"codexConfigDir": "Codex 配置目录",
|
"codexConfigDir": "Codex 配置目录",
|
||||||
"codexConfigDirDescription": "覆盖 Codex 配置目录。",
|
"codexConfigDirDescription": "覆盖 Codex 配置目录。",
|
||||||
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
|
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
|
||||||
|
|||||||
Reference in New Issue
Block a user