From e912ab9a9fa57bbd0338092667cadcbfcdcb6ef6 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 28 Apr 2026 02:13:24 +0200 Subject: [PATCH] fix: use selected vault for mcp bridge --- docs/ABSTRACTIONS.md | 2 + docs/ARCHITECTURE.md | 10 +- mcp-server/index.js | 3 +- mcp-server/test.js | 17 ++++ mcp-server/vault-path.js | 7 ++ mcp-server/ws-bridge.js | 27 ++++-- src-tauri/src/commands/system.rs | 22 +++++ src-tauri/src/lib.rs | 144 +++++++++++++++++++++++++++-- src-tauri/src/mcp.rs | 9 +- src/hooks/useVaultSwitcher.test.ts | 42 +++++++++ src/hooks/useVaultSwitcher.ts | 23 +++++ src/mock-tauri/mock-handlers.ts | 1 + 12 files changed, 283 insertions(+), 24 deletions(-) create mode 100644 mcp-server/vault-path.js diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 214667e7..eb84aa77 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -342,6 +342,8 @@ Command-layer path access is fenced to the active vault before file operations r UI-only file actions operate on paths that are already selected or indexed in React state. Reveal-in-Finder and external-open calls route through the Tauri opener plugin, while copy-path uses the browser clipboard API; none of those actions mutate vault contents or bypass the backend write boundary. +The local MCP WebSocket bridge follows the same active-vault boundary. `useVaultSwitcher` calls `sync_mcp_bridge_vault` after the persisted selection loads and after each vault switch; the desktop command starts/restarts the bridge with that vault's canonical path, or stops it when there is no selected vault. MCP Node entrypoints require `VAULT_PATH` and fail clearly instead of falling back to `~/Laputa`. + ### Vault Caching `vault::scan_vault_cached(path)` wraps scanning with git-based caching: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c3edccb9..b3232509 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -327,13 +327,13 @@ Tolaria can register itself as an MCP server in: - `~/.cursor/mcp.json` (Cursor) - `~/.config/mcp/mcp.json` (generic MCP-compatible clients) -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. Tolaria verifies Node.js is available before writing config, writes an explicit `type: "stdio"` entry, pins `VAULT_PATH` to the active vault, and sets `WS_UI_PORT=9711` so UI actions route back to the desktop app. Packaged builds resolve `mcp-server/` from the installed resource directory next to the executable before falling back to macOS `Resources`, Linux bundle paths, and AppImage paths. The `useMcpStatus` hook tracks whether the active vault is explicitly connected (`checking | installed | not_installed`). +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. Tolaria verifies Node.js is available before writing config, writes an explicit `type: "stdio"` entry, pins `VAULT_PATH` to the active vault, and sets `WS_UI_PORT=9711` so UI actions route back to the desktop app. Packaged builds resolve `mcp-server/` from the installed resource directory next to the executable before falling back to macOS `Resources`, Linux bundle paths, and AppImage paths. The `useMcpStatus` hook tracks whether the active vault is explicitly connected (`checking | installed | not_installed`). The desktop WebSocket bridge is started only when a persisted active vault exists and is resynced from React state on vault changes; no selected vault stops the bridge instead of falling back to `~/Laputa`. ### Architecture ```mermaid flowchart TD - subgraph MCP["MCP Server (Node.js) — spawned by Tauri on startup"] + subgraph MCP["MCP Server (Node.js) — selected-vault scoped"] IDX["index.js"] VAULT["vault.js\n(findMarkdownFiles, readNote, createNote,\nsearchNotes, appendToNote, editNoteFrontmatter,\ndeleteNote, linkNotes, listNotes, vaultContext)"] WSB["ws-bridge.js"] @@ -345,7 +345,7 @@ flowchart TD WSB -->|"port 9711 — UI bridge"| FE["Frontend\n(useAiActivity)"] end - TAURI["Tauri (mcp.rs)"] -->|"spawn on startup"| MCP + TAURI["Tauri bridge lifecycle"] -->|"start/stop/restart with active VAULT_PATH"| MCP UI["Status bar / Command Palette"] -->|"explicit setup or disconnect"| CFG["~/.claude.json\n~/.claude/mcp.json\n~/.cursor/mcp.json\n~/.config/mcp/mcp.json"] ``` @@ -373,11 +373,12 @@ flowchart LR | Function | Purpose | |----------|---------| | `spawn_ws_bridge(vault_path)` | Spawns `ws-bridge.js` as child process with VAULT_PATH env | +| `sync_mcp_bridge_vault(vault_path?)` | Starts, restarts, or stops the desktop WebSocket bridge as the selected vault changes | | `register_mcp(vault_path)` | Verifies Node.js, resolves the packaged `mcp-server/`, and writes Tolaria's explicit stdio entry to Claude Code, Cursor, and generic MCP configs on user request | | `remove_mcp()` | Removes Tolaria's MCP entry from Claude Code, Cursor, and generic MCP configs | | `upsert_mcp_config(path, entry)` | Atomic config file update (create/merge, preserves others) | -The `WsBridgeChild` state wrapper in `lib.rs` ensures the bridge process is killed on app exit via `RunEvent::Exit` handler. The same desktop layer now keeps the Tauri asset protocol scoped to the active vault instead of every filesystem path. +The `WsBridgeChild` state wrapper in `lib.rs` ensures the bridge process is replaced on vault switches, stopped when no active vault is selected, and killed on app exit via the `RunEvent::Exit` handler. The same desktop layer now keeps the Tauri asset protocol scoped to the active vault instead of every filesystem path. ## Search @@ -723,6 +724,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules: | `register_mcp_tools` | Register MCP in Claude/Cursor/generic config for the active vault | | `remove_mcp_tools` | Remove Tolaria's MCP entry from Claude/Cursor/generic config | | `check_mcp_status` | Check whether the active vault is explicitly registered in Claude/Cursor/generic config | +| `sync_mcp_bridge_vault` | Sync the desktop WebSocket bridge process to the selected vault, or stop it when no vault is selected | The desktop MCP WebSocket bridge is intentionally local-only. `mcp-server/ws-bridge.js` binds both bridge ports to loopback, rejects non-loopback clients, accepts browser/Tauri origins only on the UI bridge, and rejects browser-origin requests on the tool bridge so remote pages cannot drive vault tools directly. diff --git a/mcp-server/index.js b/mcp-server/index.js index 7a028afe..6d887bd6 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -21,8 +21,9 @@ import { } from '@modelcontextprotocol/sdk/types.js' import WebSocket from 'ws' import { searchNotes, getNote, vaultContext } from './vault.js' +import { requireVaultPath } from './vault-path.js' -const VAULT_PATH = process.env.VAULT_PATH || process.env.HOME + '/Laputa' +const VAULT_PATH = requireVaultPath() const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10) const WS_UI_URL = `ws://localhost:${WS_UI_PORT}` diff --git a/mcp-server/test.js b/mcp-server/test.js index cc9755f2..ed5a0cba 100644 --- a/mcp-server/test.js +++ b/mcp-server/test.js @@ -6,6 +6,7 @@ import os from 'node:os' import { findMarkdownFiles, getNote, searchNotes, vaultContext, } from './vault.js' +import { requireVaultPath } from './vault-path.js' import { evaluateBridgeRequest } from './ws-bridge.js' let tmpDir @@ -191,6 +192,22 @@ describe('evaluateBridgeRequest', () => { }) }) +describe('requireVaultPath', () => { + it('returns the explicit configured vault path', () => { + assert.equal( + requireVaultPath({ VAULT_PATH: '/tmp/Selected Vault' }), + '/tmp/Selected Vault', + ) + }) + + it('rejects missing vault paths instead of falling back to ~/Laputa', () => { + assert.throws( + () => requireVaultPath({}), + /VAULT_PATH is required/, + ) + }) +}) + async function assertRejectsOutsideVault(prefix, resolveNotePath) { const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)) const outsideNote = path.join(outsideDir, 'outside.md') diff --git a/mcp-server/vault-path.js b/mcp-server/vault-path.js new file mode 100644 index 00000000..ffe3964f --- /dev/null +++ b/mcp-server/vault-path.js @@ -0,0 +1,7 @@ +export function requireVaultPath(env = process.env) { + const vaultPath = env.VAULT_PATH?.trim() + if (!vaultPath) { + throw new Error('VAULT_PATH is required. Open a vault in Tolaria before starting MCP tools.') + } + return vaultPath +} diff --git a/mcp-server/ws-bridge.js b/mcp-server/ws-bridge.js index 855e9d61..3cb814f0 100644 --- a/mcp-server/ws-bridge.js +++ b/mcp-server/ws-bridge.js @@ -24,8 +24,8 @@ import { WebSocketServer } from 'ws' import { getNote, searchNotes, vaultContext, } from './vault.js' +import { requireVaultPath } from './vault-path.js' -const VAULT_PATH = process.env.VAULT_PATH || process.env.HOME + '/Laputa' const WS_PORT = parseInt(process.env.WS_PORT || '9710', 10) const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10) const LOOPBACK_HOST = 'localhost' @@ -37,6 +37,12 @@ const TRUSTED_UI_ORIGINS = new Set([ /** @type {WebSocketServer | null} */ let uiBridge = null +let vaultPath = null + +function activeVaultPath() { + vaultPath ??= requireVaultPath() + return vaultPath +} function broadcastUiAction(action, payload) { if (!uiBridge) return @@ -48,10 +54,10 @@ function broadcastUiAction(action, payload) { const TOOL_HANDLERS = { - open_note: (args) => getNote(VAULT_PATH, args.path).then(note => ({ content: note.content, frontmatter: note.frontmatter })), - read_note: (args) => getNote(VAULT_PATH, args.path).then(note => ({ content: note.content, frontmatter: note.frontmatter })), - search_notes: (args) => searchNotes(VAULT_PATH, args.query, args.limit), - vault_context: () => vaultContext(VAULT_PATH), + open_note: (args) => getNote(activeVaultPath(), args.path).then(note => ({ content: note.content, frontmatter: note.frontmatter })), + read_note: (args) => getNote(activeVaultPath(), args.path).then(note => ({ content: note.content, frontmatter: note.frontmatter })), + search_notes: (args) => searchNotes(activeVaultPath(), args.query, args.limit), + vault_context: () => vaultContext(activeVaultPath()), ui_open_note: (args) => { broadcastUiAction('vault_changed', { path: args.path }); broadcastUiAction('open_note', { path: args.path }); return { ok: true } }, ui_open_tab: (args) => { broadcastUiAction('vault_changed', { path: args.path }); broadcastUiAction('open_tab', { path: args.path }); return { ok: true } }, ui_highlight: (args) => { broadcastUiAction('highlight', { element: args.element, path: args.path }); return { ok: true } }, @@ -164,6 +170,7 @@ export function startUiBridge(port = WS_UI_PORT) { } export function startBridge(port = WS_PORT) { + const currentVaultPath = activeVaultPath() const wss = new WebSocketServer({ port, host: LOOPBACK_HOST, @@ -171,7 +178,7 @@ export function startBridge(port = WS_PORT) { }) wss.on('connection', (ws) => { - console.error(`[ws-bridge] Client connected (vault: ${VAULT_PATH})`) + console.error(`[ws-bridge] Client connected (vault: ${currentVaultPath})`) ws.on('message', async (raw) => { try { @@ -192,5 +199,11 @@ export function startBridge(port = WS_PORT) { // Run directly if invoked as main module const isMain = process.argv[1]?.endsWith('ws-bridge.js') if (isMain) { - startUiBridge().then(() => startBridge()) + try { + activeVaultPath() + startUiBridge().then(() => startBridge()) + } catch (err) { + console.error(`[ws-bridge] ${err.message}`) + process.exit(1) + } } diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 6249e186..5d97c925 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -131,6 +131,22 @@ pub async fn check_mcp_status(vault_path: String) -> Result, +) -> Result { + 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) -> Result { + Err("MCP is not available on mobile".into()) +} + // ── Menu commands ─────────────────────────────────────────────────────────── #[derive(Debug, Deserialize)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4af86e70..ec107d77 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -102,6 +102,78 @@ fn log_startup_result(label: &str, result: Result) { } } +#[cfg(desktop)] +fn selected_mcp_bridge_vault_path(vault_list: &vault_list::VaultList) -> Option { + 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 { + 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) { + 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() { diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index 7bd90472..e4b8eb28 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -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 { +pub fn spawn_ws_bridge(vault_path: impl AsRef) -> Result { 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 { .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) } diff --git a/src/hooks/useVaultSwitcher.test.ts b/src/hooks/useVaultSwitcher.test.ts index 0609c388..5dc89769 100644 --- a/src/hooks/useVaultSwitcher.test.ts +++ b/src/hooks/useVaultSwitcher.test.ts @@ -15,6 +15,7 @@ const mockInvokeFn = vi.fn((cmd: string, args?: Record): Promis return Promise.resolve(null) } if (cmd === 'get_default_vault_path') return Promise.resolve(mockDefaultVaultPath) + if (cmd === 'sync_mcp_bridge_vault') return Promise.resolve(args?.vaultPath ? 'started' : 'stopped') if (cmd === 'check_vault_exists') return Promise.resolve(true) return Promise.resolve(null) }) @@ -56,6 +57,7 @@ describe('useVaultSwitcher', () => { return Promise.resolve(null) } if (cmd === 'get_default_vault_path') return Promise.resolve(mockDefaultVaultPath) + if (cmd === 'sync_mcp_bridge_vault') return Promise.resolve(args?.vaultPath ? 'started' : 'stopped') if (cmd === 'check_vault_exists') { const checkVaultExists = overrides.checkVaultExists return Promise.resolve(typeof checkVaultExists === 'function' @@ -228,6 +230,46 @@ describe('useVaultSwitcher', () => { expect(mockVaultListStore.active_vault).toBeNull() }) + it('stops the MCP bridge when there is no selected active vault', async () => { + await renderLoadedVaultSwitcher() + + await waitFor(() => { + expect(mockInvokeFn).toHaveBeenCalledWith('sync_mcp_bridge_vault', { vaultPath: null }) + }) + }) + + it('syncs the MCP bridge to a persisted active vault', async () => { + mockVaultListStore = { + vaults: [{ label: 'Work', path: '/work/vault' }], + active_vault: '/work/vault', + hidden_defaults: [], + } + + await renderLoadedVaultSwitcher() + + await waitFor(() => { + expect(mockInvokeFn).toHaveBeenCalledWith('sync_mcp_bridge_vault', { vaultPath: '/work/vault' }) + }) + }) + + it('syncs the MCP bridge after switching vaults', async () => { + mockVaultListStore = { + vaults: [{ label: 'Work', path: '/work/vault' }], + active_vault: null, + hidden_defaults: [], + } + + const { result } = await renderLoadedVaultSwitcher() + + act(() => { + result.current.switchVault('/work/vault') + }) + + await waitFor(() => { + expect(mockInvokeFn).toHaveBeenCalledWith('sync_mcp_bridge_vault', { vaultPath: '/work/vault' }) + }) + }) + it('keeps the implicit default vault out of the list when its path is missing', async () => { setMockInvokeBehavior({ checkVaultExists: false }) diff --git a/src/hooks/useVaultSwitcher.ts b/src/hooks/useVaultSwitcher.ts index 4cb440f1..15d14efc 100644 --- a/src/hooks/useVaultSwitcher.ts +++ b/src/hooks/useVaultSwitcher.ts @@ -162,6 +162,17 @@ function syncDefaultVaultExport(path: string) { DEFAULT_VAULTS[0] = { label: GETTING_STARTED_LABEL, path } } +function selectedBridgeVaultPath(selectedVaultPath: string | null): string | null { + const path = selectedVaultPath?.trim() + return path ? path : null +} + +async function syncMcpBridgeVault(selectedVaultPath: string | null): Promise { + await tauriCall('sync_mcp_bridge_vault', { + vaultPath: selectedBridgeVaultPath(selectedVaultPath), + }) +} + function isCanonicalGettingStartedPath(path: string, resolvedDefaultPath: string): boolean { return path === resolvedDefaultPath } @@ -522,6 +533,16 @@ function usePersistedVaultState(onSwitchRef: MutableRefObject<() => void>): Pers } } +function useMcpBridgeVaultSync(loaded: boolean, selectedVaultPath: string | null) { + useEffect(() => { + if (!loaded) return + + syncMcpBridgeVault(selectedVaultPath).catch(err => { + console.warn('Failed to sync MCP bridge vault:', err) + }) + }, [loaded, selectedVaultPath]) +} + function formatGettingStartedRestoreError(err: unknown): string { const message = typeof err === 'string' @@ -1145,6 +1166,8 @@ export function useVaultSwitcher({ onSwitch, onToast }: UseVaultSwitcherOptions) selectedVaultPath, vaultPath, } = persistedState + useMcpBridgeVaultSync(loaded, selectedVaultPath) + const { allVaults, defaultVaults, isGettingStartedHidden } = useVaultCollections( defaultAvailable, defaultPath, diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts index 3ec6fe3d..dbf0aa47 100644 --- a/src/mock-tauri/mock-handlers.ts +++ b/src/mock-tauri/mock-handlers.ts @@ -486,6 +486,7 @@ export const mockHandlers: Record any> = { }, register_mcp_tools: () => 'registered', check_mcp_status: () => 'installed', + sync_mcp_bridge_vault: (args: { vaultPath?: string | null }) => args.vaultPath ? 'started' : 'stopped', repair_vault: (): string => { mockVaultAiGuidanceStatus = { agents_state: 'managed',