3.1¶
Released July 8, 2026.
Reconstructed after the fact
This page is a best-effort backfill written in August 2026, not a
contemporaneous release note. It was reconstructed from the commit range
(v3.0.4...v3.1.0), the pull requests in it, and the issues those pull
requests closed
(#1058).
Every claim below links its evidence, but the page is weaker evidence than
a note written at release time.
3.1 is the scale-hardening release. A single outside reporter ran
markdown-vault-mcp against a 4,400-document home directory and a symlink farm
and filed six of the issues fixed here, so discovery, watching and semantic
recall now behave the same on a large real vault as on a small test one. The
four in-panel MCP App views were also rebuilt on a shared visual foundation,
and two new tools landed. Upgrading from 3.0 touches only Python API consumers:
the configuration dataclass is now ProjectConfig with no VaultConfig alias,
and an EMBEDDINGS_PATH value ending in .npy needs a look.
This is the release where other people's vaults broke it. One outside reporter ran markdown-vault-mcp against a 4,400-document home directory and a symlink farm, and filed six of the issues behind what follows. The headline work is not a new feature: it is that discovery, watching and semantic recall now behave the same on a large real vault as they did on a small test one.
Alongside that, the four in-panel MCP App views were rebuilt on a shared visual foundation, and two new tools landed.
Upgrade notes¶
Read these before upgrading. Two affect existing deployments.
VaultConfig is now ProjectConfig, with no alias¶
The configuration dataclass was renamed from VaultConfig to ProjectConfig
(#767). No
back-compatible alias was kept, so from markdown_vault_mcp.config import
VaultConfig now raises ImportError. Only Python API consumers are affected.
Environment variables, MCP tools and the CLI are unchanged.
The rename was forced by an upstream template gate,
tests/test_config_wizard_drift.py, which hard-imports ProjectConfig and
could not collect against this repository at all. The same change taught the
gate to see configuration reads that are delegated into config_sections/
submodules, which it had previously been scanning past. Roughly 40 domain
environment variables were unchecked before this landed.
Check EMBEDDINGS_PATH if it ends in .npy¶
Setting MARKDOWN_VAULT_MCP_EMBEDDINGS_PATH to a path with a .npy extension
could destroy the vector store
(#819). The
loader probed for the sidecar by appending .npy to the configured base, while
save and load derived it with Path.with_suffix. The two agreed only when the
base had no extension. With embeddings.npy configured, the loader looked for
embeddings.npy.npy, found nothing, cold-built an empty index, and the next
save overwrote the real store.
The reporter lost a 68,796-vector store to a 604-chunk watcher delta on the
first serve after building it with index --force. embeddings_status
carried the same defect and reported chunk_count=0 for a store that existed.
Both call sites now derive with with_suffix. If you configured an extension
and your store looks empty, rebuild with index --force.
New setting: FILE_WATCHER_ROOT_FLOOR¶
MARKDOWN_VAULT_MCP_FILE_WATCHER_ROOT_FLOOR (default true) keeps the
non-recursive watch on the vault root, so root-level .md changes still
trigger a reindex. Set it to false to register zero watches rooted at the
source directory, at the cost of root-level files being picked up only by
scans.
This exists for macOS. A non-recursive watchdog watch is emulated there, and
still opens an OS-level recursive FSEvents stream over the whole tree, so a
vault rooted at $HOME produces repeated "would like to access data from other
apps" consent prompts naming the Python binary
(#823).
Highlights¶
Large vaults are no longer minutes behind¶
File discovery globbed **/*.md across the whole vault and applied exclude
patterns per file afterwards, so every excluded subtree was fully descended on
every scan
(#822). On a
home-directory vault with 4,400 documents and 23 exclude patterns, one
detect_changes call took 5,043 seconds. A second run sat 28 minutes inside a
single opendir system call before it was killed. With the watcher on, any
churn started the scan again, so the process scanned all day and
save-to-searchable latency was measured in minutes.
Discovery now prunes a directory before descending into it, but only when the exclude patterns provably cover everything beneath. The per-file filter stays as the correctness layer. The same scan now takes 3.2 seconds across 25,263 directories, and save-to-searchable drops to about 15 seconds.
The watcher was rebuilt on the same pruning helper. Instead of one recursive watch on the vault root, it derives watch roots from the root's immediate children minus the provably excluded ones: 94 scoped watches on the reporter's vault, none rooted at the source directory.
That re-rooting also fixes a second defect in the same issue. The watcher now
decides whether a path is hidden relative to the watch root that delivered the
event, rather than relative to the vault root. A vault whose content lives
under a dot directory, such as .claude/ beneath $HOME, previously never
reindexed from its own edits.
Two follow-ups landed in the same release. Watch roots are resolved to their
real paths, because watchdog's FSEvents emitter delivers realpath-prefixed
events that the root-relative filter was dropping silently, leaving the watcher
live but blind on symlinked layouts
(#849). And the
vault's own state directory and .git are never watch roots, after the scoped
watch work reintroduced the self-feedback reindex loop that
#720 had fixed:
reindex() writes tracker state unconditionally, that write is an event under
the state-directory watch, and the loop sustains itself
(#830).
Semantic search recall no longer depends on limit¶
Semantic search sized its candidate pool from the caller's limit before
grouping chunks by document, which made the pool a recall cap
(#820). When a
few chunk-heavy documents filled the pool, a smaller document whose best chunk
ranked just past it disappeared from the results, even when it was the
best-scoring document for the query.
On a 6,916-chunk vault, one query's true best document scored 0.685 and was absent at every limit from 3 to 12, while the returned top result scored 0.495. It appeared only at limit 13 or higher. Scores were also not comparable across limits, because the pool changed what got grouped.
The pool is now floored at 1,000. Because VectorIndex.search already scans
and sorts the whole index anyway, a wider pool costs only the slice at the end.
No extra distance computation happens.
Chunking gained overlap, and a ceiling¶
Adjacent chunks now share a configurable within-section overlap, so a passage straddling a chunk boundary stays retrievable from either side (#791).
Separately, the derived chunk-character cap has an upper bound
(#790). It
scaled linearly with the embedding model's context length and had no ceiling,
so an 8,192-token model produced a roughly 22,900-character cap. That is the
regime a previous release moved away from after the fastembed ONNX path froze
on oversize chunks, and picking any long-context model silently re-entered it.
Pass -1 to opt back into unbounded behaviour.
Vault Views rebuilt as "Paper"¶
The four in-panel MCP App views were restyled onto a shared warm, editorial
foundation with serif headings and one accent colour, aware of light and dark
(#809). Paper
tokens map onto the host's existing --color-* variables with Paper values as
fallback, so a host that pushes its own theme still wins.
Data fetching, routing and tool wiring are unchanged. This was a restyle, and no tool was renamed or removed.
- Foundation: token layer, fonts and tab strip (#811)
- Context Card: folder breadcrumb, relative timestamp, type and status chips, collapsible sections with count badges, similarity scores with progress bars (#812)
- Vault Browser: restyled tree with attachment glyphs, active-row highlighting, and the hybrid search field (#813)
- Note Preview: table of contents, collapsible frontmatter and tags, copy actions
- Graph Explorer: level-of-detail labels
As a prerequisite, the single 52 KB app.src.html was split into modular
partials assembled at build time by build_spa.py
(#810).
Move a whole folder in one call¶
move_folder(old_dir, new_dir) moves a subtree and rewrites every affected
link across the vault in a single pass
(#511).
Previously an agent had to enumerate the subtree and call rename per file,
which corrupted links between documents inside the moved subtree: when file A
moved, its backlink in still-unmoved sibling B was rewritten to A's new home,
then B itself moved, and the intermediate rewrites interleaved badly. Computing
the full old-to-new mapping first and rewriting once afterwards is what makes
this correct.
The directory move is all-or-nothing and validates before touching anything, so a target collision aborts with nothing moved. Link rewrites stay best-effort and report how many succeeded, matching the existing single-file behaviour.
Table of contents for a folder¶
get_toc returns a heading outline for a folder or subtree, not just one note
(#773). The
per-note outline was previously reachable only through the toc://vault/{path}
resource, which left clients that surface tools but not resources with no way
to get it at all.
Skipped notes are visible without reading container logs¶
A note whose YAML frontmatter failed to parse was dropped from the index and
reported only as a warning in the container log
(#775).
get_index_status said queryable with zero dirty paths and no error, read
said the document did not exist, and the note surfaced only indirectly as
broken inbound links from elsewhere. A parse-skipped note was indistinguishable
from one that had not synced yet.
get_index_status now returns skipped_files with a category and detail per
path. Follow-up work separates a true internal_error from a
parse_error, so tooling built on the field can tell "this document has a data
problem you can fix" from "the indexer itself hit a bug"
(#802).
Other changes¶
Indexing and watching¶
- An already-indexed file that fails to re-hash is kept rather than purged. It was dropped from the disk snapshot and then computed as deleted, which removed a live document from FTS and its embedding until a later scan (#831).
- Transient descriptor exhaustion is retried with backoff while hashing during a scan, instead of silently dropping the file from the scan (#821).
- Excluded files are invisible in
build_indexfor file-shaped patterns such as**/*.draft.md, matching the contract that discovery pruning already met for directory-shaped ones (#832). - An unreadable subtree is logged rather than silently skipped, and a watcher
that ends up with zero active watches is reported above
INFOinstead of logging success (#835).
Diagnostics¶
- The
indexandreindexCLI commands no longer swallow internal embedding failures. A blanketexcept ValueError: pass, intended to skip when embeddings are not configured, also caught an internal consistency failure, so a corrupt sidecar producedIndexed N documentsand a zero exit code (#774). - Internal semantic-search failures surface from
get_context. httpxper-request logs are quiet at the default level. OneINFOline per Ollama embed batch flooded the log during a build (#792).- One malformed prompt no longer aborts registration of every prompt after it
(#799), and
prompts are built from a synthetic signature instead of
exec().
MCP Apps¶
- Graph and context app tools no longer fail on a note larger than
MAX_NOTE_READ_BYTES. Four sites called the size-cappedread()purely to extract a title for a node label, so one 375 KB note failed the whole tool. A metadata accessor backed by the index is used instead (#855). - Graph canvas colours are resolved by probe, so nodes are not black on Claude Desktop.
- The Vault Explorer view honours host container dimensions on mobile.
Tooling and plumbing¶
- Every tool carries a human-readable
titleannotation. VS Code's MCP client honours onlytitleandreadOnlyHintamong annotations, so tools had been rendering under their raw machine names (#751). - Eight
copier updatecommits took the upstream template from v2.1.1 to v2.10.1. Most of that range is developer-facing: a diff-scoped structural health gate, Vale linting of user-facing docs, a configuration-wizard coverage gate, and aDOMAIN-COMMANDSsentinel for domain CLI subcommands. The in-browser configuration generator on the documentation site arrived the same way.
Shipped earlier, in the 3.0 patch line¶
The first draft of this page was written against the v3.0.0...v3.1.0 range
rather than v3.0.4...v3.1.0, so it narrated several fixes that had already
reached users in a 3.0 patch release. Rather than drop them, this section
keeps each one with a pointer to where it is described in full, so a reader who
followed a link expecting it on this page still finds it.
- Escaped-pipe wikilinks,
[[path\|alias]], resolve instead of being reported broken. Shipped in v3.0.2 (#731, 3.0 notes). - The vector sidecar is written atomically, a corrupt sidecar self-heals on load, and row-count parity is enforced. Shipped in v3.0.2 (3.0 notes).
read(path, section=...)returns the whole section rather than its first chunk, and whole-documentreaddegrades toNoneon malformed frontmatter instead of raising. Shipped in v3.0.3 (#741, 3.0 notes).- Git sync resolves its comparison ref as
origin/<branch>rather than@{upstream}. Shipped in v3.0.4 (3.0 notes).
Thanks¶
This release leans heavily on reports from outside the repository, several carrying measurements, stack samples and fault-injection reproductions:
Two more outside reports shape this release without belonging to it. @Finomosec reported the idle reindex loop (#720), fixed in the 3.0 patch line and credited on the 3.0 page. The scoped-watch work in this release reintroduced that loop, and #830 fixed it again here. @Denzilla04 reported the escaped-pipe wikilinks (#731), which also shipped in the 3.0 patch line.
Patch releases¶
No patch releases yet.
All changes¶
See CHANGELOG.md for the full commit-level list, or the v3.0.4 to v3.1.0 comparison.