From d3ea6326730195137a419ee250e2ed19893da05e Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 4 Mar 2026 13:11:58 +0100 Subject: [PATCH] feat: detect MCP server status and show warning in status bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add check_mcp_status Tauri command that detects whether the MCP server is installed, Claude CLI is missing, or config needs setup. The status bar shows a warning badge (MCP ⚠) when not installed, clickable to trigger install. Also available via command palette "Install MCP Server". Replaces useMcpRegistration with useMcpStatus which combines detection and registration in a single hook. Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/lib.rs | 8 ++ src-tauri/src/mcp.rs | 82 ++++++++++++++++ src/App.tsx | 8 +- src/components/StatusBar.test.tsx | 55 +++++++++++ src/components/StatusBar.tsx | 43 ++++++++- src/hooks/useAppCommands.ts | 4 + src/hooks/useCommandRegistry.ts | 5 + src/hooks/useMcpStatus.test.ts | 151 ++++++++++++++++++++++++++++++ src/hooks/useMcpStatus.ts | 72 ++++++++++++++ src/mock-tauri/mock-handlers.ts | 1 + 10 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 src/hooks/useMcpStatus.test.ts create mode 100644 src/hooks/useMcpStatus.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fd886741..963286a0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -433,6 +433,13 @@ async fn register_mcp_tools(vault_path: String) -> Result { .map_err(|e| format!("Registration task failed: {e}"))? } +#[tauri::command] +async fn check_mcp_status() -> Result { + tokio::task::spawn_blocking(mcp::check_mcp_status) + .await + .map_err(|e| format!("MCP status check failed: {e}")) +} + #[tauri::command] fn list_themes(vault_path: String) -> Result, String> { let vault_path = expand_tilde(&vault_path); @@ -670,6 +677,7 @@ pub fn run() { check_vault_exists, get_default_vault_path, register_mcp_tools, + check_mcp_status, list_themes, get_theme, get_vault_settings, diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index ee3bd2ed..1015cf8f 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -1,6 +1,19 @@ +use serde::Serialize; use std::path::{Path, PathBuf}; use std::process::{Child, Command}; +/// Status of the MCP server installation. +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum McpStatus { + /// MCP is registered in Claude config and server files exist. + Installed, + /// MCP server files or config are missing but can be installed. + NotInstalled, + /// Claude CLI is not installed — must install that first. + NoClaudeCli, +} + /// Find the `node` binary path at runtime. pub(crate) fn find_node() -> Result { let output = Command::new("which") @@ -144,6 +157,53 @@ fn upsert_mcp_config(config_path: &Path, entry: &serde_json::Value) -> Result McpStatus { + // Check Claude CLI first — no point installing MCP if Claude isn't available + if crate::claude_cli::find_claude_binary().is_err() { + return McpStatus::NoClaudeCli; + } + + let config_path = match dirs::home_dir() { + Some(h) => h.join(".claude").join("mcp.json"), + None => return McpStatus::NotInstalled, + }; + + if !config_path.exists() { + return McpStatus::NotInstalled; + } + + let raw = match std::fs::read_to_string(&config_path) { + Ok(r) => r, + Err(_) => return McpStatus::NotInstalled, + }; + + let config: serde_json::Value = match serde_json::from_str(&raw) { + Ok(c) => c, + Err(_) => return McpStatus::NotInstalled, + }; + + let entry = &config["mcpServers"]["laputa"]; + if entry.is_null() { + return McpStatus::NotInstalled; + } + + // Verify the referenced index.js actually exists on disk + if let Some(index_js) = entry["args"].as_array().and_then(|a| a.first()).and_then(|v| v.as_str()) { + if !Path::new(index_js).exists() { + return McpStatus::NotInstalled; + } + } else { + return McpStatus::NotInstalled; + } + + McpStatus::Installed +} + #[cfg(test)] mod tests { use super::*; @@ -314,4 +374,26 @@ mod tests { // With empty config list, there were no updates, so status should be "registered" assert_eq!(status, "registered"); } + + #[test] + fn check_mcp_status_returns_valid_variant() { + // On a dev machine with Claude CLI and MCP registered, this should be Installed. + // On CI without Claude it might be NoClaudeCli. Either way it must not panic. + let status = check_mcp_status(); + assert!( + matches!(status, McpStatus::Installed | McpStatus::NotInstalled | McpStatus::NoClaudeCli), + "unexpected status: {:?}", + status + ); + } + + #[test] + fn mcp_status_serializes_to_snake_case() { + let json = serde_json::to_string(&McpStatus::Installed).unwrap(); + assert_eq!(json, r#""installed""#); + let json = serde_json::to_string(&McpStatus::NotInstalled).unwrap(); + assert_eq!(json, r#""not_installed""#); + let json = serde_json::to_string(&McpStatus::NoClaudeCli).unwrap(); + assert_eq!(json, r#""no_claude_cli""#); + } } diff --git a/src/App.tsx b/src/App.tsx index 2fa9347b..99892068 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import { StatusBar } from './components/StatusBar' import { SettingsPanel } from './components/SettingsPanel' import { GitHubVaultModal } from './components/GitHubVaultModal' import { WelcomeScreen } from './components/WelcomeScreen' -import { useMcpRegistration } from './hooks/useMcpRegistration' +import { useMcpStatus } from './hooks/useMcpStatus' import { useVaultLoader } from './hooks/useVaultLoader' import { useSettings } from './hooks/useSettings' import { useNoteActions } from './hooks/useNoteActions' @@ -103,7 +103,7 @@ function App() { const { settings, saveSettings } = useSettings() const themeManager = useThemeManager(resolvedPath, vault.entries, vault.allContent) - useMcpRegistration(resolvedPath, setToastMessage) + const { mcpStatus, installMcp } = useMcpStatus(resolvedPath, setToastMessage) const autoSync = useAutoSync({ vaultPath: resolvedPath, @@ -384,6 +384,8 @@ function App() { onRestoreGettingStarted: vaultSwitcher.restoreGettingStarted, isGettingStartedHidden: vaultSwitcher.isGettingStartedHidden, vaultCount: vaultSwitcher.allVaults.length, + mcpStatus, + onInstallMcp: installMcp, }) const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null @@ -484,7 +486,7 @@ function App() { - setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} lastCommitInfo={autoSync.lastCommitInfo} onTriggerSync={autoSync.triggerSync} onOpenConflictResolver={handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} indexingProgress={indexing.progress} onRetryIndexing={indexing.retryIndexing} onRemoveVault={vaultSwitcher.removeVault} /> + setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} lastCommitInfo={autoSync.lastCommitInfo} onTriggerSync={autoSync.triggerSync} onOpenConflictResolver={handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} indexingProgress={indexing.progress} onRetryIndexing={indexing.retryIndexing} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={installMcp} /> setToastMessage(null)} /> diff --git a/src/components/StatusBar.test.tsx b/src/components/StatusBar.test.tsx index 23628b13..0452b23d 100644 --- a/src/components/StatusBar.test.tsx +++ b/src/components/StatusBar.test.tsx @@ -339,4 +339,59 @@ describe('StatusBar', () => { ) expect(screen.getByText('Installing search…')).toBeInTheDocument() }) + + it('shows MCP warning badge when status is not_installed', () => { + render( + + ) + expect(screen.getByTestId('status-mcp')).toBeInTheDocument() + expect(screen.getByTitle('MCP server not installed — click to install')).toBeInTheDocument() + }) + + it('shows MCP warning badge when status is no_claude_cli', () => { + render( + + ) + expect(screen.getByTestId('status-mcp')).toBeInTheDocument() + expect(screen.getByTitle('Claude CLI not found — install it first')).toBeInTheDocument() + }) + + it('hides MCP badge when status is installed', () => { + render( + + ) + expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument() + }) + + it('hides MCP badge when status is checking', () => { + render( + + ) + expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument() + }) + + it('hides MCP badge when no mcpStatus prop provided', () => { + render( + + ) + expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument() + }) + + it('calls onInstallMcp when clicking MCP badge with not_installed status', () => { + const onInstallMcp = vi.fn() + render( + + ) + fireEvent.click(screen.getByTestId('status-mcp')) + expect(onInstallMcp).toHaveBeenCalledOnce() + }) + + it('does not call onInstallMcp when clicking MCP badge with no_claude_cli status', () => { + const onInstallMcp = vi.fn() + render( + + ) + fireEvent.click(screen.getByTestId('status-mcp')) + expect(onInstallMcp).not.toHaveBeenCalled() + }) }) diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 26822c37..7dd1cb59 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,7 +1,8 @@ import { useState, useRef, useEffect } from 'react' -import { Package, RefreshCw, FileText, Bell, Settings, FolderOpen, Check, Github, CircleDot, AlertTriangle, Loader2, GitCommitHorizontal, Search, X } from 'lucide-react' +import { Package, RefreshCw, FileText, Bell, Settings, FolderOpen, Check, Github, CircleDot, AlertTriangle, Loader2, GitCommitHorizontal, Search, X, Cpu } from 'lucide-react' import type { LastCommitInfo, SyncStatus } from '../types' import type { IndexingProgress } from '../hooks/useIndexing' +import type { McpStatus } from '../hooks/useMcpStatus' import { openExternalUrl } from '../utils/url' export interface VaultOption { @@ -34,6 +35,8 @@ interface StatusBarProps { indexingProgress?: IndexingProgress onRetryIndexing?: () => void onRemoveVault?: (path: string) => void + mcpStatus?: McpStatus + onInstallMcp?: () => void } function VaultMenuIcon({ isActive, unavailable }: { isActive: boolean; unavailable: boolean }) { @@ -291,7 +294,42 @@ function PendingBadge({ count, onClick }: { count: number; onClick?: () => void ) } -export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onConnectGitHub, onClickPending, hasGitHub, syncStatus = 'idle', lastSyncTime = null, conflictCount = 0, lastCommitInfo, onTriggerSync, onOpenConflictResolver, zoomLevel = 100, onZoomReset, buildNumber, onCheckForUpdates, indexingProgress, onRetryIndexing, onRemoveVault }: StatusBarProps) { +const MCP_TOOLTIPS: Record = { + not_installed: 'MCP server not installed — click to install', + no_claude_cli: 'Claude CLI not found — install it first', +} + +function McpBadge({ status, onInstall }: { status: McpStatus; onInstall?: () => void }) { + if (status === 'installed' || status === 'checking') return null + const tooltip = MCP_TOOLTIPS[status] ?? 'MCP status unknown' + const clickable = status === 'not_installed' && !!onInstall + return ( + <> + | + { e.currentTarget.style.background = 'var(--hover)' } : undefined} + onMouseLeave={clickable ? (e) => { e.currentTarget.style.background = 'transparent' } : undefined} + > + MCP + + + + ) +} + +export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onConnectGitHub, onClickPending, hasGitHub, syncStatus = 'idle', lastSyncTime = null, conflictCount = 0, lastCommitInfo, onTriggerSync, onOpenConflictResolver, zoomLevel = 100, onZoomReset, buildNumber, onCheckForUpdates, indexingProgress, onRetryIndexing, onRemoveVault, mcpStatus, onInstallMcp }: StatusBarProps) { const [, setTick] = useState(0) useEffect(() => { const id = setInterval(() => setTick((t) => t + 1), 30_000) @@ -318,6 +356,7 @@ export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onS {indexingProgress && } + {mcpStatus && }
{noteCount.toLocaleString()} notes diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts index e416ba71..baf6fc57 100644 --- a/src/hooks/useAppCommands.ts +++ b/src/hooks/useAppCommands.ts @@ -64,6 +64,8 @@ interface AppCommandsConfig { onRestoreGettingStarted?: () => void isGettingStartedHidden?: boolean vaultCount?: number + mcpStatus?: string + onInstallMcp?: () => void } /** Sets up keyboard shortcuts, command registry, menu events, and keyboard navigation. */ @@ -172,6 +174,8 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onRestoreGettingStarted: config.onRestoreGettingStarted, isGettingStartedHidden: config.isGettingStartedHidden, vaultCount: config.vaultCount, + mcpStatus: config.mcpStatus, + onInstallMcp: config.onInstallMcp, }) useKeyboardNavigation({ diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts index 6aacd259..333b8c02 100644 --- a/src/hooks/useCommandRegistry.ts +++ b/src/hooks/useCommandRegistry.ts @@ -19,6 +19,8 @@ interface CommandRegistryConfig { entries: VaultEntry[] modifiedCount: number conflictCount?: number + mcpStatus?: string + onInstallMcp?: () => void onQuickOpen: () => void onCreateNote: () => void @@ -193,6 +195,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction onCheckForUpdates, onCreateType, onRemoveActiveVault, onRestoreGettingStarted, isGettingStartedHidden, vaultCount, + mcpStatus, onInstallMcp, } = config const hasActiveNote = activeTabPath !== null @@ -252,6 +255,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction { id: 'remove-vault', label: 'Remove Vault from List', group: 'Settings', keywords: ['vault', 'remove', 'disconnect', 'hide'], enabled: (vaultCount ?? 0) > 1 && !!onRemoveActiveVault, execute: () => onRemoveActiveVault?.() }, { id: 'restore-getting-started', label: 'Restore Getting Started Vault', group: 'Settings', keywords: ['vault', 'restore', 'demo', 'getting started', 'reset'], enabled: !!isGettingStartedHidden && !!onRestoreGettingStarted, execute: () => onRestoreGettingStarted?.() }, { id: 'check-updates', label: 'Check for Updates', group: 'Settings', keywords: ['update', 'version', 'upgrade', 'release'], enabled: true, execute: () => onCheckForUpdates?.() }, + { id: 'install-mcp', label: 'Install MCP Server', group: 'Settings', keywords: ['mcp', 'claude', 'ai', 'tools', 'install'], enabled: mcpStatus === 'not_installed' && !!onInstallMcp, execute: () => onInstallMcp?.() }, // Type-aware: "New [Type]" and "List [Type]" ...buildTypeCommands(vaultTypes, onCreateNoteOfType, onSelect), @@ -269,5 +273,6 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction onGoBack, onGoForward, canGoBack, canGoForward, vaultTypes, themes, activeThemeId, onSwitchTheme, onCreateTheme, onOpenTheme, onRemoveActiveVault, onRestoreGettingStarted, isGettingStartedHidden, vaultCount, + mcpStatus, onInstallMcp, ]) } diff --git a/src/hooks/useMcpStatus.test.ts b/src/hooks/useMcpStatus.test.ts new file mode 100644 index 00000000..6e2ba2c2 --- /dev/null +++ b/src/hooks/useMcpStatus.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook, act, waitFor } from '@testing-library/react' +import { useMcpStatus } from './useMcpStatus' + +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})) + +vi.mock('../mock-tauri', () => ({ + isTauri: () => false, + mockInvoke: vi.fn(), +})) + +const { mockInvoke } = await import('../mock-tauri') as { mockInvoke: ReturnType } + +describe('useMcpStatus', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('starts in checking state and resolves to installed', async () => { + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('installed') + if (cmd === 'register_mcp_tools') return Promise.resolve('updated') + return Promise.resolve(null) + }) + + const onToast = vi.fn() + const { result } = renderHook(() => useMcpStatus('/vault', onToast)) + + expect(result.current.mcpStatus).toBe('checking') + + await waitFor(() => { + expect(result.current.mcpStatus).toBe('installed') + }) + }) + + it('resolves to not_installed when check returns not_installed', async () => { + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('not_installed') + if (cmd === 'register_mcp_tools') return Promise.reject(new Error('fail')) + return Promise.resolve(null) + }) + + const onToast = vi.fn() + const { result } = renderHook(() => useMcpStatus('/vault', onToast)) + + await waitFor(() => { + expect(result.current.mcpStatus).toBe('not_installed') + }) + }) + + it('resolves to no_claude_cli when check returns no_claude_cli', async () => { + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('no_claude_cli') + if (cmd === 'register_mcp_tools') return Promise.reject(new Error('no cli')) + return Promise.resolve(null) + }) + + const onToast = vi.fn() + const { result } = renderHook(() => useMcpStatus('/vault', onToast)) + + await waitFor(() => { + expect(result.current.mcpStatus).toBe('no_claude_cli') + }) + }) + + it('install action calls register_mcp_tools and updates status', async () => { + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('not_installed') + if (cmd === 'register_mcp_tools') return Promise.resolve('registered') + return Promise.resolve(null) + }) + + const onToast = vi.fn() + const { result } = renderHook(() => useMcpStatus('/vault', onToast)) + + await waitFor(() => { + expect(result.current.mcpStatus).toBe('installed') + }) + + // Reset to test install action directly + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('not_installed') + if (cmd === 'register_mcp_tools') return Promise.resolve('registered') + return Promise.resolve(null) + }) + + await act(async () => { + await result.current.installMcp() + }) + + expect(result.current.mcpStatus).toBe('installed') + expect(onToast).toHaveBeenCalledWith('MCP server installed successfully') + }) + + it('install action shows error toast on failure', async () => { + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('not_installed') + if (cmd === 'register_mcp_tools') return Promise.reject(new Error('disk full')) + return Promise.resolve(null) + }) + + const onToast = vi.fn() + const { result } = renderHook(() => useMcpStatus('/vault', onToast)) + + await waitFor(() => { + expect(result.current.mcpStatus).toBe('not_installed') + }) + + await act(async () => { + await result.current.installMcp() + }) + + expect(result.current.mcpStatus).toBe('not_installed') + expect(onToast).toHaveBeenCalledWith(expect.stringContaining('MCP install failed')) + }) + + it('shows toast when registered for the first time', async () => { + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('installed') + if (cmd === 'register_mcp_tools') return Promise.resolve('registered') + return Promise.resolve(null) + }) + + const onToast = vi.fn() + renderHook(() => useMcpStatus('/vault', onToast)) + + await waitFor(() => { + expect(onToast).toHaveBeenCalledWith('Laputa registered as MCP tool for Claude Code') + }) + }) + + it('does not show toast when already registered', async () => { + mockInvoke.mockImplementation((cmd: string) => { + if (cmd === 'check_mcp_status') return Promise.resolve('installed') + if (cmd === 'register_mcp_tools') return Promise.resolve('updated') + return Promise.resolve(null) + }) + + const onToast = vi.fn() + renderHook(() => useMcpStatus('/vault', onToast)) + + await waitFor(() => { + expect(mockInvoke).toHaveBeenCalledWith('register_mcp_tools', { vaultPath: '/vault' }) + }) + + // 'updated' should not trigger a toast + expect(onToast).not.toHaveBeenCalledWith('Laputa registered as MCP tool for Claude Code') + }) +}) diff --git a/src/hooks/useMcpStatus.ts b/src/hooks/useMcpStatus.ts new file mode 100644 index 00000000..c95dbcb1 --- /dev/null +++ b/src/hooks/useMcpStatus.ts @@ -0,0 +1,72 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { invoke } from '@tauri-apps/api/core' +import { isTauri, mockInvoke } from '../mock-tauri' + +export type McpStatus = 'checking' | 'installed' | 'not_installed' | 'no_claude_cli' + +function tauriCall(command: string, args?: Record): Promise { + return isTauri() ? invoke(command, args) : mockInvoke(command, args) +} + +/** + * Detects MCP server status on vault open and provides an install action. + * + * Combines the old `useMcpRegistration` functionality (auto-register on vault + * switch) with new status detection for the status bar indicator. + */ +export function useMcpStatus( + vaultPath: string, + onToast: (msg: string) => void, +) { + const [status, setStatus] = useState('checking') + const registeredRef = useRef(null) + const onToastRef = useRef(onToast) + useEffect(() => { onToastRef.current = onToast }) + + // Check MCP status on vault open / vault switch + useEffect(() => { + let cancelled = false + setStatus('checking') // eslint-disable-line react-hooks/set-state-in-effect -- reset to checking on vault switch + + tauriCall('check_mcp_status') + .then((result) => { + if (!cancelled) setStatus(result as McpStatus) + }) + .catch(() => { + if (!cancelled) setStatus('not_installed') + }) + + return () => { cancelled = true } + }, [vaultPath]) + + // Auto-register on vault switch (preserves old useMcpRegistration behavior) + useEffect(() => { + if (registeredRef.current === vaultPath) return + registeredRef.current = vaultPath + + tauriCall('register_mcp_tools', { vaultPath }) + .then((result) => { + if (result === 'registered') { + onToastRef.current('Laputa registered as MCP tool for Claude Code') + } + setStatus('installed') + }) + .catch(() => { + // Non-critical — status check will show the right state + }) + }, [vaultPath]) + + const install = useCallback(async () => { + setStatus('checking') + try { + await tauriCall('register_mcp_tools', { vaultPath }) + setStatus('installed') + onToastRef.current('MCP server installed successfully') + } catch (e) { + setStatus('not_installed') + onToastRef.current(`MCP install failed: ${e}`) + } + }, [vaultPath]) + + return { mcpStatus: status, installMcp: install } +} diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts index eba206ef..efc783b2 100644 --- a/src/mock-tauri/mock-handlers.ts +++ b/src/mock-tauri/mock-handlers.ts @@ -264,6 +264,7 @@ export const mockHandlers: Record any> = { }, create_getting_started_vault: () => '/Users/mock/Documents/Getting Started', register_mcp_tools: () => 'registered', + check_mcp_status: () => 'installed', get_index_status: () => ({ available: true, qmd_installed: true, collection_exists: true, indexed_count: 100, embedded_count: 80, pending_embed: 0 }), start_indexing: () => null, trigger_incremental_index: () => null,