Files
ajarbot/obsidian_mcp.py
Jordan Ramos fe7c146dc6 feat: Add Gitea MCP integration and project cleanup
## New Features
- **Gitea MCP Tools** (zero API cost):
  - gitea_read_file: Read files from homelab repo
  - gitea_list_files: Browse directories
  - gitea_search_code: Search by filename
  - gitea_get_tree: Get directory tree
- **Gitea Client** (gitea_tools/client.py): REST API wrapper with OAuth
- **Proxmox SSH Scripts** (scripts/): Homelab data collection utilities
- **Obsidian MCP Support** (obsidian_mcp.py): Advanced vault operations
- **Voice Integration Plan** (JARVIS_VOICE_INTEGRATION_PLAN.md)

## Improvements
- **Increased timeout**: 5min → 10min for complex tasks (llm_interface.py)
- **Removed Direct API fallback**: Gitea tools are MCP-only (zero cost)
- **Updated .env.example**: Added Obsidian MCP configuration
- **Enhanced .gitignore**: Protect personal memory files (SOUL.md, MEMORY.md)

## Cleanup
- Deleted 24 obsolete files (temp/test/experimental scripts, outdated docs)
- Untracked personal memory files (SOUL.md, MEMORY.md now in .gitignore)
- Removed: AGENT_SDK_IMPLEMENTATION.md, HYBRID_SEARCH_SUMMARY.md,
  IMPLEMENTATION_SUMMARY.md, MIGRATION.md, test_agent_sdk.py, etc.

## Configuration
- Added config/gitea_config.example.yaml (Gitea setup template)
- Added config/obsidian_mcp.example.yaml (Obsidian MCP template)
- Updated scheduled_tasks.yaml with new task examples

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 20:31:32 -07:00

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",
]