feat: detect MCP server status and show warning in status bar
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 <noreply@anthropic.com>
This commit is contained in:
@@ -339,4 +339,59 @@ describe('StatusBar', () => {
|
||||
)
|
||||
expect(screen.getByText('Installing search…')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows MCP warning badge when status is not_installed', () => {
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} mcpStatus="not_installed" />
|
||||
)
|
||||
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(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} mcpStatus="no_claude_cli" />
|
||||
)
|
||||
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(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} mcpStatus="installed" />
|
||||
)
|
||||
expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides MCP badge when status is checking', () => {
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} mcpStatus="checking" />
|
||||
)
|
||||
expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides MCP badge when no mcpStatus prop provided', () => {
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} />
|
||||
)
|
||||
expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onInstallMcp when clicking MCP badge with not_installed status', () => {
|
||||
const onInstallMcp = vi.fn()
|
||||
render(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} mcpStatus="not_installed" onInstallMcp={onInstallMcp} />
|
||||
)
|
||||
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(
|
||||
<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} mcpStatus="no_claude_cli" onInstallMcp={onInstallMcp} />
|
||||
)
|
||||
fireEvent.click(screen.getByTestId('status-mcp'))
|
||||
expect(onInstallMcp).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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 (
|
||||
<>
|
||||
<span style={SEP_STYLE}>|</span>
|
||||
<span
|
||||
role={clickable ? 'button' : undefined}
|
||||
onClick={clickable ? onInstall : undefined}
|
||||
style={{
|
||||
...ICON_STYLE,
|
||||
color: 'var(--accent-orange)',
|
||||
cursor: clickable ? 'pointer' : 'default',
|
||||
padding: '2px 4px',
|
||||
borderRadius: 3,
|
||||
background: 'transparent',
|
||||
}}
|
||||
title={tooltip}
|
||||
data-testid="status-mcp"
|
||||
onMouseEnter={clickable ? (e) => { e.currentTarget.style.background = 'var(--hover)' } : undefined}
|
||||
onMouseLeave={clickable ? (e) => { e.currentTarget.style.background = 'transparent' } : undefined}
|
||||
>
|
||||
<Cpu size={13} />MCP
|
||||
<AlertTriangle size={10} style={{ marginLeft: 2 }} />
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
<ConflictBadge count={conflictCount} onClick={onOpenConflictResolver} />
|
||||
<PendingBadge count={modifiedCount} onClick={onClickPending} />
|
||||
{indexingProgress && <IndexingBadge progress={indexingProgress} onRetry={onRetryIndexing} />}
|
||||
{mcpStatus && <McpBadge status={mcpStatus} onInstall={onInstallMcp} />}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span style={ICON_STYLE}><FileText size={13} />{noteCount.toLocaleString()} notes</span>
|
||||
|
||||
Reference in New Issue
Block a user