Skip to main content
Normalized for Mintlify from knowledge-base/neurigraph-memory-architecture/neurigraph-tool-references/04-Markdown-Based-Local-Knowledge-Graph.mdx.

Clean-Room Specification: Markdown-Based Local Knowledge Graph with Hybrid Search

Purpose of This Document

This document specifies the complete architecture, data model, storage format, synchronization system, search implementation, and MCP API surface of a local-first knowledge graph that stores all knowledge as structured Markdown files on the user’s filesystem. Files are parsed to extract entities, observations, and relations, which are indexed into a relational database (SQLite or PostgreSQL) with optional vector embeddings for semantic search. The system watches the filesystem for changes and automatically syncs. It is exposed to AI assistants via MCP (Model Context Protocol) tools. This specification is detailed enough that a professional AI coding model can produce a functionally identical working system without reference to any existing codebase.

1. System Overview

1.1 Core Concept

Users write Markdown notes in a project directory. Each note can contain:
  • Frontmatter (YAML metadata: title, type, tags, custom fields)
  • Observations (atomic facts in bracket-category notation)
  • Relations (explicit directed links using [[wiki-link]] syntax)
  • Free-form content (standard Markdown)
A background service watches the directory, parses files, extracts structured data, and indexes everything into a database. An MCP server exposes tools for AI assistants to read, write, search, and traverse the knowledge graph.

1.2 Architecture Layers

1.3 Key Design Principles

  1. Markdown-first: The filesystem is the source of truth. The database is a derived index.
  2. Async throughout: All I/O (database, files, HTTP) uses async/await.
  3. Protocol-based repositories: Search backend is swappable (SQLite FTS5 vs PostgreSQL tsvector).
  4. Graceful degradation: If vector search is unavailable, fall back to FTS. If FTS returns nothing, retry with relaxed query.
  5. Multi-project: Multiple independent knowledge bases, each with its own directory and database.

2. Data Model

2.1 Database Schema

2.1.1 Project Table

Permalink auto-generation: When a project is created, its permalink is generated from name by lowercasing and replacing non-alphanumeric characters with hyphens. Example: “My Research” → “my-research”.

2.1.2 Entity Table

Unique constraints:
  • (permalink, project_id) — No two entities share a permalink within a project
  • (file_path, project_id) — No two entities share a file path within a project
Permalink generation: Title → lowercase → replace spaces/special chars with hyphens → strip leading/trailing hyphens. Example: “Machine Learning Basics” → “machine-learning-basics”.

2.1.3 Observation Table

Cascade: When an entity is deleted, all its observations are automatically deleted.

2.1.4 Relation Table

Unique constraints:
  • (from_id, to_id, relation_type) when to_id is not NULL
  • (from_id, to_name, relation_type) for unresolved relations
Link resolution: Relations start with to_id=NULL and to_name set. A LinkResolver service periodically attempts to match to_name against entity titles/permalinks. When matched, to_id is set.

2.1.5 Search Index Tables

FTS5 Virtual Table (SQLite):
Vector Storage Tables (when semantic search is enabled):

3. Markdown File Format

3.1 File Structure

Each Markdown file in the project directory represents one entity. The file format:

3.2 Frontmatter Parsing

The YAML frontmatter between --- delimiters is parsed using python-frontmatter. All values are normalized to strings:
  • Dates → ISO 8601 strings
  • Numbers → string representation
  • Booleans"True" or "False"
  • Lists → preserved as lists of strings
  • None/null → excluded from metadata
Required fields (title, type) are coerced to strings even if they parse as other types. If title is missing from frontmatter, the filename (without extension) is used.

3.3 Observation Extraction

Observations are extracted from list items matching this pattern:
Regex pattern: ^\[([^\[\]()]+)\]\s+(.+) This matches:
  • [definition] ML is...
  • [technique] Supervised learning #ml
  • [x] Completed task ✗ (excluded — checkbox)
  • [ ] Incomplete task ✗ (excluded — checkbox)
  • [link text](url) ✗ (excluded — markdown link)
  • [[wiki-link]] ✗ (excluded — wiki link)
Tag extraction: From the content text, extract all #word patterns. Tags are stored as a JSON array. Context extraction: If the content ends with (text in parens), extract that as the context field. Processing order: Extract tags first, then context, leaving the remaining text as the observation content.

3.4 Relation Extraction

Two types of relations are extracted: Explicit relations (from list items):
Pattern: A list item starting with a word/phrase followed by a [[wiki-link]]. The word before the wiki-link becomes relation_type, the wiki-link content becomes to_name. Implicit relations (from inline wiki-links): Any [[Target Entity]] found in the body text (not already captured as an explicit relation) creates an implicit relation with relation_type = "links_to". Wiki-link parsing: Handle nested brackets correctly. Track bracket depth: increment on [, decrement on ]. Content between matched [[ and ]] is the target name. Normalize target names: “Entity Name” → “entity-name” (lowercase, spaces to hyphens).

3.5 Entity Output Schema

After parsing, each file yields:

4. Filesystem Synchronization

4.1 File Watcher

Use the watchfiles library for cross-platform filesystem monitoring. Configuration:
  • Debounce delay: configurable, default 1000ms
  • Filter patterns: respect .gitignore and .bmignore files (custom ignore patterns)
  • Watch only .md files
Event types: Created, Modified, Deleted State tracking (per watcher instance):
  • running: bool
  • start_time: datetime
  • error_count: int
  • synced_files: int
  • recent_events: deque(maxlen=100) — last 100 file events

4.2 Sync Algorithm

The sync process runs in three phases: Phase 1 — Directory Scan:
  1. Walk the project directory using a thread pool executor (to avoid blocking async loop)
  2. For each .md file found:
    • Compute SHA-256 checksum of file content
    • Record mtime and file size
    • Store as {file_path, checksum, mtime, size}
Phase 2 — Change Detection: Compare filesystem state against database state:
Move detection algorithm:
  1. Collect all checksums from DB entities and from filesystem scan
  2. For each file in DB that’s NOT in filesystem:
    • Check if its checksum appears in a NEW filesystem file
    • If yes: classify as moved (old_path → new_path)
    • If no: classify as deleted
Phase 3 — Apply Changes:
  • New files: Parse markdown → create entity + observations + relations → update search index
  • Modified files: Parse markdown → update entity + diff observations/relations → update search index
  • Deleted files: Delete entity (cascades to observations/relations) → remove from search index
  • Moved files: Update entity.file_path, preserve entity.id and all relations

4.3 Circuit Breaker

To prevent infinite retry loops on consistently failing files:
  • Track consecutive failure count per file path
  • After 3 consecutive failures, skip the file in future sync cycles
  • Reset failure count when the file’s checksum changes (indicating the user modified it)
  • Log skipped files at warning level

4.4 Sync Coordinator

A top-level coordinator manages the sync lifecycle:
  1. Initialization: Run database migrations (Alembic), perform initial full sync
  2. Watch loop: Start file watcher, process events through SyncService
  3. Background tasks: Embedding backfill (process entities lacking vector embeddings)
  4. Shutdown: Cancel all watchers, cancel backfill tasks, close database connections

5. Search System

5.1 Search Modes

Three search modes, selected via search_type parameter:

5.2 FTS Implementation (SQLite)

Query preparation:
  1. Split query into tokens
  2. For tokens containing special characters (hyphens, dots, colons): wrap in double quotes
    • "machine-learning""\"machine-learning\""
  3. Preserve boolean operators: AND, OR, NOT (case-sensitive)
  4. Append * for prefix matching on the last token
  5. Join with spaces (implicit AND in FTS5)
Relaxed fallback: If FTS returns zero results for a multi-term query:
  1. Remove stopwords (“the”, “a”, “an”, “is”, “are”, “was”, “were”, “in”, “on”, “at”, “to”, “for”, “of”, “with”, “by”)
  2. Join remaining terms with OR instead of implicit AND
  3. Retry query
Ranking: FTS5 built-in rank function (BM25-based). Results ordered by rank descending.

5.3 Vector Search Implementation

Embedding providers (configurable): Provider protocol interface:
Chunking strategy:
  • Split entity content into chunks for embedding
  • Store each chunk with its index: (entity_id, chunk_text, chunk_index)
  • Embed each chunk independently
Similarity computation:
  • Store embeddings as raw float32 BLOBs
  • Compute L2 distance, convert to cosine similarity: similarity = 1 - (L2_distance² / 2)
  • Filter results by minimum similarity threshold (default: 0.55)
  • Return top-k results (default k=100)
Combine FTS and vector results:
Score normalization: FTS scores are normalized to [0, 1] range using min-max scaling within the result set. Merging: Union results from both searches, keyed by entity_id. If an entity appears in both, use the hybrid score. If only in one, use 0.5 × that score.

5.5 Search Filters

All search modes support these filters:

6. MCP Server

6.1 Server Setup

Use FastMCP framework. Server name: configurable (default “Basic Memory”). Lifespan handler (runs on server startup):
  1. Initialize dependency container (services, repositories, database connection)
  2. Run database migrations (Alembic)
  3. Log embedding provider status
  4. Start sync coordinator (initial sync + file watching)
Shutdown: Stop sync coordinator, close all database connections.

6.2 MCP Tools

6.2.1 write_note

Create or overwrite a Markdown file in the project directory. Parameters: Behavior:
  1. Generate filename from title: title.lower().replace(" ", "-") + ".md"
  2. Construct full path: project_root / directory / filename
  3. If file exists and overwrite is false: return error
  4. Build frontmatter YAML from title, type, tags, metadata
  1. The file watcher will detect the change and sync to database
Returns: Entity data including permalink and file_path.

6.2.2 read_note

Read a note by permalink or file path. Parameters: Returns: Full entity data including frontmatter, content, observations, relations, and related entities.

6.2.3 edit_note

Apply targeted edits to an existing note. Parameters: Behavior: Read existing file, apply updates (append, replace section, etc.), write back. The sync service detects the change.

6.2.4 delete_note

Delete a note file and its database records. Parameters: Behavior: Delete the physical file. The sync service detects the deletion and removes the entity (cascading to observations and relations).

6.2.5 search_notes

Search across all indexed content. Parameters: Returns: List of matching entities with relevance scores, snippets, and metadata.

6.2.6 build_context

Resolve a memory:// URI and build rich context. Parameters: Behavior:
  1. Strip memory:// prefix if present
  2. Resolve to entity by permalink or file path
  3. Return entity metadata, content, observations, relations, and related entity summaries
Returns: Formatted context string suitable for AI consumption.

6.2.7 list_directory

List files and subdirectories in the project. Parameters: Returns: List of files and folders with metadata.

6.2.8 recent_activity

Get recently modified entities. Parameters: Returns: Entities modified within the timeframe, sorted by modification date descending.

6.2.9 list_memory_projects

List all configured projects. Parameters: None. Returns: Array of project objects with name, path, is_active, is_default, entity count.

6.2.10 create_memory_project

Create a new project. Parameters: Behavior: Create project record, create directory if not exists, start watching.

6.3 MCP Resources

project_info: Returns current project metadata, entity/observation/relation counts, and sync status.

6.4 MCP Prompts


7. URI Scheme

7.1 Format

Examples:
  • memory://machine-learning-basics
  • memory://specs/search-implementation
  • memory://id/123 (by internal ID)

7.2 Validation Rules

A valid memory URI path must NOT contain:
  • Empty string
  • :// (double protocol)
  • // (double slash within path)
  • <, >, ", |, ? characters

7.3 Resolution

  1. Strip memory:// prefix
  2. If path starts with id/: look up entity by numeric ID
  3. Otherwise: look up entity by permalink match
  4. If not found by permalink: try as file_path
  5. Return entity with full context (observations, relations, neighbors)

8. Service Layer Architecture

8.1 Base Service Pattern

All services inherit from this base, receiving their repository via constructor injection.

8.2 Dependency Container

A container class holds all services and repositories, constructed during server lifespan:

8.3 EntityService

Core operations:
  • create_entity(parsed: ParsedEntity, project_id: int) → Entity
  • update_entity(entity_id: int, parsed: ParsedEntity) → Entity
  • delete_entity(entity_id: int) → None
  • get_by_permalink(permalink: str, project_id: int) → Entity
  • get_by_file_path(file_path: str, project_id: int) → Entity
  • resolve_path(path: str, project_id: int) → Entity — tries permalink first, then file_path

8.4 SearchService

  • search(query, project_id, search_type, filters, limit, offset) → SearchResults
  • index_entity(entity: Entity) → None — update FTS + vector indexes
  • remove_from_index(entity_id: int) → None
  • reindex_all(project_id: int) → None

8.5 SyncService

  • full_sync(project_id: int) → SyncReport
  • sync_file(file_path: str, project_id: int) → Entity
  • remove_file(file_path: str, project_id: int) → None
  • detect_moves(db_state, fs_state) → List[Move]

8.6 ContextService

  • build_context(path: str, project_id: int) → ContextResult
    • Returns: entity metadata, content, observations, relations, related entities (1-hop neighbors)

8.7 LinkResolver

  • resolve_pending(project_id: int) → int — returns count of newly resolved links
  • Runs after each sync cycle
  • Matches relation.to_name against entity titles and permalinks (case-insensitive)
  • When matched: sets relation.to_id

9. Configuration

9.1 Configuration Schema

9.2 Configuration Sources (Priority Order)

  1. Environment variables: Prefixed with BASIC_MEMORY_ (e.g., BASIC_MEMORY_DATABASE_BACKEND=postgres)
  2. Config file: ~/.basic-memory/config.json
  3. Defaults: Values in the Config dataclass

9.3 Auto-Detection

Semantic search is automatically enabled if:
  • The configured embedding provider library is importable (fastembed or openai)
  • AND the vector storage extension is available (sqlite-vec for SQLite)

10. Database Migrations

Use Alembic for schema migrations. Migration strategy:
  • Migrations run automatically on server startup (as part of lifespan handler)
  • Migration directory stored alongside application code
Key migrations:
  1. Initial schema: Create entity, observation, relation, project tables
  2. Add FTS5 virtual table
  3. Add vector storage tables (search_vector_chunks, search_vector_embeddings)
  4. Add permalink columns and indexes
  5. Add file sync tracking columns (mtime, size, checksum)

11. Project Resolution

When an MCP tool receives a project parameter:
  1. If project is provided: look up by name
  2. If not provided: use the configured default project
  3. If no default configured: use the first active project found
  4. If no projects exist: return error
Single-project mode: When only one project is configured, all tools implicitly use it without requiring the project parameter.

12. Error Handling

12.1 File Parsing Errors

  • If frontmatter is invalid YAML: skip file, log warning, continue sync
  • If file is empty: create entity with title from filename, no observations/relations
  • If file encoding is not UTF-8: attempt detection, fall back to latin-1

12.2 Sync Errors

  • File read permission denied: log error, skip file, increment circuit breaker
  • File deleted during sync: handle gracefully (already gone)
  • Database write conflict: retry with exponential backoff (up to 3 attempts)

12.3 Search Errors

  • FTS query syntax error: fall back to relaxed query (OR terms, no special operators)
  • Vector provider unavailable: fall back to FTS-only
  • No results: return empty list with suggestion to broaden query

13. Complete Behavioral Test Specifications

13.1 Markdown Parsing Tests

13.2 Sync Tests

13.3 Search Tests

13.4 MCP Tool Tests


14. Key Implementation Algorithms

14.3 FTS Query Preparation (SQLite)

14.4 L2 to Cosine Similarity Conversion

14.5 Hybrid Score Computation


15. Dependencies

15.1 Required

15.2 Optional


16. Directory Structure


17. Startup Sequence

  1. Load configuration (env vars → config file → defaults)
  2. Initialize database engine (SQLite or PostgreSQL async)
  3. Run Alembic migrations
  4. Create dependency container (repositories, services)
  5. Check for semantic search availability (auto-detect)
  6. For each active project: a. Run full sync (Phase 1-3) b. Resolve pending links c. Start file watcher d. Start background embedding backfill (if semantic search enabled)
  7. Register MCP tools, resources, and prompts
  8. Begin accepting MCP connections

18. Shutdown Sequence

  1. Stop accepting new MCP requests
  2. Cancel all file watchers
  3. Cancel background embedding tasks
  4. Flush pending sync operations
  5. Close database connections
  6. Exit cleanly

This specification provides complete architectural and behavioral detail for independent implementation of a markdown-based local knowledge graph with hybrid search, filesystem synchronization, and MCP integration.