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:
lucaronin
2026-03-04 13:11:58 +01:00
parent 1f1e115d70
commit 4dbcc7c298
10 changed files with 424 additions and 5 deletions

View File

@@ -433,6 +433,13 @@ async fn register_mcp_tools(vault_path: String) -> Result<String, String> {
.map_err(|e| format!("Registration task failed: {e}"))?
}
#[tauri::command]
async fn check_mcp_status() -> Result<mcp::McpStatus, String> {
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<Vec<ThemeFile>, 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,

View File

@@ -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<PathBuf, String> {
let output = Command::new("which")
@@ -144,6 +157,53 @@ fn upsert_mcp_config(config_path: &Path, entry: &serde_json::Value) -> Result<bo
Ok(was_update)
}
/// Check whether the MCP server is properly installed and registered.
///
/// Returns `Installed` when the laputa entry exists in `~/.claude/mcp.json`
/// and the referenced index.js file is present. Returns `NoClaudeCli` when
/// the Claude CLI binary cannot be found. Otherwise returns `NotInstalled`.
pub fn check_mcp_status() -> 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""#);
}
}