Files
ajarbot/obsidian_mcp.py

169 lines
4.9 KiB
Python
Raw Permalink Normal View History

"""Obsidian MCP Server Integration.
Manages the external obsidian-mcp-server process and provides
health checking, fallback routing, and configuration loading.
"""
import os
import time
import threading
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
import httpx
# Default config path
_CONFIG_FILE = Path("config/obsidian_mcp.yaml")
# Cached state
_obsidian_healthy: bool = False
_last_health_check: float = 0.0
_health_lock = threading.Lock()
_config_cache: Optional[Dict] = None
def load_obsidian_config() -> Dict[str, Any]:
"""Load Obsidian MCP configuration with env var overrides."""
global _config_cache
if _config_cache is not None:
return _config_cache
config = {}
if _CONFIG_FILE.exists():
with open(_CONFIG_FILE, encoding="utf-8") as f:
config = yaml.safe_load(f) or {}
obsidian = config.get("obsidian_mcp", {})
# Apply environment variable overrides
env_overrides = {
"OBSIDIAN_API_KEY": ("connection", "api_key"),
"OBSIDIAN_BASE_URL": ("connection", "base_url"),
"OBSIDIAN_MCP_ENABLED": None, # Special: top-level "enabled"
"OBSIDIAN_ROUTING_STRATEGY": ("routing", "strategy"),
"OBSIDIAN_VAULT_PATH": ("vault", "path"),
}
for env_var, path in env_overrides.items():
value = os.getenv(env_var)
if value is None:
continue
if path is None:
# Top-level key
obsidian["enabled"] = value.lower() in ("true", "1", "yes")
else:
section, key = path
obsidian.setdefault(section, {})[key] = value
_config_cache = obsidian
return obsidian
def is_obsidian_enabled() -> bool:
"""Check if Obsidian MCP integration is enabled in config."""
config = load_obsidian_config()
return config.get("enabled", False)
def check_obsidian_health(force: bool = False) -> bool:
"""Check if Obsidian REST API is reachable.
Uses cached result unless force=True or cache has expired.
Thread-safe.
"""
global _obsidian_healthy, _last_health_check
config = load_obsidian_config()
check_interval = config.get("routing", {}).get("health_check_interval", 60)
timeout = config.get("routing", {}).get("api_timeout", 10)
with _health_lock:
now = time.time()
if not force and (now - _last_health_check) < check_interval:
return _obsidian_healthy
base_url = config.get("connection", {}).get(
"base_url", "http://127.0.0.1:27123"
)
api_key = config.get("connection", {}).get("api_key", "")
try:
# Obsidian Local REST API health endpoint
response = httpx.get(
f"{base_url}/",
headers={"Authorization": f"Bearer {api_key}"},
timeout=timeout,
verify=config.get("connection", {}).get("verify_ssl", False),
)
_obsidian_healthy = response.status_code == 200
except Exception:
_obsidian_healthy = False
_last_health_check = now
return _obsidian_healthy
def get_obsidian_server_config() -> Dict[str, Any]:
"""Build the MCP server configuration for Agent SDK registration.
Returns the config dict suitable for ClaudeAgentOptions.mcp_servers.
The obsidian-mcp-server runs as a stdio subprocess.
"""
config = load_obsidian_config()
connection = config.get("connection", {})
vault = config.get("vault", {})
cache = config.get("cache", {})
logging = config.get("logging", {})
env = {
"OBSIDIAN_API_KEY": connection.get("api_key", ""),
"OBSIDIAN_BASE_URL": connection.get(
"base_url", "http://127.0.0.1:27123"
),
"OBSIDIAN_VERIFY_SSL": str(
connection.get("verify_ssl", False)
).lower(),
"OBSIDIAN_VAULT_PATH": str(vault.get("path", "")),
"OBSIDIAN_ENABLE_CACHE": str(
cache.get("enabled", True)
).lower(),
"OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN": str(
cache.get("refresh_interval_min", 10)
),
"MCP_LOG_LEVEL": logging.get("level", "info"),
}
return {
"command": "npx",
"args": ["obsidian-mcp-server"],
"env": env,
}
def get_routing_strategy() -> str:
"""Get the configured tool routing strategy."""
config = load_obsidian_config()
return config.get("routing", {}).get("strategy", "obsidian_preferred")
def should_fallback_to_custom() -> bool:
"""Check if fallback to custom tools is enabled."""
config = load_obsidian_config()
return config.get("routing", {}).get("fallback_to_custom", True)
# List of all Obsidian MCP tool names
OBSIDIAN_TOOLS = [
"obsidian_read_note",
"obsidian_update_note",
"obsidian_search_replace",
"obsidian_global_search",
"obsidian_list_notes",
"obsidian_manage_frontmatter",
"obsidian_manage_tags",
"obsidian_delete_note",
]