diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dc482f57f..312392eb5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,7 +21,11 @@ tauri-build = { version = "2.0.2", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" -tauri = { version = "2.1.0", features = [ "protocol-asset",'macos-private-api'] } +tauri = { version = "2.1.0", features = [ + "protocol-asset", + 'macos-private-api', + "test", +] } tauri-plugin-log = "2.0.0-rc" tauri-plugin-shell = "2.2.0" flate2 = "1.0" @@ -33,3 +37,9 @@ hyper = { version = "0.14", features = ["server"] } reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["full"] } tracing = "0.1.41" +rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", branch = "main", features = [ + "client", + "transport-sse", + "transport-child-process", + "tower", +] } diff --git a/src-tauri/src/core/cmd.rs b/src-tauri/src/core/cmd.rs index 656fc8806..62f53bf56 100644 --- a/src-tauri/src/core/cmd.rs +++ b/src-tauri/src/core/cmd.rs @@ -1,6 +1,8 @@ +use rmcp::model::{CallToolRequestParam, CallToolResult, Tool}; use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; use std::{fs, path::PathBuf}; -use tauri::{AppHandle, Manager, State}; +use tauri::{AppHandle, Manager, Runtime, State}; use super::{server, setup, state::AppState}; @@ -21,7 +23,7 @@ impl AppConfiguration { } #[tauri::command] -pub fn get_app_configurations(app_handle: tauri::AppHandle) -> AppConfiguration { +pub fn get_app_configurations(app_handle: tauri::AppHandle) -> AppConfiguration { let mut app_default_configuration = AppConfiguration::default(); if std::env::var("CI").unwrap_or_default() == "e2e" { @@ -90,7 +92,7 @@ pub fn update_app_configuration( } #[tauri::command] -pub fn get_jan_data_folder_path(app_handle: tauri::AppHandle) -> PathBuf { +pub fn get_jan_data_folder_path(app_handle: tauri::AppHandle) -> PathBuf { let app_configurations = get_app_configurations(app_handle); PathBuf::from(app_configurations.data_folder) } @@ -132,8 +134,13 @@ pub fn read_theme(app_handle: tauri::AppHandle, theme_name: String) -> Result PathBuf { +pub fn get_configuration_file_path(app_handle: tauri::AppHandle) -> PathBuf { let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| { + eprintln!( + "Failed to get app data directory: {}. Using home directory instead.", + err + ); + let home_dir = std::env::var(if cfg!(target_os = "windows") { "USERPROFILE" } else { @@ -148,7 +155,7 @@ pub fn get_configuration_file_path(app_handle: tauri::AppHandle) -> PathBuf { } #[tauri::command] -pub fn default_data_folder_path(app_handle: tauri::AppHandle) -> String { +pub fn default_data_folder_path(app_handle: tauri::AppHandle) -> String { return app_handle .path() .app_data_dir() @@ -258,12 +265,9 @@ pub async fn start_server( port: u16, prefix: String, ) -> Result { - server::start_server( - host, - port, - prefix, - app_token(app.state()).unwrap(), - ).await.map_err(|e| e.to_string())?; + server::start_server(host, port, prefix, app_token(app.state()).unwrap()) + .await + .map_err(|e| e.to_string())?; Ok(true) } @@ -272,3 +276,75 @@ pub async fn stop_server() -> Result<(), String> { server::stop_server().await.map_err(|e| e.to_string())?; Ok(()) } + +/// Retrieves all available tools from all MCP servers +/// +/// # Arguments +/// * `state` - Application state containing MCP server connections +/// +/// # Returns +/// * `Result, String>` - A vector of all tools if successful, or an error message if failed +/// +/// This function: +/// 1. Locks the MCP servers mutex to access server connections +/// 2. Iterates through all connected servers +/// 3. Gets the list of tools from each server +/// 4. Combines all tools into a single vector +/// 5. Returns the combined list of all available tools +#[tauri::command] +pub async fn get_tools(state: State<'_, AppState>) -> Result, String> { + let servers = state.mcp_servers.lock().await; + let mut all_tools: Vec = Vec::new(); + + for (_, service) in servers.iter() { + // List tools + let tools = service.list_all_tools().await.map_err(|e| e.to_string())?; + + for tool in tools { + all_tools.push(tool); + } + } + + Ok(all_tools) +} + +/// Calls a tool on an MCP server by name with optional arguments +/// +/// # Arguments +/// * `state` - Application state containing MCP server connections +/// * `tool_name` - Name of the tool to call +/// * `arguments` - Optional map of argument names to values +/// +/// # Returns +/// * `Result` - Result of the tool call if successful, or error message if failed +/// +/// This function: +/// 1. Locks the MCP servers mutex to access server connections +/// 2. Searches through all servers for one containing the named tool +/// 3. When found, calls the tool on that server with the provided arguments +/// 4. Returns error if no server has the requested tool +#[tauri::command] +pub async fn call_tool( + state: State<'_, AppState>, + tool_name: String, + arguments: Option>, +) -> Result { + let servers = state.mcp_servers.lock().await; + + // Iterate through servers and find the first one that contains the tool + for (_, service) in servers.iter() { + if let Ok(tools) = service.list_all_tools().await { + if tools.iter().any(|t| t.name == tool_name) { + return service + .call_tool(CallToolRequestParam { + name: tool_name.into(), + arguments, + }) + .await + .map_err(|e| e.to_string()); + } + } + } + + Err(format!("Tool {} not found", tool_name)) +} diff --git a/src-tauri/src/core/fs.rs b/src-tauri/src/core/fs.rs index dc046689a..f0ffb9a71 100644 --- a/src-tauri/src/core/fs.rs +++ b/src-tauri/src/core/fs.rs @@ -1,9 +1,12 @@ +// WARNING: These APIs will be deprecated soon due to removing FS API access from frontend. +// It's added to ensure the legacy implementation from frontend still functions before removal. use crate::core::cmd::get_jan_data_folder_path; use std::fs; use std::path::PathBuf; +use tauri::Runtime; #[tauri::command] -pub fn rm(app_handle: tauri::AppHandle, args: Vec) -> Result<(), String> { +pub fn rm(app_handle: tauri::AppHandle, args: Vec) -> Result<(), String> { if args.is_empty() || args[0].is_empty() { return Err("rm error: Invalid argument".to_string()); } @@ -12,7 +15,7 @@ pub fn rm(app_handle: tauri::AppHandle, args: Vec) -> Result<(), String> fs::remove_dir_all(&path).map_err(|e| e.to_string()) } #[tauri::command] -pub fn mkdir(app_handle: tauri::AppHandle, args: Vec) -> Result<(), String> { +pub fn mkdir(app_handle: tauri::AppHandle, args: Vec) -> Result<(), String> { if args.is_empty() || args[0].is_empty() { return Err("mkdir error: Invalid argument".to_string()); } @@ -22,7 +25,10 @@ pub fn mkdir(app_handle: tauri::AppHandle, args: Vec) -> Result<(), Stri } #[tauri::command] -pub fn join_path(app_handle: tauri::AppHandle, args: Vec) -> Result { +pub fn join_path( + app_handle: tauri::AppHandle, + args: Vec, +) -> Result { if args.is_empty() { return Err("join_path error: Invalid argument".to_string()); } @@ -32,7 +38,10 @@ pub fn join_path(app_handle: tauri::AppHandle, args: Vec) -> Result) -> Result { +pub fn exists_sync( + app_handle: tauri::AppHandle, + args: Vec, +) -> Result { if args.is_empty() || args[0].is_empty() { return Err("exist_sync error: Invalid argument".to_string()); } @@ -42,7 +51,10 @@ pub fn exists_sync(app_handle: tauri::AppHandle, args: Vec) -> Result) -> Result { +pub fn read_file_sync( + app_handle: tauri::AppHandle, + args: Vec, +) -> Result { if args.is_empty() || args[0].is_empty() { return Err("read_file_sync error: Invalid argument".to_string()); } @@ -52,8 +64,8 @@ pub fn read_file_sync(app_handle: tauri::AppHandle, args: Vec) -> Result } #[tauri::command] -pub fn readdir_sync( - app_handle: tauri::AppHandle, +pub fn readdir_sync( + app_handle: tauri::AppHandle, args: Vec, ) -> Result, String> { if args.is_empty() || args[0].is_empty() { @@ -74,7 +86,7 @@ fn normalize_file_path(path: &str) -> String { path.replace("file:/", "").replace("file:\\", "") } -fn resolve_path(app_handle: tauri::AppHandle, path: &str) -> PathBuf { +fn resolve_path(app_handle: tauri::AppHandle, path: &str) -> PathBuf { let path = if path.starts_with("file:/") || path.starts_with("file:\\") { let normalized = normalize_file_path(path); let relative_normalized = normalized.strip_prefix("/").unwrap_or(&normalized); @@ -89,3 +101,93 @@ fn resolve_path(app_handle: tauri::AppHandle, path: &str) -> PathBuf { path.canonicalize().unwrap_or(path) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use std::io::Write; + use tauri::test::mock_app; + + #[test] + fn test_rm() { + let app = mock_app(); + let path = "test_rm_dir"; + fs::create_dir_all(get_jan_data_folder_path(app.handle().clone()).join(path)).unwrap(); + let args = vec![format!("file://{}", path).to_string()]; + let result = rm(app.handle().clone(), args); + assert!(result.is_ok()); + assert!(!get_jan_data_folder_path(app.handle().clone()) + .join(path) + .exists()); + } + + #[test] + fn test_mkdir() { + let app = mock_app(); + let path = "test_mkdir_dir"; + let args = vec![format!("file://{}", path).to_string()]; + let result = mkdir(app.handle().clone(), args); + assert!(result.is_ok()); + assert!(get_jan_data_folder_path(app.handle().clone()) + .join(path) + .exists()); + fs::remove_dir_all(get_jan_data_folder_path(app.handle().clone()).join(path)).unwrap(); + } + + #[test] + fn test_join_path() { + let app = mock_app(); + let path = "file://test_dir"; + let args = vec![path.to_string(), "test_file".to_string()]; + let result = join_path(app.handle().clone(), args).unwrap(); + assert_eq!( + result, + get_jan_data_folder_path(app.handle().clone()) + .join("test_dir/test_file") + .to_string_lossy() + .to_string() + ); + } + + #[test] + fn test_exists_sync() { + let app = mock_app(); + let path = "file://test_exists_sync_file"; + let file_path = get_jan_data_folder_path(app.handle().clone()).join(path); + File::create(&file_path).unwrap(); + let args = vec![path.to_string()]; + let result = exists_sync(app.handle().clone(), args).unwrap(); + assert!(result); + fs::remove_file(file_path).unwrap(); + } + + #[test] + fn test_read_file_sync() { + let app = mock_app(); + let path = "file://test_read_file_sync_file"; + let file_path = get_jan_data_folder_path(app.handle().clone()).join(path); + let mut file = File::create(&file_path).unwrap(); + file.write_all(b"test content").unwrap(); + let args = vec![path.to_string()]; + let result = read_file_sync(app.handle().clone(), args).unwrap(); + assert_eq!(result, "test content".to_string()); + fs::remove_file(file_path).unwrap(); + } + + #[test] + fn test_readdir_sync() { + let app = mock_app(); + let path = "file://test_readdir_sync_dir"; + let dir_path = get_jan_data_folder_path(app.handle().clone()).join(path); + fs::create_dir_all(&dir_path).unwrap(); + File::create(dir_path.join("file1.txt")).unwrap(); + File::create(dir_path.join("file2.txt")).unwrap(); + + let args = vec![path.to_string()]; + let result = readdir_sync(app.handle().clone(), args).unwrap(); + assert_eq!(result.len(), 2); + + fs::remove_dir_all(dir_path).unwrap(); + } +} diff --git a/src-tauri/src/core/mcp.rs b/src-tauri/src/core/mcp.rs new file mode 100644 index 000000000..c016effd7 --- /dev/null +++ b/src-tauri/src/core/mcp.rs @@ -0,0 +1,94 @@ +use std::{collections::HashMap, sync::Arc}; + +use rmcp::{service::RunningService, transport::TokioChildProcess, RoleClient, ServiceExt}; +use serde_json::Value; +use tokio::{process::Command, sync::Mutex}; + +/// Runs MCP commands by reading configuration from a JSON file and initializing servers +/// +/// # Arguments +/// * `app_path` - Path to the application directory containing mcp_config.json +/// * `servers_state` - Shared state containing running MCP services +/// +/// # Returns +/// * `Ok(())` if servers were initialized successfully +/// * `Err(String)` if there was an error reading config or starting servers +pub async fn run_mcp_commands( + app_path: String, + servers_state: Arc>>>, +) -> Result<(), String> { + println!( + "Load MCP configs from {}", + app_path.clone() + "/mcp_config.json" + ); + // let mut client_list = HashMap::new(); + let config_content = std::fs::read_to_string(app_path.clone() + "/mcp_config.json") + .map_err(|e| format!("Failed to read config file: {}", e))?; + + let mcp_servers: serde_json::Value = serde_json::from_str(&config_content) + .map_err(|e| format!("Failed to parse config: {}", e))?; + + if let Some(server_map) = mcp_servers.get("mcpServers").and_then(Value::as_object) { + println!("MCP Servers: {server_map:#?}"); + + for (name, config) in server_map { + if let Some((command, args)) = extract_command_args(config) { + let mut cmd = Command::new(command); + args.iter().filter_map(Value::as_str).for_each(|arg| { cmd.arg(arg); }); + + let service = ().serve(TokioChildProcess::new(&mut cmd).map_err(|e| e.to_string())?) + .await + .map_err(|e| e.to_string())?; + + servers_state.lock().await.insert(name.clone(), service); + } + } + } + + // Collect servers into a Vec to avoid holding the RwLockReadGuard across await points + let servers_map = servers_state.lock().await; + for (_, service) in servers_map.iter() { + // Initialize + let _server_info = service.peer_info(); + println!("Connected to server: {_server_info:#?}"); + } + Ok(()) +} + +fn extract_command_args(config: &Value) -> Option<(&str, &Vec)> { + let obj = config.as_object()?; + let command = obj.get("command")?.as_str()?; + let args = obj.get("args")?.as_array()?; + Some((command, args)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::fs::File; + use std::io::Write; + use std::sync::Arc; + use tokio::sync::Mutex; + + #[tokio::test] + async fn test_run_mcp_commands() { + // Create a mock mcp_config.json file + let config_path = "mcp_config.json"; + let mut file = File::create(config_path).expect("Failed to create config file"); + file.write_all(b"{\"mcpServers\":{}}") + .expect("Failed to write to config file"); + + // Call the run_mcp_commands function + let app_path = ".".to_string(); + let servers_state: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); + let result = run_mcp_commands(app_path, servers_state).await; + + // Assert that the function returns Ok(()) + assert!(result.is_ok()); + + // Clean up the mock config file + std::fs::remove_file(config_path).expect("Failed to remove config file"); + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 5ef113aa4..baa8c2834 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -2,4 +2,5 @@ pub mod cmd; pub mod fs; pub mod setup; pub mod state; -pub mod server; \ No newline at end of file +pub mod server; +pub mod mcp; \ No newline at end of file diff --git a/src-tauri/src/core/server.rs b/src-tauri/src/core/server.rs index 67e802564..359da13da 100644 --- a/src-tauri/src/core/server.rs +++ b/src-tauri/src/core/server.rs @@ -1,5 +1,5 @@ use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, Server, StatusCode}; +use hyper::{Body, Request, Response, Server, StatusCode}; use reqwest::Client; use std::convert::Infallible; use std::net::SocketAddr; @@ -25,7 +25,7 @@ struct ProxyConfig { /// Removes a prefix from a path, ensuring proper formatting fn remove_prefix(path: &str, prefix: &str) -> String { debug!("Processing path: {}, removing prefix: {}", path, prefix); - + if !prefix.is_empty() && path.starts_with(prefix) { let result = path[prefix.len()..].to_string(); if result.is_empty() { @@ -41,10 +41,13 @@ fn remove_prefix(path: &str, prefix: &str) -> String { /// Determines the final destination path based on the original request path fn get_destination_path(original_path: &str, prefix: &str) -> String { let removed_prefix_path = remove_prefix(original_path, prefix); - + println!("Removed prefix path: {}", removed_prefix_path); // Special paths don't need the /v1 prefix - if !original_path.contains(prefix) || removed_prefix_path.contains("/healthz") || removed_prefix_path.contains("/process") { + if !original_path.contains(prefix) + || removed_prefix_path.contains("/healthz") + || removed_prefix_path.contains("/process") + { original_path.to_string() } else { format!("/v1{}", removed_prefix_path) @@ -55,7 +58,7 @@ fn get_destination_path(original_path: &str, prefix: &str) -> String { fn build_upstream_url(upstream: &str, path: &str) -> String { let upstream_clean = upstream.trim_end_matches('/'); let path_clean = path.trim_start_matches('/'); - + format!("{}/{}", upstream_clean, path_clean) } @@ -67,7 +70,7 @@ async fn proxy_request( ) -> Result, hyper::Error> { let original_path = req.uri().path(); let path = get_destination_path(original_path, &config.prefix); - + // Block access to /configs endpoint if path.contains("/configs") { return Ok(Response::builder() @@ -79,12 +82,13 @@ async fn proxy_request( // Build the outbound request let upstream_url = build_upstream_url(&config.upstream, &path); debug!("Proxying request to: {}", upstream_url); - + let mut outbound_req = client.request(req.method().clone(), &upstream_url); - + // Copy original headers for (name, value) in req.headers() { - if name != hyper::header::HOST { // Skip host header + if name != hyper::header::HOST { + // Skip host header outbound_req = outbound_req.header(name, value); } } @@ -97,9 +101,9 @@ async fn proxy_request( Ok(response) => { let status = response.status(); debug!("Received response with status: {}", status); - + let mut builder = Response::builder().status(status); - + // Copy response headers for (name, value) in response.headers() { builder = builder.header(name, value); @@ -151,7 +155,7 @@ pub async fn start_server( prefix, auth_token, }; - + // Create HTTP client let client = Client::builder() .timeout(std::time::Duration::from_secs(30)) @@ -161,7 +165,7 @@ pub async fn start_server( let make_svc = make_service_fn(move |_conn| { let client = client.clone(); let config = config.clone(); - + async move { Ok::<_, Infallible>(service_fn(move |req| { proxy_request(req, client.clone(), config.clone()) @@ -189,13 +193,13 @@ pub async fn start_server( /// Stops the currently running proxy server pub async fn stop_server() -> Result<(), Box> { let mut handle_guard = SERVER_HANDLE.lock().await; - + if let Some(handle) = handle_guard.take() { handle.abort(); info!("Proxy server stopped"); } else { debug!("No server was running"); } - + Ok(()) } diff --git a/src-tauri/src/core/setup.rs b/src-tauri/src/core/setup.rs index 8e9e03d2b..705ee27e0 100644 --- a/src-tauri/src/core/setup.rs +++ b/src-tauri/src/core/setup.rs @@ -11,19 +11,18 @@ use tauri_plugin_shell::process::CommandEvent; use tauri_plugin_shell::ShellExt; use tauri_plugin_store::StoreExt; -use super::{cmd::get_jan_extensions_path, state::AppState}; +use super::{ + cmd::{get_jan_data_folder_path, get_jan_extensions_path}, + mcp::run_mcp_commands, + state::AppState, +}; pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), String> { let store = app.store("store.json").expect("Store not initialized"); - let stored_version = if let Some(version) = store.get("version") { - if let Some(version_str) = version.as_str() { - version_str.to_string() - } else { - "".to_string() - } - } else { - "".to_string() - }; + let stored_version = store + .get("version") + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_default(); let app_version = app .config() @@ -31,10 +30,8 @@ pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), Stri .clone() .unwrap_or_else(|| "".to_string()); - if !force { - if stored_version == app_version { - return Ok(()); - } + if !force && stored_version == app_version { + return Ok(()); } let extensions_path = get_jan_extensions_path(app.clone()); let pre_install_path = PathBuf::from("./resources/pre-install"); @@ -46,6 +43,10 @@ pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), Stri }); } + if !force { + return Ok(()); + }; + // Attempt to create it again if !extensions_path.exists() { fs::create_dir_all(&extensions_path).map_err(|e| e.to_string())?; @@ -177,6 +178,19 @@ fn extract_extension_manifest( Ok(None) } +pub fn setup_mcp(app: &App) { + let app_path = get_jan_data_folder_path(app.handle().clone()); + + let state = app.state::().inner(); + let app_path_str = app_path.to_str().unwrap().to_string(); + let servers = state.mcp_servers.clone(); + tauri::async_runtime::spawn(async move { + if let Err(e) = run_mcp_commands(app_path_str, servers).await { + eprintln!("Failed to run mcp commands: {}", e); + } + }); +} + pub fn setup_sidecar(app: &App) -> Result<(), String> { // Setup sidecar diff --git a/src-tauri/src/core/state.rs b/src-tauri/src/core/state.rs index 8654c74b5..93d770bc2 100644 --- a/src-tauri/src/core/state.rs +++ b/src-tauri/src/core/state.rs @@ -1,13 +1,18 @@ -use rand::{distributions::Alphanumeric, Rng}; +use std::{collections::HashMap, sync::Arc}; +use rand::{distributions::Alphanumeric, Rng}; +use rmcp::{service::RunningService, RoleClient}; +use tokio::sync::Mutex; + +#[derive(Default)] pub struct AppState { pub app_token: Option, + pub mcp_servers: Arc>>> } - pub fn generate_app_token() -> String { rand::thread_rng() .sample_iter(&Alphanumeric) .take(32) .map(char::from) .collect() -} \ No newline at end of file +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e7f94e3dd..85578d680 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,12 @@ mod core; use core::{ - setup::{self, setup_engine_binaries, setup_sidecar}, + setup::{self, setup_engine_binaries, setup_mcp, setup_sidecar}, state::{generate_app_token, AppState}, }; +use std::{collections::HashMap, sync::Arc}; use tauri::Emitter; +use tokio::sync::Mutex; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -13,6 +15,7 @@ pub fn run() { .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ + // FS commands - Deperecate soon core::fs::join_path, core::fs::mkdir, core::fs::exists_sync, @@ -35,9 +38,13 @@ pub fn run() { core::cmd::app_token, core::cmd::start_server, core::cmd::stop_server, + // MCP commands + core::cmd::get_tools, + core::cmd::call_tool ]) .manage(AppState { app_token: Some(generate_app_token()), + mcp_servers: Arc::new(Mutex::new(HashMap::new())), }) .setup(|app| { if cfg!(debug_assertions) { @@ -53,6 +60,8 @@ pub fn run() { eprintln!("Failed to install extensions: {}", e); } + setup_mcp(app); + setup_sidecar(app).expect("Failed to setup sidecar"); setup_engine_binaries(app).expect("Failed to setup engine binaries"); @@ -60,7 +69,7 @@ pub fn run() { Ok(()) }) .on_window_event(|window, event| match event { - tauri::WindowEvent::CloseRequested { api, .. } => { + tauri::WindowEvent::CloseRequested { .. } => { window.emit("kill-sidecar", ()).unwrap(); } _ => {} diff --git a/web/containers/Providers/CoreConfigurator.tsx b/web/containers/Providers/CoreConfigurator.tsx index 8af31162d..54b716309 100644 --- a/web/containers/Providers/CoreConfigurator.tsx +++ b/web/containers/Providers/CoreConfigurator.tsx @@ -24,7 +24,9 @@ export const CoreConfigurator = ({ children }: PropsWithChildren) => { setTimeout(async () => { if (!isCoreExtensionInstalled()) { setSettingUp(true) - await setupBaseExtensions() + + await new Promise((resolve) => setTimeout(resolve, 500)) + setupBaseExtensions() return } diff --git a/web/services/tauriService.ts b/web/services/tauriService.ts index efb201ccd..488593658 100644 --- a/web/services/tauriService.ts +++ b/web/services/tauriService.ts @@ -2,12 +2,16 @@ import { CoreRoutes, APIRoutes } from '@janhq/core' import { invoke } from '@tauri-apps/api/core' // Define API routes based on different route types -export const Routes = [...CoreRoutes, ...APIRoutes, 'installExtensions'].map( - (r) => ({ - path: `app`, - route: r, - }) -) +export const Routes = [ + ...CoreRoutes, + ...APIRoutes, + 'installExtensions', + 'getTools', + 'callTool', +].map((r) => ({ + path: `app`, + route: r, +})) // Function to open an external URL in a new browser window export function openExternalUrl(url: string) {