Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ jobs:
- name: Install dependencies
run: |
pip install ruff black mypy
pip install -r requirements.txt
pip install $(python3 -c "import tomllib; print(' '.join(tomllib.load(open('pyproject.toml','rb'))['project']['dependencies']))")

- name: Run ruff
run: ruff check src/ tests/
run: ruff check src/ tests/ scripts/

- name: Run black (check only)
run: black --check src/ tests/
run: black --check src/ tests/ scripts/

- name: Run mypy
run: mypy src/ --ignore-missing-imports
Expand Down Expand Up @@ -57,7 +57,7 @@ jobs:
python-version: "3.11"

- name: Install dependencies
run: pip install -r requirements.txt
run: pip install $(python3 -c "import tomllib; print(' '.join(tomllib.load(open('pyproject.toml','rb'))['project']['dependencies']))") pytest pytest-asyncio

- name: Initialize database schema
env:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ build/
.coverage
htmlcov/

# Type checking / linting caches
.mypy_cache/
.ruff_cache/

# IDE
.vscode/
.idea/
Expand Down
20 changes: 12 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

All notable changes to the Obsidian Graph MCP Server will be documented in this file.
All notable changes to Obsidian Graph will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Expand Down Expand Up @@ -53,18 +53,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Stale database entries no longer persist after file deletions (Issue #2)
- File moves now update paths correctly instead of creating duplicates
- Embedding token limit errors on large/dense notes: dynamic batch sizing with retry-halving (#7)
- Hub notes returning empty on first call: inline-await refresh instead of fire-and-forget (#8)
- Event loop blocking during embedding API calls: async embedder methods (#9)
- Missing database timeouts on 6 vector store methods (#9)
- Schema trigger overwriting file modification times (#10)
- Hub analyzer raising wrong exception type (#10)
- Dead code cleanup and weak test assertions (#10)

### Changed
- File watcher now defaults to polling mode in Docker (native filesystem events unreliable)
- Startup scan now cleans up orphan paths before re-indexing stale files

## [Unreleased - Planned]
- Renamed project from "MCP Server" to "Obsidian Graph" (semantic knowledge graph engine)
- Container names: obsidian-graph (app), obsidian-graph-pgvector (db)

### Planned
- Additional embedding provider support (OpenAI, SentenceTransformers)
- Separate src/ into engine/ and mcp/ packages
- Additional embedding provider support
- Cluster analysis tool (community detection)
- Temporal statistics tool
- Embedding validation tool
- Performance optimizations for large vaults (>10k notes)
- GitHub Actions CI/CD
- Code coverage reporting
12 changes: 4 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@ WORKDIR /app
# Create non-root user
RUN useradd -m -u 1000 mcpuser

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install dependencies from pyproject.toml (deps only, not the package itself)
COPY pyproject.toml .
RUN pip install --no-cache-dir $(python3 -c "import tomllib; print(' '.join(tomllib.load(open('pyproject.toml','rb'))['project']['dependencies']))")

# Copy source code and tests and set ownership
# Copy source code and set ownership
COPY --chown=mcpuser:mcpuser src/ ./src/
COPY --chown=mcpuser:mcpuser tests/ ./tests/

# Copy configuration files for testing
COPY --chown=mcpuser:mcpuser pyproject.toml mypy.ini ./

# Create directories for data and cache
RUN mkdir -p /home/mcpuser/.obsidian-graph/cache && \
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,6 @@ POSTGRES_MIN_CONNECTIONS=5
POSTGRES_MAX_CONNECTIONS=20
EMBEDDING_BATCH_SIZE=128
EMBEDDING_REQUESTS_PER_MINUTE=300

# HNSW index (advanced)
HNSW_M=16
HNSW_EF_CONSTRUCTION=64
```

### Cloud Sync Support (iCloud, Google Drive, Dropbox, OneDrive)
Expand Down Expand Up @@ -268,7 +264,7 @@ By default, the indexer excludes common system and tool folders:
drafts/
```

See [`.obsidian-graph.conf.example`](.obsidian-graph.conf.example) for more patterns and examples.
See [`docs/obsidian-graph.conf.example`](docs/obsidian-graph.conf.example) for more patterns and examples.

**Pattern Syntax:**

Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ services:
- OBSIDIAN_WATCH_POLLING_INTERVAL=${OBSIDIAN_WATCH_POLLING_INTERVAL:-30}
- POSTGRES_MIN_CONNECTIONS=${POSTGRES_MIN_CONNECTIONS:-5}
- POSTGRES_MAX_CONNECTIONS=${POSTGRES_MAX_CONNECTIONS:-20}
- HNSW_M=${HNSW_M:-16}
- HNSW_EF_CONSTRUCTION=${HNSW_EF_CONSTRUCTION:-64}
- EMBEDDING_BATCH_SIZE=${EMBEDDING_BATCH_SIZE:-128}
- EMBEDDING_REQUESTS_PER_MINUTE=${EMBEDDING_REQUESTS_PER_MINUTE:-300}
- MCP_SERVER_NAME=${MCP_SERVER_NAME:-obsidian-graph}
Expand Down
File renamed without changes.
43 changes: 0 additions & 43 deletions mypy.ini

This file was deleted.

55 changes: 53 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ name = "obsidian-graph"
version = "1.0.0"
description = "Semantic knowledge graph engine for markdown vaults"
requires-python = ">=3.11"
dependencies = [
"mcp>=1.0.0",
"asyncpg>=0.29.0",
"pgvector>=0.3.0",
"voyageai>=0.3.0",
"watchdog>=4.0.0",
"loguru>=0.7.2",
"python-dotenv>=1.0.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"black",
"ruff",
"mypy",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand All @@ -19,7 +37,7 @@ markers = [
source = ["src"]
omit = [
"tests/*",
"src/diagnose_vault.py", # Diagnostic utility, not core functionality
"scripts/diagnose_vault.py", # Diagnostic utility, not core functionality
]

[tool.coverage.report]
Expand Down Expand Up @@ -60,7 +78,7 @@ ignore = [

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "S106", "T201", "S108", "B007", "F841", "E402"] # Allow assert, passwords, print, /tmp, unused vars, non-top imports in tests
"src/diagnose_vault.py" = ["T201"] # CLI diagnostic script uses print() for output
"scripts/diagnose_vault.py" = ["T201"] # CLI diagnostic script uses print() for output

[tool.black]
line-length = 100
Expand All @@ -76,3 +94,36 @@ extend-exclude = '''
| __pycache__
)/
'''

[tool.mypy]
python_version = "3.11"
# Gradual adoption - relaxed settings for existing code
disallow_untyped_defs = false
check_untyped_defs = false
warn_return_any = false
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = false
warn_no_return = true
warn_unreachable = false
strict_optional = false
show_error_codes = true
pretty = true
# Disable error codes for gradual adoption (enable as codebase is typed)
disable_error_code = ["var-annotated", "arg-type", "assignment", "call-overload", "return-value", "attr-defined"]

[[tool.mypy.overrides]]
module = "src.validation"
disallow_untyped_defs = true
disallow_any_unimported = true
warn_return_any = true

[[tool.mypy.overrides]]
module = "src.security_utils"
disallow_untyped_defs = true
disallow_any_unimported = true
warn_return_any = true

[[tool.mypy.overrides]]
module = ["watchdog.*", "voyageai.*", "pgvector.*", "mcp.*", "loguru.*"]
ignore_missing_imports = true
20 changes: 0 additions & 20 deletions requirements.txt

This file was deleted.

2 changes: 1 addition & 1 deletion src/diagnose_vault.py → scripts/diagnose_vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import sys
from pathlib import Path

sys.path.insert(0, "/app")
sys.path.insert(0, str(Path(__file__).parent.parent))

from src.embedder import VoyageEmbedder
from src.exceptions import EmbeddingError
Expand Down
2 changes: 1 addition & 1 deletion src/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class EmbeddingError(ObsidianGraphError):
- cause: Original exception that triggered the failure
"""

def __init__(self, message: str, text_preview: str = "", cause: Exception = None):
def __init__(self, message: str, text_preview: str = "", cause: Exception | None = None):
self.text_preview = text_preview[:100] # First 100 chars for debugging
self.cause = cause
super().__init__(message)
Expand Down
2 changes: 1 addition & 1 deletion src/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ async def index_vault(vault_path: str, batch_size: int = 100):
try:
embeddings_list, total_chunks = await embedder.embed_with_chunks(
note_data["content"],
chunk_size=2000, # oachatbot standard
chunk_size=2000,
input_type="document",
)
except EmbeddingError as e:
Expand Down
10 changes: 4 additions & 6 deletions src/vector_store.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"""
PostgreSQL+pgvector Vector Store for Obsidian Graph MCP Server.
PostgreSQL+pgvector Vector Store for Obsidian Graph.

Adapted from oachatbot's PostgreSQL store, simplified for Obsidian notes:
- Stores whole notes and chunked documents (automatic chunking for large notes)
- Uses 'path' as identifier (not document_id)
- No site_id or publish_date (Obsidian-specific)
- Adds connection_count materialization for graph queries
Stores notes (whole or chunked) with vector embeddings for semantic search,
graph analysis, and hub/orphan detection. Uses connection_count materialization
for fast graph queries.
"""

import asyncio
Expand Down
23 changes: 4 additions & 19 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
"""
Shared test fixtures for Obsidian Graph MCP Server.
Shared test fixtures for Obsidian Graph.

Provides reusable fixtures for:
- Event loops (module-scoped for async tests)
- Mock stores and embedders
- Temporary vaults with test data
- Server contexts for integration testing
"""

import asyncio
from datetime import datetime
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock

import pytest


@pytest.fixture(scope="module")
def event_loop():
"""
Module-scoped event loop for async tests.

This ensures all async tests in a module share the same event loop,
which is more efficient and avoids loop recreation overhead.
"""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()


@pytest.fixture
def mock_store():
"""
Expand Down Expand Up @@ -203,7 +188,7 @@ def sample_notes():
title="Test 1",
content="This is test note 1",
embedding=[0.1] * 1024,
modified_at=datetime.now(),
modified_at=datetime.now(UTC),
file_size_bytes=100,
chunk_index=0,
total_chunks=1,
Expand All @@ -213,7 +198,7 @@ def sample_notes():
title="Test 2",
content="This is test note 2",
embedding=[0.2] * 1024,
modified_at=datetime.now(),
modified_at=datetime.now(UTC),
file_size_bytes=150,
chunk_index=0,
total_chunks=1,
Expand Down
Loading
Loading