Skip to content

Vault

The Vault class is the primary public API for the library. MCP tools, CLI commands, and direct integrations all go through this class. It is a thin composition root: the read / write / graph / index operations live on the four facets, reached through the reader / writer / graph / index accessors (see Facets).

Quick Start

Construction uses VaultSettings: pass source_dir plus a VaultSettings carrying the configuration knobs. Collaborator objects (embedding_provider, summarizer, git_strategy, on_write, and chunk_strategy) stay explicit keywords.

from pathlib import Path
from markdown_vault_mcp.vault import Vault, VaultSettings

# Basic read-only vault (all settings at their defaults)
vault = Vault(source_dir=Path("/path/to/vault"))
stats = vault.index.build_index()
print(f"Indexed {stats.documents_indexed} documents")
vault.close()

# Configured vault: knobs travel on VaultSettings
vault = Vault(
    source_dir=Path("/path/to/vault"),
    settings=VaultSettings(
        read_only=False,
        index_path=Path("/path/to/index.db"),
        exclude_patterns=[".obsidian/**"],
    ),
)

# Build (or refresh) the index before the first search: construction never builds it
vault.index.build_index()

# Search (reader facet)
results = vault.reader.search("query text", limit=10)
for r in results:
    print(f"{r.path}: {r.title} (score: {r.score:.2f})")

# Read a document (reader facet)
note = vault.reader.read("Journal/note.md")
print(note.content)
vault.close()

Migrating from 4.x

The 31 configuration keywords on Vault have been removed. Move each value to the same-named field on VaultSettings. Replace Vault(source_dir=root, read_only=False, index_path=index) with:

vault = Vault(
    source_dir=root,
    settings=VaultSettings(read_only=False, index_path=index),
)

Old keywords now raise TypeError, including when their values match the defaults or when settings is also supplied. source_dir and the five collaborator keywords remain on Vault.

Omitting settings (or passing None) uses VaultSettings(). Library defaults are unchanged: read-only, no chunk overlap, and no overwrite protection once writes are enabled. Server configuration continues to use its own defaults through configuration assembly.

Build completion

Synchronous, asynchronous, and legacy background builds use one build lifecycle. The Future from vault.index.build_index_async() completes after the scan, completion-marker write, and readiness update. Marker-write failures raise through this Future. Pending builds can be cancelled; running builds finish normally. The legacy start_background_build_index() method schedules work once on the same writer.

Index freshness after writes

File writes complete before returning; index updates run on the background writer. For a following library search or graph read, use vault.index.wait_for_drain(timeout=60) and check whether it returns True. A disk read does not need this wait.

Link conversion, index generation, rename with update_links=True, and folder move perform their own queued refresh before reading index data. They fail before mutation if that refresh fails or exceeds 60 seconds. A timeout raised by the refresh job propagates as that job error; the generic refresh-timeout message is reserved for expiration of the wait budget. The wait covers prior writes; it does not isolate concurrent edits. The same 60-second budget covers the preceding build and its readiness update, including synchronous and background builds. A failed or cancelled build blocks these mutations; an index with no scheduled build still supports rename and folder move. OKF generators still reject an index that was never built. Healthy notes continue to receive vector updates when another note in their refresh batch fails. Retrying that batch reuses matching stored vectors, avoiding repeated provider requests for unchanged notes. Direct DocumentManager integrations can supply the sync_index callback to provide the same boundary.

API Reference

VaultSettings(index_path=None, embeddings_path=None, read_only=True, write_protect_existing=False, report_unresolved_links=False, state_path=None, indexed_frontmatter_fields=None, required_frontmatter=None, git_pull_interval_s=0, exclude_patterns=None, attachment_extensions=None, max_attachment_size_mb=1.0, max_note_read_bytes=262144, chunks_per_file=2, snippet_words=200, length_downweight_alpha=0.25, default_search_mode='auto', max_chunk_words=400, max_chunk_chars=None, max_chunk_chars_override=None, chunk_overlap_words=0, summarize_max_notes=50, summarize_max_input_chars=200000, title_field='title', searchable_frontmatter_fields=None, embed_context=False, embedding_batch_size=4, folder_weights=None, fts_weights=None, conventions_file='_conventions.md', okf_mode='auto', okf_write=False, owns_index_files=True) dataclass

Config-derived construction settings for a :class:~markdown_vault_mcp.vault.Vault.

One field per configuration knob, preserving the names, types, and defaults of the constructor keywords removed in #1225, plus owns_index_files, which only a library caller sets. Construct directly for library use, or from a served config via :meth:from_project_config / :func:~markdown_vault_mcp.config_sections._assembly.to_vault_settings.

Attributes:

Name Type Description
index_path Path | None

Path to the SQLite index file. None (default) uses an in-memory database that is discarded when the object is collected.

embeddings_path Path | None

Base path for the {path}.npy and {path}.json sidecar files. None (default) puts them beside index_path as {index name}-embeddings (dots made dashes: index-db-embeddings), or keeps the vectors in memory without an index path (#1708). Semantic search is on whenever Vault is given an embedding provider.

owns_index_files bool

When True (default), this vault owns the index files on disk: the SQLite index, the vector sidecar and the state file. False is for a process that reads files another one owns, such as the CLI search reading a running server's index, and leaves them as they are: an on-disk index opens read-only and ranks with the weights stored in it (#1758), a search that finds the vector sidecar incompatible or corrupt raises :exc:~markdown_vault_mcp.exceptions.VectorIndexUnusableError instead of rebuilding it (#1734), and a pre-#1693 state file is not copied into place. An in-memory index is this process's own and stays writable. A library setting with no configuration knob behind it.

read_only bool

When True (default), write operations raise :exc:~markdown_vault_mcp.exceptions.ReadOnlyError.

This library default deliberately stays True even though the server's MARKDOWN_VAULT_MCP_READ_ONLY now defaults to False (#1113). They are separate tiers: the operator default is a product decision about what an installed server should do, while this one is a fail-safe for a downstream Python consumer who constructs a Vault without naming the argument. Keeping it costs nothing — the server path always passes the value explicitly through to_vault_settings — and moving it would be an independent breaking change to the public library interface.

write_protect_existing bool

When True, a write that would overwrite an existing file without an if_match etag raises :exc:~markdown_vault_mcp.exceptions.DocumentExistsError (default False, i.e. writes overwrite unconditionally).

report_unresolved_links bool

When True, the write / edit / append MCP tools add unresolved_links to their response: the links the call added that get_broken_links reports (default False, #1725). Read by the tool layer through :attr:~markdown_vault_mcp.vault.Vault.report_unresolved_links; library callers ask per call instead.

state_path Path | None

Path to the hash-state JSON file used by :class:~markdown_vault_mcp.tracker.ChangeTracker. Defaults to {index file name}.state.json beside index_path, and to memory when the index is; never inside the vault (#1693).

indexed_frontmatter_fields Sequence[str] | None

Frontmatter keys whose values are promoted to the document_tags table for structured filtering.

required_frontmatter Sequence[str] | None

If provided, documents missing any listed field are excluded from the index entirely.

git_pull_interval_s int

Interval in seconds for periodic pulls. 0 disables the pull loop.

exclude_patterns Sequence[str] | None

Glob patterns (relative to source_dir) for files and directories to exclude from indexing.

attachment_extensions Sequence[str] | None

Allowlist of extensions for binary attachments, in any case and with or without leading dots ("pdf", "PDF" and ".pdf" are the same type). ["*"] accepts all extensions. Also decides which link targets are attachment references rather than note links (#1333), so it is recorded as index provenance and a change rebuilds the index once.

max_attachment_size_mb float

Attachment context-size cap in megabytes, enforced by the read / write / fetch MCP tools (not by the vault library). 0 disables the limit (default 1.0).

max_note_read_bytes int

Maximum bytes returned by full-document reads. 0 disables the limit (default 262144, i.e. 256 KB).

chunks_per_file int

Search results kept per file before grouping.

snippet_words int

Snippet truncation length in words.

length_downweight_alpha float

Length-downweight exponent for ranking.

default_search_mode str

Mode used when search gets no mode.

max_chunk_words int

Word cap for the default heading chunker.

max_chunk_chars int | None

Character cap for the default heading chunker.

max_chunk_chars_override int | None

Explicit operator char-cap override — the stable warm-restart key (#649).

chunk_overlap_words int

Overlap carried between split chunks.

summarize_max_notes int

Cap on notes summarised per summarize call (subtree expansion is truncated to this many notes; default 50).

summarize_max_input_chars int

Aggregate cap on note characters sent to the summarization backend per call (default 200000).

title_field str

Frontmatter key consulted first when resolving document titles (default "title"; falls back to title → first H1 → filename stem).

searchable_frontmatter_fields Sequence[str] | None

Frontmatter keys whose scalar values are keyword-searchable via the FTS summary column and — because configuring this activates format v2 — prefixed to first-chunk embedding text (triggering a one-time re-embed). None disables both.

embed_context bool

When True, forces format v2 (document title + chunk heading enrichment) even with no searchable_frontmatter_fields; any searchable field also activates v2 (default False — raw chunk content).

embedding_batch_size int

Maximum number of chunk texts sent to the embedding provider per call in the cold-build, convergence, and inline-reindex paths (default 4).

folder_weights dict[str, float] | None

Folder-prefix score multipliers applied to search results just before file grouping (None disables).

fts_weights dict[str, float] | None

Per-column BM25 weights persisted into the FTS5 rank configuration (None or all-1.0 keeps the default).

conventions_file str | None

Well-known per-folder conventions filename resolved by :attr:~markdown_vault_mcp.vault.Vault.conventions (default "_conventions.md"); None disables folder conventions. When set, the filename is automatically appended to exclude_patterns (in both fnmatch forms) so convention files stay out of the index while remaining disk-readable. Must not contain fnmatch metacharacters.

okf_mode str

OKF (Open Knowledge Format) read-semantics mode — "auto" (default; follow the vault's okf_version declaration in the root index.md), "off", or "on". When read semantics are active at construction time, the OKF scalar keys (type / status / stale_after) extend the effective indexed_frontmatter_fields set. See :attr:~markdown_vault_mcp.vault.Vault.okf.

okf_write bool

Enable the OKF enforced-write layer.

from_project_config(config, *, embedding_context_length=None) classmethod

Map a served :class:ProjectConfig onto vault settings.

Maps server configuration names onto library setting names (searchable_frontmatter → searchable_frontmatter_fields, default_mode → default_search_mode) and the weight-map tuple → dict conversions (#639). The git pull interval resolves the same way the git-strategy assembly does: config.git.pull_interval_s only when a remote is configured (repo URL or token), else 0.

Parameters:

Name Type Description Default
config ProjectConfig

The served project configuration.

required
embedding_context_length int | None

Token context length of the resolved embedding provider, used to derive max_chunk_chars (None — no provider — falls back to the bounded ceiling). Prefer :func:~markdown_vault_mcp.config_sections._assembly.to_vault_settings, which resolves the provider and threads this automatically.

None

Returns:

Type Description
VaultSettings

The mapped :class:VaultSettings.

effective_indexed_fields(*, okf_active)

Return the indexed-frontmatter set, OKF-extended when active.

When OKF read semantics are active at construction time, the OKF scalar keys (type / status / stale_after) join the configured set so document_tags carries them (design okf.md §3).

Parameters:

Name Type Description Default
okf_active bool

Whether the vault's OKF detection probe reports active read semantics.

required

Returns:

Type Description
list[str]

The effective field list (possibly empty, never None).

effective_exclude_patterns()

Return the exclusion patterns with the conventions-file derivation.

When a conventions file is configured, both fnmatch forms are appended (name and **/name — the **/ form alone does not match a root-level file) so convention files stay out of the index while remaining disk-readable. Without one, the configured patterns pass through unchanged (possibly None).

Returns:

Type Description
Sequence[str] | None

The effective pattern sequence, or None when nothing is

Sequence[str] | None

excluded.

Raises:

Type Description
ValueError

If conventions_file contains fnmatch metacharacters (which would invert the exclusion).

effective_embeddings_path()

Return the embeddings sidecar base, defaulting beside the index (#1708).

Returns:

Type Description
Path | None

The explicit embeddings_path; else {index name}-embeddings

Path | None

with the name's dots made dashes,

Path | None

beside an on-disk index_path; else None, meaning the

Path | None

vectors are held in memory and rebuilt at each start.

effective_state_path(source_dir)

Return the hash-state path, defaulting beside the index (#1693).

The state records which notes the index holds, so it lives with the index and never inside the vault: with no on-disk index there is nothing for a state file to describe, and the state stays in memory.

Parameters:

Name Type Description Default
source_dir Path

The vault root. Unused since the default left the vault (#1693); kept so existing calls keep working.

required

Returns:

Type Description
Path | None

The explicit state_path; else {index file name}.state.json

Path | None

beside an on-disk index_path; else None, meaning in memory

Path | None

(no index_path, or SQLite's :memory:).

Vault(*, source_dir, settings=None, embedding_provider=None, summarizer=None, git_strategy=None, on_write=None, chunk_strategy='heading')

Facade over FTS5 index, vector index, and change tracker.

Instantiate once per vault root. The read / write / graph / index operations live on the four facets, reached through the :attr:reader / :attr:writer / :attr:graph / :attr:index accessors (e.g. vault.reader.search(...)); this class itself exposes only construction, those accessors, and lifecycle.

Callers must invoke :meth:IndexFacet.build_index before bucket-3 relational/FTS-backed queries (:meth:GraphFacet.get_backlinks, :meth:GraphFacet.get_outlinks, :meth:ReaderFacet.get_similar, :meth:ReaderFacet.get_context, :meth:GraphFacet.get_connection_path, :meth:ReaderFacet.get_toc) or the bucket-4 coordinators :meth:IndexFacet.reindex and :meth:IndexFacet.build_embeddings; otherwise :exc:~markdown_vault_mcp.exceptions.IndexUnavailableError is raised. :meth:IndexFacet.build_index must also precede :meth:start — see :meth:start for the rationale. Bucket-1 file operations (:meth:ReaderFacet.read, :meth:WriterFacet.write, :meth:WriterFacet.edit, :meth:WriterFacet.delete, :meth:WriterFacet.rename, :meth:WriterFacet.write_attachment) and bucket-2 aggregate queries (:meth:ReaderFacet.search, :meth:ReaderFacet.list_documents, :meth:ReaderFacet.stats, …) work on an unbuilt index — bucket-1 hits disk directly; bucket-2 returns whatever is currently in the index (empty on cold start). See issue #525.

Index lifecycle (issues #513, #526, #559). The MCP server lifespan submits a :class:~markdown_vault_mcp.indexing.BuildIndex job to the single-owner :class:~markdown_vault_mcp.indexing.IndexWriter via :meth:IndexFacet.build_index_async and yields immediately. On a warm restart the persisted FTS completeness sentinel (PR #526) causes :meth:IndexFacet.build_index_async to return an already-resolved Future in O(1) without touching the writer queue. On a cold restart the writer thread runs the job asynchronously while the lifespan yields; bucket-3/4 MCP tool clients block on the :class:markdown_vault_mcp._server_queryable.needs_queryable decorator, which calls :meth:IndexFacet.wait_until_queryable with a bounded default timeout (MARKDOWN_VAULT_MCP_BUILD_TIMEOUT_S, default 60s). The library stays honest: bucket-3/4 methods keep the PR #525 raise-immediately contract via :meth:_require_built. Internal callers (lifespan, git pull loop, CLI, direct library users) get the raise contract and handle "not ready" with caller-appropriate logic — never block.

Thread safety (issue #519): every facet operation and lifecycle method is safe to call from any thread, concurrently with other reads and writes from any other thread. Index mutations (FTS + vector index) are serialised by the single-owner :class:~markdown_vault_mcp.indexing.IndexWriter thread (#559); file-mutation operations on disk are serialised via _file_write_lock (RLock) so two MCP write tools racing on the same path do not tear. close() is safe from any thread; after close() the vault must not be used. Cross-method atomicity (e.g. read-then-write without intervening concurrent write) is the caller's responsibility — pass if_match= to write methods for optimistic concurrency. fork() is not supported. See docs/design/design.md "Vault thread-safety contract" for the underlying per-thread SQLite-connection model.

Construction (#1225). Pass source_dir and optional :class:~markdown_vault_mcp.config_sections.vault_settings.VaultSettings. All configuration knobs belong on settings; the five collaborators remain explicit keyword arguments. Omitting settings uses VaultSettings(): read-only, with no chunk overlap. These library defaults remain independent of the server's operator defaults.

Parameters:

Name Type Description Default
source_dir Path

Root directory of the markdown vault.

required
settings VaultSettings | None

Configuration settings. None uses VaultSettings().

None
embedding_provider EmbeddingProvider | None

Provider used to generate embeddings; semantic search is on whenever one is given. Its vectors persist beside the index, or at settings.embeddings_path, and stay in memory without either.

None
summarizer Summarizer | None

Optional summarization backend. Without one the :attr:summarizer accessor raises.

None
git_strategy VersionedStore | None

Optional strategy for background Git tasks, started via :meth:start.

None
on_write WriteCallback | None

Callback invoked after successful writes; see :obj:~markdown_vault_mcp.types.WriteCallback.

None
chunk_strategy str | ChunkStrategy

"heading" (default), "whole", or a custom :class:~markdown_vault_mcp.scanner.ChunkStrategy instance.

'heading'

reader property

Read-only facet: search, read, list, toc, similar, stats, history.

writer property

Document-mutation facet: write/edit/append/delete/rename/attachments.

graph property

Link-graph facet: backlinks, outlinks, broken, orphans, paths.

index property

Index facet: build/reindex/embeddings, readiness, writer status.

summarizer property

Summarize facet: LLM-backed note/subtree summarization.

Raises:

Type Description
RuntimeError

If no summarization backend was configured (set OPENAI_API_KEY or an OpenAI-compatible base URL and install the [summarize] extra).

conventions property

Folder-conventions resolver (disk-read, index-independent).

okf property

OKF detection probe (disk-read, index-independent).

exclude_patterns property

The effective exclusion patterns, including derived ones.

Contains the configured patterns plus the conventions-file patterns derived in __init__ — the list the scanner, reconcile, and incremental index paths actually enforce.

source_dir property

The vault's root directory.

Whether the write tools report the unresolved links a write added.

Read by the write / edit / append MCP tools, which pass it as report_unresolved_links to the writer facet (#1725). The vault library itself reports only when a call asks.

max_attachment_size_mb property

The attachment context-size cap in MB (0 = unlimited).

Enforced by the read / write / fetch MCP tools, not by the vault library itself.

pause_writes()

Block file-mutation write operations until the context exits.

Holds the :attr:_file_write_lock so concurrent :class:DocumentManager document-mutation calls block on the lock until the context exits. Index mutations on the :class:IndexWriter thread continue unaffected — the writer thread does not contend on this lock. Reads and search remain unblocked at the Python level.

sync_from_remote_before_index()

One-time git fetch + ff-only update before build_index().

Intended to run during server startup before the initial index build. No reindex is triggered here: on a cold start build_index() will scan the updated working tree, but on a warm restart build_index_async() short-circuits in O(1) on the existing FTS sentinel and scans nothing. In that case the boot reindex (gated by config.boot_reindex, see #1535) is what actually indexes the tree this pull just updated — with it disabled, the server accepts the index at the resulting HEAD and only reindexes once HEAD moves on.

start()

Start background tasks for this Vault (e.g. git pull loop).

Call :meth:IndexFacet.build_index (or submit it) before :meth:start. The git pull loop reconciles the index with HEAD after every tick through :meth:reconcile_index_with_head (#1532); a tick that finds the index still unbuilt defers to the next one.

git_head()

Return the working tree's current git HEAD.

Returns:

Type Description
str | None

The HEAD revision, or None without a git strategy or when git

str | None

cannot read one.

reconcile_index_with_head(*, source)

Reindex when the index does not yet reflect the current git HEAD.

Level-triggered (#1532): a reindex lost after a pull (the index still building, a writer error, a pull reported as not applied although HEAD advanced) is retried by the next call instead of waiting for the next pull that moves HEAD. Called by the pull loop on every tick, by webhook deliveries and by the git_sync tool.

Parameters:

Name Type Description Default
source str

Which caller asked, for the log line.

required

Returns:

Type Description
ReconcileOutcome

What happened; "current" without a git strategy.

adopt_boot_reindex(pending, head)

Count the submitted boot reindex as the reconcile of head.

Reconciles defer until it finishes rather than pausing writes behind it and scanning twice; head is recorded once it succeeds.

Parameters:

Name Type Description Default
pending Future[Any]

The boot reindex returned by reindex_async().

required
head str | None

The HEAD left by the startup sync, which it covers.

required

mark_index_reconciled(head)

Record that the index reflects head without reindexing.

Used by the server's startup once the boot reindex has covered the tree at head, or when that reindex is disabled by configuration.

Parameters:

Name Type Description Default
head str | None

The HEAD the index reflects.

required

force_pull()

Pull from the git remote synchronously.

Thin public facade over :meth:~markdown_vault_mcp.git.Syncer.force_pull used by the GitHub webhook handler so the store stays an implementation detail.

The strategy self-quiesces around its own merge: it pauses new writes (via the :meth:pause_writes callable wired in :meth:__init__ through set_write_quiescer) and drains the deferred-commit queue before the merge, so a write that landed just before the pull is committed first and the merge runs on a clean tree (#571). This facade therefore no longer wraps pause_writes itself.

Returns:

Type Description
PullResult | None

class:~markdown_vault_mcp.git.PullResult from the strategy, or

PullResult | None

None when no git strategy is configured.

stop()

Stop background tasks (e.g. git pull loop) without closing the vault.

Safe to call multiple times. A no-op if no pull loop was started. The SQLite connection and write callback remain open; only the pull loop thread is signalled to stop.

end_commit_scope(scope)

Close a tool call's commit scope, grouping its writes into one commit.

Called by :class:~markdown_vault_mcp._commit_scope.CommitScopeMiddleware when a tool call returns. Never blocks: the marker is queued behind the writes that call fired, and the dispatcher flushes it in order.

Parameters:

Name Type Description Default
scope CommitScope

The scope to close.

required

close()

Release resources held by the vault.

Flushes deferred embeddings and pending write callbacks, then closes the SQLite connection and git strategy.