{"id":"a7645249-b422-49d2-8acc-6bfc7c8774b8","slug":"obsidian-graph","kind":"mcp","name":"obsidian-graph","summary":"Semantic knowledge graph navigation for Obsidian or markdown vaults using AI-powered vector embeddings and PostgreSQL+pgvector","owner":"drewburchfield","repoUrl":"https://github.com/drewburchfield/obsidian-graph","license":"MIT","licenseTier":"permissive","trust":"community","stars":25,"match":null,"fewStars":false,"format":"mcp","createdAt":"2026-01-07T23:04:31.000Z","addedAt":"2026-10-14T05:02:47.593Z","logoUrl":"https://avatars.githubusercontent.com/u/1084679?u=56a0b1e68807a49d09e9e98e9431e5a38422f89f&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["files-documents","databases","search"],"tags":["obsidian","pgvector","semantic-search"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-17T07:20:27.140Z","sample":false,"url":"https://conduits.dev/tools/obsidian-graph","language":"Python","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"[![Obsidian Graph](https://ghrb.waren.build/banner?header=obsidian-graph%20![obsidian]&subheader=Semantic%20knowledge%20graph%20engine%20for%20markdown%20vaults&bg=0a1628&secondaryBg=1e3a5f&color=e8f0fe&subheaderColor=7eb8da&headerFont=Inter&subheaderFont=Inter&support=false)](https://github.com/drewburchfield/obsidian-graph)\n\n[![CI](https://github.com/drewburchfield/obsidian-graph/actions/workflows/ci.yml/badge.svg)](https://github.com/drewburchfield/obsidian-graph/actions/workflows/ci.yml) [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io/) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/drewburchfield/obsidian-graph)\n\nSemantic knowledge graph engine for markdown vaults. Discovers hidden connections between notes using AI-powered vector embeddings and PostgreSQL+pgvector. Accessible to any AI app or harness compatible with MCP.\n\n## Overview\n\nObsidian Graph builds a semantic knowledge graph of your markdown vault, discovering relationships between notes that go beyond keywords and explicit links. It embeds your notes as vectors using Voyage Context-4, stores them in PostgreSQL+pgvector, and provides tools for semantic search, multi-hop graph traversal, hub detection, and orphan analysis.\n\nDesigned for Obsidian vaults but works with any folder of markdown files. Connects to any AI app or harness compatible with the Model Context Protocol (MCP).\n\n## Features\n\n- **Semantic Search**: Find notes by meaning, not just keywords\n- **Connection Discovery**: Multi-hop BFS graph traversal to map note relationships\n- **Hub Analysis**: Identify highly connected conceptual anchors (MOC candidates)\n- **Orphan Detection**: Find isolated insights that need integration\n- **Auto-Indexing**: Automatic file watching with 30-second debounce\n- **Superior Quality**: Voyage Context-4 (1024d) vs typical 384d embeddings\n\n## Architecture\n\n```\n┌─ obsidian-graph container ─────────────────┐\n│                                            │\n│  MCP Client ◄──stdio──► server.py          │\n│                            │               │\n│                     ┌──────┴──────┐        │\n│                     ▼             ▼        │\n│              graph_builder   hub_analyzer  │\n│              embedder.py     file_watcher  │\n│                  │               │         │\n│                  │ HTTPS         │ watch   │\n│                  ▼               ▼         │\n│            Voyage AI API    /vault (ro)    │\n│                  │                         │\n│                  │ 1024d vectors           │\n│                  ▼                         │\n│           vector_store.py                  │\n│                  │                         │\n└──────────────────┼─────────────────────────┘\n                   │ SQL\n                   ▼\n┌─ obsidian-graph-pgvector container ────────┐\n│  PostgreSQL 15 + pgvector (HNSW index)     │\n└────────────────────────────────────────────┘\n```\n\n- **Embeddings**: Voyage Context-4 (1024 dimensions, contextualized)\n- **Vector Store**: PostgreSQL 15+ with pgvector HNSW indexing\n- **Performance**: 0.9ms search (555x better than target), <2s graph building\n- **File Watching**: Watchdog with polling mode for cloud sync compatibility\n- **Transport**: Docker stdio for MCP communication\n\n## MCP Tools\n\n### Overview\n\nAll tools use **semantic similarity** via 1024-dimensional Voyage Context-4 embeddings. Similarity scores range from 0.0 (unrelated) to 1.0 (identical). Default threshold is 0.5 (clear connection).\n\n**How it works:**\n1. Notes are embedded as vectors in 1024-dimensional space\n2. Cosine similarity measures semantic closeness between vectors\n3. HNSW index enables sub-millisecond vector search\n4. Results ranked by similarity score (0.0-1.0)\n\n### Tool Reference\n\n| Tool | Purpose | Method | Performance | Use Case |\n|------|---------|--------|------------|----------|\n| `search_notes` | Semantic search across vault | Query embedding → vector search | <1ms | Find notes by concept |\n| `get_similar_notes` | Find notes similar to given note | Note embedding → vector search | <300ms | Discover related ideas |\n| `get_connection_graph` | Multi-hop BFS graph traversal | Recursive similarity search | <2s | Map knowledge networks |\n| `get_hub_notes` | Identify highly connected notes | Materialized connection counts | <100ms | Find conceptual anchors |\n| `get_orphaned_notes` | Find isolated notes | Materialized connection counts | <100ms | Unintegrated insights |\n\n### Methodology Details\n\n**search_notes:**\n- Generates query embedding using Voyage Context-4\n- Performs cosine similarity search against all note embeddings\n- Returns top-k most similar notes above threshold\n- HNSW index enables O(log n) search complexity\n\n**get_similar_notes:**\n- Fetches source note's embedding from database\n- Searches for notes with similar embeddings\n- Excludes source note from results\n- Useful for exploring conceptual neighborhoods\n\n**get_connection_graph:**\n- Uses Breadth-First Search (BFS) for level-by-level exploration\n- Prevents cycles by tracking visited nodes\n- Builds multi-hop network (depth 1-5 levels)\n- Each level: finds top-k most similar notes from previous level\n- Returns: nodes (with level), edges (with similarity), stats\n\n**get_hub_notes:**\n- Uses materialized `connection_count` column (O(1) query)\n- Connection count = # of notes above threshold similarity\n- Background refresh when >50% of counts are stale\n- Identifies notes with many semantic connections\n- High hub scores → good MOC (Map of Content) candidates\n\n**get_orphaned_notes:**\n- Uses materialized `connection_count` column\n- Finds notes with few semantic connections\n- Sorted by: connection count (ASC), modified date (DESC)\n- Shows recent notes first (likely new insights)\n- Helps identify notes needing integration\n\n### Chunking Support\n\n**For large notes (>30k tokens):**\n- Automatically split into sentence-aligned chunks (target: ~2000 characters, 0 overlap)\n- Chunking algorithm breaks at sentence boundaries (`. ` or `\\n\\n`) for readability\n- Chunk sizes vary (1800-2200 chars) to preserve sentence integrity\n- Embedded in batches of 60 chunks (preserves context)\n- Voyage Context-4 maintains semantic coherence across chunks\n- Each chunk stored separately with `chunk_index`\n- Search returns individual chunks (can aggregate by path)\n\n**Example:** 168k-char note → ~87 variable-sized chunks → 2 batches (60+27) → context preserved\n\nMost Obsidian notes are <10k tokens and embedded whole (single chunk).\n\n## Prerequisites\n\n### Voyage AI Account Setup\n\nThis server requires a Voyage AI API key for generating embeddings:\n\n1. **Create account**: Sign up at https://www.voyageai.com/\n2. **Get API key**: Visit https://dashboard.voyageai.com/ → API Keys → Create new key\n3. **Add payment method** (Important!):\n   - Go to https://dashboard.voyageai.com/billing\n   - Add a payment method (credit card)\n   - **Why**: Without payment, rate limit is only 3 RPM (unusable)\n   - **With payment**: 300 RPM rate limit unlocked\n4. **Free tier**: Voyage Context-4 includes 200M free tokens (one-time per account, per model):\n   - First 200M tokens are FREE\n   - Sufficient for indexing ~50,000 notes\n   - After free tier: ~$0.12 per 1M tokens\n\n**Cost estimate**: Indexing 1,000 notes ≈ 4M tokens ≈ **$0.48** (or free if within 200M token limit)\n\n## Installation\n\n1. **Clone the repository**:\n```bash\ngit clone https://github.com/drewburchfield/obsidian-graph.git\ncd obsidian-graph\n```\n\n2. **Configure environment**:\n```bash\ncp .env.example .env\n# Edit .env with your settings:\n#   - VOYAGE_API_KEY (from https://dashboard.voyageai.com/)\n#   - OBSIDIAN_VAULT_PATH (absolute path to your vault)\n#   - POSTGRES_PASSWORD (generate with: openssl rand -base64 36)\n```\n\n3. **Start services**:\n```bash\ndocker-compose up -d\n```\n\n4. **Initial indexing** (first time only):\n```bash\ndocker exec -i obsidian-graph .venv/bin/python -m src.indexer\n```\nIndexes entire vault (30-60 min for large vaults). After this, file watching handles incremental updates.\n\n5. **Add to MCP client** (`~/.mcp.json`):\n```json\n{\n  \"mcpServers\": {\n    \"obsidian-graph\": {\n      \"command\": \"docker\",\n      \"args\": [\"exec\", \"-i\", \"-e\", \"OBSIDIAN_WATCH_ENABLED=false\", \"obsidian-graph\", \".venv/bin/python\", \"-m\", \"src.server\"],\n      \"disabled\": false\n    }\n  }\n}\n```\nThe watcher is disabled for MCP sessions because the main container process already watches the vault; without this, every connected client would spawn a duplicate watcher. `scripts/run_vault_mcp.sh` wraps the same command if you prefer pointing your client at a script.\n\n## Configuration\n\n### Required Environment Variables\n\n```bash\n# Voyage AI\nVOYAGE_API_KEY=your_key_here  # Get from https://www.voyageai.com/\n\n# PostgreSQL (POSTGRES_HOST is set by docker-compose.yml, no need to set in .env)\nPOSTGRES_PASSWORD=your_secure_password_here  # Generate with: openssl rand -base64 36\n\n# Obsidian Vault\nOBSIDIAN_VAULT_PATH=/path/to/your/vault  # Absolute path on your system\n```\n\n### Optional Tuning\n\n```bash\n# File watching\nOBSIDIAN_WATCH_ENABLED=true\nOBSIDIAN_DEBOUNCE_SECONDS=30\n\n# Polling mode (auto-enabled for Docker and cloud-synced vaults)\n# OBSIDIAN_WATCH_USE_POLLING=       # true | false (unset = auto-detect)\n# OBSIDIAN_WATCH_POLLING_INTERVAL=30  # seconds between polls (default: 30)\n\n# Performance\nPOSTGRES_MIN_CONNECTIONS=5\nPOSTGRES_MAX_CONNECTIONS=20\nEMBEDDING_BATCH_SIZE=128\nEMBEDDING_REQUESTS_PER_MINUTE=300\n```\n\n### Cloud Sync Support (iCloud, Google Drive, Dropbox, OneDrive)\n\nIf your Obsidian vault is stored in a cloud-synced folder, the file watcher automatically uses **polling mode** for reliable change detection. This is because Docker's filesystem events don't propagate reliably through cloud sync virtualization layers.\n\n**Auto-detection:** Polling mode is automatically enabled when:\n- Running inside Docker (always uses polling for reliability)\n- Vault path contains cloud sync patterns (`Library/Mobile Documents`, `Library/CloudStorage`, etc.)\n\n**How it works:**\n- Polling mode compares directory snapshots every 30 seconds (configurable)\n- Detects file creates, modifications, moves, and deletions\n- Slightly higher CPU than native filesystem events, but works reliably everywhere\n\n**Mobile workflow:** Edit notes on mobile (iOS/Android) via Obsidian's iCloud/Google Drive sync. Changes sync to your Mac, and the polling watcher detects them within the polling interval.\n\n**Override behavior:**\n```bash\n# Force polling on (for edge cases)\nOBSIDIAN_WATCH_USE_POLLING=true\n\n# Force native events (may miss changes with cloud sync)\nOBSIDIAN_WATCH_USE_POLLING=false\n\n# Faster detection (higher CPU)\nOBSIDIAN_WATCH_POLLING_INTERVAL=15\n```\n\n### Excluding Folders from Indexing\n\nBy default, the indexer excludes common system and tool folders:\n- `.obsidian/` / `.trash/` / `.Trash/` (Obsidian system)\n- `.git/` / `.github/` (version control)\n- `.vscode/` / `.cursor/` (editor config)\n- `.claude/` / `.aider/` / `.smart-env/` (AI tools)\n\n**Custom Exclusions**: To exclude additional folders (like a soft-delete folder), create `.obsidian-graph.conf` in your vault root:\n\n```conf\n# Exclude soft delete folder\n07_Archive/Trash/\n\n# Exclude drafts\ndrafts/\n```\n\nSee [`docs/obsidian-graph.conf.example`](docs/obsidian-graph.conf.example) for more patterns and examples.\n\n**Pattern Syntax:**\n\n| Pattern | Matches |\n|---------|---------|\n| `folder/` | All files in `folder/` and subfolders |\n| `drafts/*` | All files directly in `drafts/` |\n| `*.tmp.md` | All files ending in `.tmp.md` |\n\n## Security\n\nThis server implements multiple security layers to protect your vault:\n\n- **Path Traversal Protection**: Validates all file paths stay within vault (`src/security_utils.py`)\n- **Input Validation**: All parameters validated before processing (`src/validation.py`)\n- **Secure Credentials**: Random generated database passwords (`scripts/generate-db-password.sh`)\n- **Container Isolation**: Read-only vault mount, dropped capabilities, non-root user\n\n**Concurrency**: See [docs/CONCURRENCY.md](docs/CONCURRENCY.md) for thread-safety guarantees and race condition prevention.\n\n### Running Security Tests\n\n```bash\n# Security tests\npytest tests/test_security*.py -v\n\n# Input validation tests\nuv run pytest tests/test_validation.py -v\n\n# Race condition tests\nuv run pytest tests/test_race_conditions.py -v\n\n# All tests with coverage (dev extras include pytest-cov)\nuv run pytest tests/ --cov=src --cov-report=html\n```\n\nAll commands require dev dependencies: `uv sync --extra dev`.\n\n## Usage Examples\n\n### Semantic Search\n```\nsearch_notes(query=\"neural networks and consciousness\", limit=10, threshold=0.5)\n\nReturns notes semantically related to the query, even if they don't contain\nthe exact keywords.\n```\n\n### Find Similar Notes\n```\nget_similar_notes(note_path=\"neuroscience/dopamine.md\", limit=10, threshold=0.6)\n\nDiscovers notes conceptually similar to dopamine note (might find:\nreward-systems.md, motivation.md, decision-making.md)\n```\n\n### Build Connection Graph\n```\nget_connection_graph(\n  note_path=\"philosophy/free-will.md\",\n  depth=3,\n  max_per_level=5,\n  threshold=0.65\n)\n\nMaps 3-level network showing how free-will connects to neuroscience,\npsychology, and ethics notes through semantic similarity.\n```\n\n### Identify Hubs\n```\nget_hub_notes(min_connections=10, threshold=0.5, limit=20)\n\nFinds notes with >=10 connections - candidates for Maps of Content (MOCs).\nExample: \"decision-making.md\" might connect to psychology, neuroscience,\neconomics, and philosophy notes.\n```\n\n### Find Orphans\n```\nget_orphaned_notes(max_connections=2, limit=20)\n\nIdentifies isolated notes that need integration into knowledge graph.\nSorted by modification date to surface recent unconnected insights.\n```\n\n## Performance\n\nValidated metrics:\n\n| Metric | Target | Actual | Status |\n|--------|--------|--------|--------|\n| Search latency | <500ms | 0.9ms | ✅ 555x better |\n| Graph building (depth=3) | <2s | <2s | ✅ On target |\n| Hub/orphan queries | <100ms | <100ms | ✅ Materialized |\n| Similarity range | [0.0-1.0] | [0.0-1.0] | ✅ Validated |\n| Embedding quality | 1024-dim | 1024-dim | ✅ Voyage Context-4 |\n\n**Performance Note**: Metrics measured on development vault (~500 notes, M1 MacBook Pro). Actual performance depends on vault size, hardware (CPU/RAM/SSD), and database configuration. HNSW indexing provides O(log n) search, so performance degrades gracefully with vault size.\n\n## Troubleshooting\n\n### \"Reduced rate limits of 3 RPM\"\n- **Cause**: No payment method on Voyage account\n- **Solution**: Add payment method at https://dashboard.voyageai.com/\n- **Note**: 200M free tokens still apply\n\n### \"PostgreSQL connection failed\"\n```bash\n# Check postgres container\ndocker ps | grep obsidian-graph-pgvector\ndocker logs obsidian-graph-pgvector\n\n# Verify credentials\ngrep POSTGRES_ .env\n```\n\n### \"Note not found\" errors\n- Ensure initial indexing completed: `docker exec -i obsidian-graph .venv/bin/python -m src.indexer`\n- Check vault path is mounted: `docker exec -i obsidian-graph ls /vault`\n\n### File changes not detected\n- Verify `OBSIDIAN_WATCH_ENABLED=true`\n- Check logs: `docker logs obsidian-graph`\n- Look for: `Watching vault: /vault [polling (interval: 30s)]`\n- File watcher starts after PostgreSQL connection\n- **Cloud sync users**: Changes take up to polling interval (default 30s) plus cloud sync time\n- **Reduce detection time**: Set `OBSIDIAN_WATCH_POLLING_INTERVAL=15` in `.env`\n\n## Development\n\n### Running Tests\n\nTests run on the host (the Docker image ships without dev dependencies or the `tests/` directory):\n\n```bash\n# Full test suite\nuv sync --extra dev\nuv run pytest tests/ -v\n\n# Docker end-to-end tests (requires the stack running and VOYAGE_API_KEY)\nuv run pytest tests/test_e2e_docker.py -v -s\n\n# Standalone E2E regression suite against the running stack (read-only, 35 checks)\ndocker cp scripts/run_e2e_tests.py obsidian-graph:/tmp/\ndocker exec -w /app obsidian-graph .venv/bin/python /tmp/run_e2e_tests.py\n```\n\n### Rebuilding\n\n```bash\ndocker-compose build obsidian-graph\ndocker-compose restart obsidian-graph\n```\n\n### Debugging\n\n```bash\n# View logs\ndocker logs -f obsidian-graph\n\n# Interactive shell\ndocker exec -it obsidian-graph /bin/bash\n\n# Check database\ndocker exec -it obsidian-graph-pgvector psql -U obsidian -d obsidian_graph\n```\n\n## Comparison to mcp-obsidian\n\n| Feature | mcp-obsidian | obsidian-graph |\n|---------|--------------|----------------|\n| Embeddings | 384-dim (all-MiniLM-L6-v2) | 1024-dim (Voyage Context-4) |\n| Vector Store | ChromaDB | PostgreSQL+pgvector |\n| Tools | 2 (search, reindex) | 5 (search, similar, graph, hubs, orphans) |\n| Search perf | Unknown | 0.9ms validated |\n| Graph traversal | ❌ No | ✅ BFS with cycle prevention |\n| Hub detection | ❌ No | ✅ Materialized stats |\n\n## License\n\nMIT License - Copyright (c) 2025 Drew Burchfield\n\nSee LICENSE file for details.\n\n## Links\n\n- **Voyage AI**: https://www.voyageai.com/\n- **pgvector**: https://github.com/pgvector/pgvector\n- **MCP Protocol**: https://modelcontextprotocol.io/\n"}