From 6104402bfb37caa6c2e4cfae6493ed72898361db Mon Sep 17 00:00:00 2001
From: lucaronin
Date: Fri, 24 Apr 2026 13:22:33 +0200
Subject: [PATCH] fix: write Claude Code MCP config to .claude.json
---
docs/ARCHITECTURE.md | 4 +-
src-tauri/src/mcp.rs | 152 +++++++++++++++++--------
src/components/McpSetupDialog.test.tsx | 3 +
src/components/McpSetupDialog.tsx | 3 +-
4 files changed, 113 insertions(+), 49 deletions(-)
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 288bb1dc..52da2fa4 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -310,7 +310,7 @@ The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistan
### Explicit External Tool Setup
Tolaria can register itself as an MCP server in:
-- `~/.claude/mcp.json` (Claude Code)
+- `~/.claude.json` and `~/.claude/mcp.json` (Claude Code compatibility across current CLI and legacy MCP-file setups)
- `~/.cursor/mcp.json` (Cursor)
That setup is user-initiated through the status bar / command palette flow, not a startup side effect. Registration is non-destructive (additive, preserves other servers), uses `upsert` semantics, and can be reversed by removing Tolaria's entry again. The `useMcpStatus` hook tracks whether the active vault is explicitly connected (`checking | installed | not_installed`).
@@ -332,7 +332,7 @@ flowchart TD
end
TAURI["Tauri (mcp.rs)"] -->|"spawn on startup"| MCP
- UI["Status bar / Command Palette"] -->|"explicit setup or disconnect"| CFG["~/.claude/mcp.json\n~/.cursor/mcp.json"]
+ UI["Status bar / Command Palette"] -->|"explicit setup or disconnect"| CFG["~/.claude.json\n~/.claude/mcp.json\n~/.cursor/mcp.json"]
```
### WebSocket Bridge
diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs
index 43e8369c..cd2c446e 100644
--- a/src-tauri/src/mcp.rs
+++ b/src-tauri/src/mcp.rs
@@ -115,13 +115,17 @@ pub fn spawn_ws_bridge(vault_path: &str) -> Result {
}
fn mcp_config_paths() -> Vec {
- [
- dirs::home_dir().map(|home| home.join(".claude").join("mcp.json")),
- dirs::home_dir().map(|home| home.join(".cursor").join("mcp.json")),
+ dirs::home_dir()
+ .map(|home| mcp_config_paths_for_home(&home))
+ .unwrap_or_default()
+}
+
+fn mcp_config_paths_for_home(home: &Path) -> Vec {
+ vec![
+ home.join(".claude.json"),
+ home.join(".claude").join("mcp.json"),
+ home.join(".cursor").join("mcp.json"),
]
- .into_iter()
- .flatten()
- .collect()
}
fn read_registered_mcp_entry(config_path: &Path) -> Option {
@@ -321,6 +325,32 @@ mod tests {
serde_json::from_str(&raw).unwrap()
}
+ fn temp_config_path(file_name: &str) -> (tempfile::TempDir, PathBuf) {
+ let tmp = tempfile::tempdir().unwrap();
+ let config_path = tmp.path().join(file_name);
+ (tmp, config_path)
+ }
+
+ fn write_config_json(config_path: &Path, config: serde_json::Value) {
+ std::fs::write(config_path, serde_json::to_string(&config).unwrap()).unwrap();
+ }
+
+ fn managed_server(index_js: &str, vault_path: &str) -> serde_json::Value {
+ serde_json::json!({
+ "command": "node",
+ "args": [index_js],
+ "env": { "VAULT_PATH": vault_path }
+ })
+ }
+
+ fn write_mcp_servers_config(config_path: &Path, servers: Vec<(&str, serde_json::Value)>) {
+ let servers = servers
+ .into_iter()
+ .map(|(name, server)| (name.to_string(), server))
+ .collect::>();
+ write_config_json(config_path, serde_json::json!({ "mcpServers": servers }));
+ }
+
fn write_index_js(dir: &Path) -> PathBuf {
let index_js = dir.join("index.js");
std::fs::write(&index_js, "console.log('ok');").unwrap();
@@ -404,15 +434,11 @@ mod tests {
#[test]
fn upsert_preserves_other_servers() {
- let tmp = tempfile::tempdir().unwrap();
- let config_path = tmp.path().join("mcp.json");
-
- let existing = serde_json::json!({
- "mcpServers": {
- "other-server": { "command": "other", "args": [] }
- }
- });
- std::fs::write(&config_path, serde_json::to_string(&existing).unwrap()).unwrap();
+ let (_tmp, config_path) = temp_config_path("mcp.json");
+ write_mcp_servers_config(
+ &config_path,
+ vec![("other-server", serde_json::json!({ "command": "other", "args": [] }))],
+ );
let entry = build_mcp_entry("/test/index.js", "/vault");
upsert_mcp_config(&config_path, &entry).unwrap();
@@ -423,6 +449,30 @@ mod tests {
assert!(config["mcpServers"][MCP_SERVER_NAME].is_object());
}
+ #[test]
+ fn upsert_preserves_other_top_level_settings() {
+ let (_tmp, config_path) = temp_config_path(".claude.json");
+ write_config_json(
+ &config_path,
+ serde_json::json!({
+ "model": "sonnet",
+ "theme": "dark",
+ "mcpServers": {
+ "other-server": { "command": "other", "args": [] }
+ }
+ }),
+ );
+
+ let entry = build_mcp_entry("/test/index.js", "/vault");
+ upsert_mcp_config(&config_path, &entry).unwrap();
+
+ let config = read_config(&config_path);
+ assert_eq!(config["model"], "sonnet");
+ assert_eq!(config["theme"], "dark");
+ assert!(config["mcpServers"]["other-server"].is_object());
+ assert!(config["mcpServers"][MCP_SERVER_NAME].is_object());
+ }
+
#[test]
fn upsert_creates_parent_dirs() {
let tmp = tempfile::tempdir().unwrap();
@@ -491,22 +541,42 @@ mod tests {
#[test]
fn register_mcp_to_configs_writes_multiple_configs() {
let tmp = tempfile::tempdir().unwrap();
+ let claude_user_cfg = tmp.path().join(".claude.json");
let claude_cfg = tmp.path().join("claude").join("mcp.json");
let cursor_cfg = tmp.path().join("cursor").join("mcp.json");
let entry = build_mcp_entry("/test/index.js", "/vault");
- register_mcp_to_configs(&entry, &[claude_cfg.clone(), cursor_cfg.clone()]);
+ register_mcp_to_configs(
+ &entry,
+ &[claude_user_cfg.clone(), claude_cfg.clone(), cursor_cfg.clone()],
+ );
+ assert!(claude_user_cfg.exists());
assert!(claude_cfg.exists());
assert!(cursor_cfg.exists());
- let raw = std::fs::read_to_string(&claude_cfg).unwrap();
+ let raw = std::fs::read_to_string(&claude_user_cfg).unwrap();
let config: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(
config["mcpServers"][MCP_SERVER_NAME]["args"][0],
"/test/index.js"
);
}
+
+ #[test]
+ fn mcp_config_paths_for_home_includes_claude_root_and_legacy_paths() {
+ let home = Path::new("/Users/tester");
+ let paths = mcp_config_paths_for_home(home);
+
+ assert_eq!(
+ paths,
+ vec![
+ home.join(".claude.json"),
+ home.join(".claude").join("mcp.json"),
+ home.join(".cursor").join("mcp.json"),
+ ]
+ );
+ }
#[test]
fn upsert_returns_error_for_invalid_json() {
let tmp = tempfile::tempdir().unwrap();
@@ -528,23 +598,17 @@ mod tests {
#[test]
fn read_registered_mcp_entry_prefers_primary_server_name() {
- let tmp = tempfile::tempdir().unwrap();
- let config_path = tmp.path().join("mcp.json");
- let config = serde_json::json!({
- "mcpServers": {
- "tolaria": {
- "command": "node",
- "args": ["/primary/index.js"],
- "env": { "VAULT_PATH": "/primary" }
- },
- "laputa": {
- "command": "node",
- "args": ["/legacy/index.js"],
- "env": { "VAULT_PATH": "/legacy" }
- }
- }
- });
- std::fs::write(&config_path, serde_json::to_string(&config).unwrap()).unwrap();
+ let (_tmp, config_path) = temp_config_path("mcp.json");
+ write_mcp_servers_config(
+ &config_path,
+ vec![
+ (MCP_SERVER_NAME, managed_server("/primary/index.js", "/primary")),
+ (
+ LEGACY_MCP_SERVER_NAME,
+ managed_server("/legacy/index.js", "/legacy"),
+ ),
+ ],
+ );
let entry = read_registered_mcp_entry(&config_path).unwrap();
assert_eq!(entry["env"]["VAULT_PATH"], "/primary");
@@ -552,18 +616,14 @@ mod tests {
#[test]
fn read_registered_mcp_entry_uses_legacy_server_name() {
- let tmp = tempfile::tempdir().unwrap();
- let config_path = tmp.path().join("mcp.json");
- let config = serde_json::json!({
- "mcpServers": {
- "laputa": {
- "command": "node",
- "args": ["/legacy/index.js"],
- "env": { "VAULT_PATH": "/legacy" }
- }
- }
- });
- std::fs::write(&config_path, serde_json::to_string(&config).unwrap()).unwrap();
+ let (_tmp, config_path) = temp_config_path("mcp.json");
+ write_mcp_servers_config(
+ &config_path,
+ vec![(
+ LEGACY_MCP_SERVER_NAME,
+ managed_server("/legacy/index.js", "/legacy"),
+ )],
+ );
let entry = read_registered_mcp_entry(&config_path).unwrap();
assert_eq!(entry["env"]["VAULT_PATH"], "/legacy");
diff --git a/src/components/McpSetupDialog.test.tsx b/src/components/McpSetupDialog.test.tsx
index cc2a6eb8..52e165c0 100644
--- a/src/components/McpSetupDialog.test.tsx
+++ b/src/components/McpSetupDialog.test.tsx
@@ -17,6 +17,9 @@ describe('McpSetupDialog', () => {
expect(screen.getByText('Set Up External AI Tools')).toBeInTheDocument()
expect(screen.getByText(/will not touch third-party config files until you confirm here/i)).toBeInTheDocument()
+ expect(screen.getAllByText('~/.claude.json')).toHaveLength(2)
+ expect(screen.getByText('~/.claude/mcp.json')).toBeInTheDocument()
+ expect(screen.getByText(/Claude Code CLI reads/i)).toBeInTheDocument()
expect(screen.getByTestId('mcp-setup-connect')).toHaveTextContent('Connect External AI Tools')
expect(screen.queryByTestId('mcp-setup-disconnect')).not.toBeInTheDocument()
})
diff --git a/src/components/McpSetupDialog.tsx b/src/components/McpSetupDialog.tsx
index 2ba48343..0f6aa379 100644
--- a/src/components/McpSetupDialog.tsx
+++ b/src/components/McpSetupDialog.tsx
@@ -70,11 +70,12 @@ export function McpSetupDialog({
Confirming this action will write or update Tolaria's single tolaria MCP entry in:
+
~/.claude.json
~/.claude/mcp.json
~/.cursor/mcp.json
- The entry points those tools at the current vault. Cancel leaves both files untouched, reconnect is idempotent, and disconnect removes Tolaria's entry again.
+ Claude Code CLI reads ~/.claude.json, while Tolaria also refreshes the legacy Claude MCP file for compatibility with older setups. Cancel leaves all files untouched, reconnect is idempotent, and disconnect removes Tolaria's entry again.