Skip to content

Commit 54d17c9

Browse files
authored
fix: migrate new mcp server config (#6651)
1 parent eb79642 commit 54d17c9

File tree

4 files changed

+251
-35
lines changed

4 files changed

+251
-35
lines changed

src-tauri/src/core/mcp/helpers.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,3 +934,47 @@ pub async fn should_restart_server(
934934
}
935935
}
936936
}
937+
938+
// Add a new server configuration to the MCP config file
939+
pub fn add_server_config<R: Runtime>(
940+
app_handle: tauri::AppHandle<R>,
941+
server_key: String,
942+
server_value: Value,
943+
) -> Result<(), String> {
944+
add_server_config_with_path(app_handle, server_key, server_value, None)
945+
}
946+
947+
// Add a new server configuration to the MCP config file with custom path support
948+
pub fn add_server_config_with_path<R: Runtime>(
949+
app_handle: tauri::AppHandle<R>,
950+
server_key: String,
951+
server_value: Value,
952+
config_filename: Option<&str>,
953+
) -> Result<(), String> {
954+
let config_filename = config_filename.unwrap_or("mcp_config.json");
955+
let config_path = get_jan_data_folder_path(app_handle).join(config_filename);
956+
957+
let mut config: Value = serde_json::from_str(
958+
&std::fs::read_to_string(&config_path)
959+
.map_err(|e| format!("Failed to read config file: {e}"))?,
960+
)
961+
.map_err(|e| format!("Failed to parse config: {e}"))?;
962+
963+
config
964+
.as_object_mut()
965+
.ok_or("Config root is not an object")?
966+
.entry("mcpServers")
967+
.or_insert_with(|| Value::Object(serde_json::Map::new()))
968+
.as_object_mut()
969+
.ok_or("mcpServers is not an object")?
970+
.insert(server_key, server_value);
971+
972+
std::fs::write(
973+
&config_path,
974+
serde_json::to_string_pretty(&config)
975+
.map_err(|e| format!("Failed to serialize config: {e}"))?,
976+
)
977+
.map_err(|e| format!("Failed to write config file: {e}"))?;
978+
979+
Ok(())
980+
}

src-tauri/src/core/mcp/tests.rs

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::helpers::run_mcp_commands;
1+
use super::helpers::{add_server_config, add_server_config_with_path, run_mcp_commands};
22
use crate::core::app::commands::get_jan_data_folder_path;
33
use crate::core::state::SharedMcpServers;
44
use std::collections::HashMap;
@@ -37,3 +37,140 @@ async fn test_run_mcp_commands() {
3737
// Clean up the mock config file
3838
std::fs::remove_file(&config_path).expect("Failed to remove config file");
3939
}
40+
41+
#[test]
42+
fn test_add_server_config_new_file() {
43+
let app = mock_app();
44+
let app_path = get_jan_data_folder_path(app.handle().clone());
45+
let config_path = app_path.join("mcp_config_test_new.json");
46+
47+
// Ensure the directory exists
48+
if let Some(parent) = config_path.parent() {
49+
std::fs::create_dir_all(parent).expect("Failed to create parent directory");
50+
}
51+
52+
// Create initial config file with empty mcpServers
53+
let mut file = File::create(&config_path).expect("Failed to create config file");
54+
file.write_all(b"{\"mcpServers\":{}}")
55+
.expect("Failed to write to config file");
56+
drop(file);
57+
58+
// Test adding a new server config
59+
let server_value = serde_json::json!({
60+
"command": "npx",
61+
"args": ["-y", "test-server"],
62+
"env": { "TEST_API_KEY": "test_key" },
63+
"active": false
64+
});
65+
66+
let result = add_server_config_with_path(
67+
app.handle().clone(),
68+
"test_server".to_string(),
69+
server_value.clone(),
70+
Some("mcp_config_test_new.json"),
71+
);
72+
73+
assert!(result.is_ok(), "Failed to add server config: {:?}", result);
74+
75+
// Verify the config was added correctly
76+
let config_content = std::fs::read_to_string(&config_path)
77+
.expect("Failed to read config file");
78+
let config: serde_json::Value = serde_json::from_str(&config_content)
79+
.expect("Failed to parse config");
80+
81+
assert!(config["mcpServers"]["test_server"].is_object());
82+
assert_eq!(config["mcpServers"]["test_server"]["command"], "npx");
83+
assert_eq!(config["mcpServers"]["test_server"]["args"][0], "-y");
84+
assert_eq!(config["mcpServers"]["test_server"]["args"][1], "test-server");
85+
86+
// Clean up
87+
std::fs::remove_file(&config_path).expect("Failed to remove config file");
88+
}
89+
90+
#[test]
91+
fn test_add_server_config_existing_servers() {
92+
let app = mock_app();
93+
let app_path = get_jan_data_folder_path(app.handle().clone());
94+
let config_path = app_path.join("mcp_config_test_existing.json");
95+
96+
// Ensure the directory exists
97+
if let Some(parent) = config_path.parent() {
98+
std::fs::create_dir_all(parent).expect("Failed to create parent directory");
99+
}
100+
101+
// Create config file with existing server
102+
let initial_config = serde_json::json!({
103+
"mcpServers": {
104+
"existing_server": {
105+
"command": "existing_command",
106+
"args": ["arg1"],
107+
"active": true
108+
}
109+
}
110+
});
111+
112+
let mut file = File::create(&config_path).expect("Failed to create config file");
113+
file.write_all(serde_json::to_string_pretty(&initial_config).unwrap().as_bytes())
114+
.expect("Failed to write to config file");
115+
drop(file);
116+
117+
// Add new server
118+
let new_server_value = serde_json::json!({
119+
"command": "new_command",
120+
"args": ["new_arg"],
121+
"active": false
122+
});
123+
124+
let result = add_server_config_with_path(
125+
app.handle().clone(),
126+
"new_server".to_string(),
127+
new_server_value,
128+
Some("mcp_config_test_existing.json"),
129+
);
130+
131+
assert!(result.is_ok(), "Failed to add server config: {:?}", result);
132+
133+
// Verify both servers exist
134+
let config_content = std::fs::read_to_string(&config_path)
135+
.expect("Failed to read config file");
136+
let config: serde_json::Value = serde_json::from_str(&config_content)
137+
.expect("Failed to parse config");
138+
139+
// Check existing server is still there
140+
assert!(config["mcpServers"]["existing_server"].is_object());
141+
assert_eq!(config["mcpServers"]["existing_server"]["command"], "existing_command");
142+
143+
// Check new server was added
144+
assert!(config["mcpServers"]["new_server"].is_object());
145+
assert_eq!(config["mcpServers"]["new_server"]["command"], "new_command");
146+
147+
// Clean up
148+
std::fs::remove_file(&config_path).expect("Failed to remove config file");
149+
}
150+
151+
#[test]
152+
fn test_add_server_config_missing_config_file() {
153+
let app = mock_app();
154+
let app_path = get_jan_data_folder_path(app.handle().clone());
155+
let config_path = app_path.join("nonexistent_config.json");
156+
157+
// Ensure the file doesn't exist
158+
if config_path.exists() {
159+
std::fs::remove_file(&config_path).ok();
160+
}
161+
162+
let server_value = serde_json::json!({
163+
"command": "test",
164+
"args": [],
165+
"active": false
166+
});
167+
168+
let result = add_server_config(
169+
app.handle().clone(),
170+
"test".to_string(),
171+
server_value,
172+
);
173+
174+
assert!(result.is_err(), "Expected error when config file doesn't exist");
175+
assert!(result.unwrap_err().contains("Failed to read config file"));
176+
}

src-tauri/src/core/setup.rs

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,23 @@ use std::{
33
fs::{self, File},
44
io::Read,
55
path::PathBuf,
6+
sync::Arc,
67
};
78
use tar::Archive;
89
use tauri::{
910
menu::{Menu, MenuItem, PredefinedMenuItem},
1011
tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent},
11-
App, Emitter, Manager,
12+
App, Emitter, Manager, Wry,
1213
};
13-
use tauri_plugin_store::StoreExt;
14-
// use tokio::sync::Mutex;
15-
// use tokio::time::{sleep, Duration}; // Using tokio::sync::Mutex
16-
// // MCP
14+
use tauri_plugin_store::Store;
15+
16+
use crate::core::mcp::helpers::add_server_config;
1717

18-
// MCP
1918
use super::{
20-
app::commands::get_jan_data_folder_path, extensions::commands::get_jan_extensions_path,
21-
mcp::helpers::run_mcp_commands, state::AppState,
19+
extensions::commands::get_jan_extensions_path, mcp::helpers::run_mcp_commands, state::AppState,
2220
};
2321

2422
pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), String> {
25-
let mut store_path = get_jan_data_folder_path(app.clone());
26-
store_path.push("store.json");
27-
let store = app.store(store_path).expect("Store not initialized");
28-
let stored_version = store
29-
.get("version")
30-
.and_then(|v| v.as_str().map(String::from))
31-
.unwrap_or_default();
32-
33-
let app_version = app
34-
.config()
35-
.version
36-
.clone()
37-
.unwrap_or_else(|| "".to_string());
38-
3923
let extensions_path = get_jan_extensions_path(app.clone());
4024
let pre_install_path = app
4125
.path()
@@ -50,13 +34,8 @@ pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), Stri
5034
if std::env::var("IS_CLEAN").is_ok() {
5135
clean_up = true;
5236
}
53-
log::info!(
54-
"Installing extensions. Clean up: {}, Stored version: {}, App version: {}",
55-
clean_up,
56-
stored_version,
57-
app_version
58-
);
59-
if !clean_up && stored_version == app_version && extensions_path.exists() {
37+
log::info!("Installing extensions. Clean up: {}", clean_up);
38+
if !clean_up && extensions_path.exists() {
6039
return Ok(());
6140
}
6241

@@ -160,10 +139,36 @@ pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), Stri
160139
)
161140
.map_err(|e| e.to_string())?;
162141

163-
// Store the new app version
164-
store.set("version", serde_json::json!(app_version));
165-
store.save().expect("Failed to save store");
142+
Ok(())
143+
}
166144

145+
// Migrate MCP servers configuration
146+
pub fn migrate_mcp_servers(
147+
app_handle: tauri::AppHandle,
148+
store: Arc<Store<Wry>>,
149+
) -> Result<(), String> {
150+
let mcp_version = store
151+
.get("mcp_version")
152+
.and_then(|v| v.as_i64())
153+
.unwrap_or_else(|| 0);
154+
if mcp_version < 1 {
155+
log::info!("Migrating MCP schema version 1");
156+
let result = add_server_config(
157+
app_handle,
158+
"exa".to_string(),
159+
serde_json::json!({
160+
"command": "npx",
161+
"args": ["-y", "exa-mcp-server"],
162+
"env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" },
163+
"active": false
164+
}),
165+
);
166+
if let Err(e) = result {
167+
log::error!("Failed to add server config: {}", e);
168+
}
169+
}
170+
store.set("mcp_version", 1);
171+
store.save().expect("Failed to save store");
167172
Ok(())
168173
}
169174

src-tauri/src/lib.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use jan_utils::generate_app_token;
1010
use std::{collections::HashMap, sync::Arc};
1111
use tauri::{Emitter, Manager, RunEvent};
1212
use tauri_plugin_llamacpp::cleanup_llama_processes;
13+
use tauri_plugin_store::StoreExt;
1314
use tokio::sync::Mutex;
1415

1516
use crate::core::setup::setup_tray;
@@ -151,11 +152,40 @@ pub fn run() {
151152
)?;
152153
app.handle()
153154
.plugin(tauri_plugin_updater::Builder::new().build())?;
154-
// Install extensions
155-
if let Err(e) = setup::install_extensions(app.handle().clone(), false) {
155+
156+
// Start migration
157+
let mut store_path = get_jan_data_folder_path(app.handle().clone());
158+
store_path.push("store.json");
159+
let store = app
160+
.handle()
161+
.store(store_path)
162+
.expect("Store not initialized");
163+
let stored_version = store
164+
.get("version")
165+
.and_then(|v| v.as_str().map(String::from))
166+
.unwrap_or_default();
167+
let app_version = app
168+
.config()
169+
.version
170+
.clone()
171+
.unwrap_or_else(|| "".to_string());
172+
// Migrate extensions
173+
if let Err(e) =
174+
setup::install_extensions(app.handle().clone(), stored_version != app_version)
175+
{
156176
log::error!("Failed to install extensions: {}", e);
157177
}
158178

179+
// Migrate MCP servers
180+
if let Err(e) = setup::migrate_mcp_servers(app.handle().clone(), store.clone()) {
181+
log::error!("Failed to migrate MCP servers: {}", e);
182+
}
183+
184+
// Store the new app version
185+
store.set("version", serde_json::json!(app_version));
186+
store.save().expect("Failed to save store");
187+
// Migration completed
188+
159189
if option_env!("ENABLE_SYSTEM_TRAY_ICON").unwrap_or("false") == "true" {
160190
log::info!("Enabling system tray icon");
161191
let _ = setup_tray(app);

0 commit comments

Comments
 (0)