fix: use selected vault for mcp bridge

This commit is contained in:
lucaronin
2026-04-28 02:13:24 +02:00
parent 49717dc671
commit e912ab9a9f
12 changed files with 283 additions and 24 deletions

View File

@@ -131,6 +131,22 @@ pub async fn check_mcp_status(vault_path: String) -> Result<crate::mcp::McpStatu
.map_err(|e| format!("MCP status check failed: {e}"))
}
#[cfg(desktop)]
#[tauri::command]
pub async fn sync_mcp_bridge_vault(
app: tauri::AppHandle,
vault_path: Option<String>,
) -> Result<String, String> {
let expanded_vault_path = vault_path
.as_deref()
.map(str::trim)
.filter(|path| !path.is_empty())
.map(|path| super::expand_tilde(path).into_owned());
let vault_path = expanded_vault_path.as_deref().map(std::path::Path::new);
crate::sync_ws_bridge_for_vault(&app, vault_path).map(str::to_string)
}
// ── MCP commands (mobile stubs) ─────────────────────────────────────────────
#[cfg(mobile)]
@@ -151,6 +167,12 @@ pub async fn check_mcp_status(_vault_path: String) -> Result<crate::mcp::McpStat
Ok(crate::mcp::McpStatus::NotInstalled)
}
#[cfg(mobile)]
#[tauri::command]
pub async fn sync_mcp_bridge_vault(_vault_path: Option<String>) -> Result<String, String> {
Err("MCP is not available on mobile".into())
}
// ── Menu commands ───────────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]

View File

@@ -102,6 +102,78 @@ fn log_startup_result(label: &str, result: Result<usize, String>) {
}
}
#[cfg(desktop)]
fn selected_mcp_bridge_vault_path(vault_list: &vault_list::VaultList) -> Option<PathBuf> {
vault_list
.active_vault
.as_deref()
.map(str::trim)
.filter(|path| !path.is_empty())
.map(PathBuf::from)
}
#[cfg(desktop)]
fn validate_mcp_bridge_vault_path(vault_path: &Path) -> Result<PathBuf, String> {
let resolved = std::fs::canonicalize(vault_path).map_err(|e| {
format!(
"MCP bridge vault is not available: {} ({e})",
vault_path.display()
)
})?;
if !resolved.is_dir() {
return Err(format!(
"MCP bridge vault is not available: {} is not a directory",
vault_path.display()
));
}
Ok(resolved)
}
#[cfg(desktop)]
fn stop_ws_bridge_child(active_child: &mut Option<Child>) {
if let Some(mut child) = active_child.take() {
let _ = child.kill();
let _ = child.wait();
log::info!("ws-bridge child process stopped");
}
}
#[cfg(desktop)]
pub(crate) fn sync_ws_bridge_for_vault(
app_handle: &tauri::AppHandle,
vault_path: Option<&Path>,
) -> Result<&'static str, String> {
use tauri::Manager;
let state: tauri::State<'_, WsBridgeChild> = app_handle.state();
let mut active_child = state
.0
.lock()
.map_err(|_| "Failed to lock ws-bridge state".to_string())?;
let Some(vault_path) = vault_path else {
stop_ws_bridge_child(&mut active_child);
return Ok("stopped");
};
let resolved_vault_path = match validate_mcp_bridge_vault_path(vault_path) {
Ok(path) => path,
Err(e) => {
stop_ws_bridge_child(&mut active_child);
return Err(e);
}
};
stop_ws_bridge_child(&mut active_child);
let child = mcp::spawn_ws_bridge(&resolved_vault_path)?;
*active_child = Some(child);
Ok("started")
}
/// Run startup housekeeping on the default vault (migrate legacy frontmatter, seed configs).
#[cfg(desktop)]
fn run_startup_tasks() {
@@ -124,17 +196,21 @@ fn run_startup_tasks() {
#[cfg(desktop)]
fn spawn_ws_bridge(app: &mut tauri::App) {
use tauri::Manager;
let vault_path = dirs::home_dir()
.map(|h| h.join("Laputa"))
.unwrap_or_default();
let vp_str = vault_path.to_string_lossy().to_string();
match mcp::spawn_ws_bridge(&vp_str) {
Ok(child) => {
let state: tauri::State<'_, WsBridgeChild> = app.state();
*state.0.lock().unwrap() = Some(child);
let vault_path = match vault_list::load_vault_list() {
Ok(vault_list) => selected_mcp_bridge_vault_path(&vault_list),
Err(e) => {
log::warn!("Failed to load active vault for ws-bridge startup: {}", e);
None
}
Err(e) => log::warn!("Failed to start ws-bridge: {}", e),
};
let Some(vault_path) = vault_path else {
log::info!("ws-bridge not started: no active vault selected");
return;
};
if let Err(e) = sync_ws_bridge_for_vault(app.handle(), Some(&vault_path)) {
log::warn!("Failed to start ws-bridge: {}", e);
}
}
@@ -356,6 +432,7 @@ macro_rules! app_invoke_handler {
commands::register_mcp_tools,
commands::remove_mcp_tools,
commands::check_mcp_status,
commands::sync_mcp_bridge_vault,
commands::repair_vault,
commands::reinit_telemetry,
commands::list_views,
@@ -416,6 +493,11 @@ mod tests {
use super::MACOS_WEBVIEW_RESERVED_COMMAND_KEYS;
use super::MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS;
#[cfg(desktop)]
use super::{selected_mcp_bridge_vault_path, validate_mcp_bridge_vault_path};
#[cfg(desktop)]
use crate::vault_list::VaultList;
#[cfg(all(desktop, unix))]
use super::vault_asset_scope_roots;
@@ -459,6 +541,48 @@ mod tests {
assert!(overrides.is_empty());
}
#[cfg(desktop)]
#[test]
fn selected_mcp_bridge_vault_path_uses_persisted_active_vault() {
let list = VaultList {
vaults: Vec::new(),
active_vault: Some("/tmp/Selected Vault".to_string()),
hidden_defaults: Vec::new(),
};
assert_eq!(
selected_mcp_bridge_vault_path(&list),
Some(std::path::PathBuf::from("/tmp/Selected Vault"))
);
}
#[cfg(desktop)]
#[test]
fn selected_mcp_bridge_vault_path_ignores_blank_active_vault() {
let list = VaultList {
vaults: Vec::new(),
active_vault: Some(" ".to_string()),
hidden_defaults: Vec::new(),
};
assert_eq!(selected_mcp_bridge_vault_path(&list), None);
}
#[cfg(desktop)]
#[test]
fn validate_mcp_bridge_vault_path_requires_existing_directory() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("Vault With Spaces");
std::fs::create_dir(&vault).unwrap();
let resolved = validate_mcp_bridge_vault_path(&vault).unwrap();
assert_eq!(resolved, vault.canonicalize().unwrap());
let missing = dir.path().join("Missing Vault");
let err = validate_mcp_bridge_vault_path(&missing).unwrap_err();
assert!(err.contains("MCP bridge vault is not available"));
}
#[cfg(all(desktop, unix))]
#[test]
fn vault_asset_scope_roots_include_requested_symlink_path() {

View File

@@ -215,10 +215,11 @@ fn mcp_server_dir_has_files(path: &Path) -> bool {
}
/// Spawn the WebSocket bridge as a child process.
pub fn spawn_ws_bridge(vault_path: &str) -> Result<Child, String> {
pub fn spawn_ws_bridge(vault_path: impl AsRef<Path>) -> Result<Child, String> {
let node = find_node()?;
let server_dir = mcp_server_dir()?;
let script = server_dir.join("ws-bridge.js");
let vault_path = vault_path.as_ref();
let child = crate::hidden_command(node)
.arg(&script)
@@ -231,7 +232,11 @@ pub fn spawn_ws_bridge(vault_path: &str) -> Result<Child, String> {
.spawn()
.map_err(|e| format!("Failed to spawn ws-bridge: {e}"))?;
log::info!("ws-bridge spawned (pid: {})", child.id());
log::info!(
"ws-bridge spawned (pid: {}, vault: {})",
child.id(),
vault_path.display()
);
Ok(child)
}