169 lines
4.9 KiB
Python
169 lines
4.9 KiB
Python
|
|
"""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",
|
||
|
|
]
|