Configuration¶
Markdown Vault MCP is configured via environment variables with the
MARKDOWN_VAULT_MCP_ prefix.
See fastmcp-pvl-core's README for the full list of universal
variables (MARKDOWN_VAULT_MCP_TRANSPORT, MARKDOWN_VAULT_MCP_HOST,
MARKDOWN_VAULT_MCP_PORT, MARKDOWN_VAULT_MCP_HTTP_PATH,
MARKDOWN_VAULT_MCP_BASE_URL, auth vars, etc.).
Prefer a guided setup?
Use the Configuration Generator to answer a few
questions and copy a ready-made .env, Docker, or Claude config.
Configuration is validated at startup
Numeric variables are validated against the Type column below (such as int ≥ 1). A non-numeric or out-of-range value makes the server fail fast at startup with a ConfigurationError naming the offending setting, rather than silently falling back to a default. A typo in an env var surfaces immediately instead of producing surprising behavior later.
Tool visibility¶
Operators can trim which tools this instance exposes. Each variable takes a comma-separated list of explicit tool names:
MARKDOWN_VAULT_MCP_TOOLS_ALLOW: expose only the listed tools.MARKDOWN_VAULT_MCP_TOOLS_DENY: hide the listed tools.
Hidden tools disappear from tools/list and are rejected on tools/call;
resources and prompts are unaffected. Setting both variables, or setting one
to a value with no names in it, is a startup error. A name matching no
registered tool is ignored, but an allowlist that matches nothing logs a
startup WARNING since the instance then exposes zero tools. See
fastmcp-pvl-core's README for the full semantics.
Background tasks¶
Every Markdown Vault MCP instance wires a background-task backend at startup, so
a tool registered with task=True works with no extra setup. One variable
picks the backend:
MARKDOWN_VAULT_MCP_TASKS_URL:memory://runs tasks in-process and loses them on restart;redis://...is durable and shared across processes.
Unset, a redis:// MARKDOWN_VAULT_MCP_KV_STORE_URL is reused for tasks as
well, so a single URL configures every stateful subsystem. With neither set,
the backend falls back to memory://, which the server logs at startup when
running over HTTP. The queue name comes from the MARKDOWN_VAULT_MCP prefix, so
two servers sharing one Redis do not share a queue.
Worker tuning stays on the native FASTMCP_DOCKET_* variables
(FASTMCP_DOCKET_CONCURRENCY and friends, listed in .env.example). Set the
backend through MARKDOWN_VAULT_MCP_TASKS_URL rather than
FASTMCP_DOCKET_URL: the former wins when both are set, and the server warns
about the disagreement.
Core¶
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
MARKDOWN_VAULT_MCP_SOURCE_DIR |
path | (none) | Yes | Path to the markdown vault directory. Symbolic links inside the vault are followed on Python 3.13+ (3.11/3.12 do not follow symlinks); cyclic links hang the scan, so symlink-farm layouts must be acyclic |
MARKDOWN_VAULT_MCP_READ_ONLY |
bool | false |
No | Set to true to hide the write tools and serve a search-only vault. See the upgrade note below |
MARKDOWN_VAULT_MCP_INDEX_PATH |
path | in-memory | No | Path to the SQLite FTS5 index file; set for persistence across restarts |
MARKDOWN_VAULT_MCP_EMBEDDINGS_PATH |
path | disabled | No | Path to the numpy embeddings file; required to enable semantic search |
MARKDOWN_VAULT_MCP_STATE_PATH |
path | {SOURCE_DIR}/.markdown_vault_mcp/state.json |
No | Path to the change-tracking state file |
MARKDOWN_VAULT_MCP_INDEXED_FIELDS |
csv | (none) | No | Comma-separated frontmatter fields to promote to the tag index for structured filtering. Recorded as a warm-restart key, so setting or changing it cold-rebuilds the index once on next startup. When SEARCHABLE_FIELDS is unset, it defaults to this value, so a field indexed for filtering is also keyword/semantically searchable out of the box; a change here then also activates context-enriched embeddings and re-embeds the vault once. Set SEARCHABLE_FIELDS explicitly (or to none) to decouple. One-time rebuild on upgrade: a vault with INDEXED_FIELDS already configured cold-rebuilds once on its first startup after upgrading to the release that introduced this tracking (#927), and (if embeddings are enabled and SEARCHABLE_FIELDS was unset) also re-embeds once as the inherited default takes effect. No action is required; subsequent restarts warm-restart as normal |
MARKDOWN_VAULT_MCP_REQUIRED_FIELDS |
csv | (none) | No | Comma-separated frontmatter fields required on every document; documents missing any are excluded from the index |
MARKDOWN_VAULT_MCP_EXCLUDE |
csv | (none) | No | Comma-separated glob patterns to exclude from scanning (such as .obsidian/**,.trash/**) |
MARKDOWN_VAULT_MCP_TITLE_FIELD |
string | title |
No | Frontmatter field used as the document title. Resolution order: this field → title (when a custom field is set) → first H1 heading → filename stem. Recorded as a warm-restart key, so changing it cold-rebuilds the index once on next startup |
MARKDOWN_VAULT_MCP_SEARCHABLE_FIELDS |
csv | defaults to INDEXED_FIELDS |
No | Comma-separated frontmatter fields whose scalar values are indexed into the FTS summary column (chunk-0 row of each document), making them keyword-searchable (including summary:term column filters). They are also prefixed to first-chunk embedding text (format v2), independent of EMBED_CONTEXT. When unset, defaults to INDEXED_FIELDS; set explicitly to a different field list to diverge from that default, or to the sentinel none for no searchable fields at all ("filterable but not searchable"). An empty string is treated the same as unset, matching every other list-valued config var. Recorded as a warm-restart key, so setting or changing it cold-rebuilds the index and re-embeds the vault once on next startup |
MARKDOWN_VAULT_MCP_TEMPLATES_FOLDER |
string | _templates |
No | Relative folder path used by the create_from_template prompt to discover/read template files |
MARKDOWN_VAULT_MCP_PROMPTS_FOLDER |
path | (none) | No | Path to a directory of .md prompt files that extend or override built-in prompts. Each declared argument name must be a plain Python identifier (letters, digits, underscore; not a keyword); a prompt with a non-conforming argument name is skipped with a logged warning. |
MARKDOWN_VAULT_MCP_CONVENTIONS_FILE |
string | _conventions.md |
No | Filename of the per-folder conventions files surfaced to clients at write time. Must be a bare .md filename without glob characters (*?[]). Set to none to disable folder conventions. Convention files are excluded from the search index but stay readable; existing notes matching the name are removed from the index on the next boot reconcile. Note: setting MARKDOWN_VAULT_MCP_INSTRUCTIONS replaces the default server instructions entirely, including the sentence that points clients at get_conventions; mention conventions in your custom instructions if you rely on them. |
MARKDOWN_VAULT_MCP_OKF_MODE |
string | auto |
No | OKF (Open Knowledge Format) read semantics. With auto (the default), read-side annotations (note type, lifecycle status, staleness, trust tier) switch on in search, read, get_context, and stats when the vault declares an okf_version field in its root index.md frontmatter. Use off to disable OKF semantics entirely, or on to force them for an undeclared vault. Annotations are read-only either way: this setting never changes write behavior. Note: setting MARKDOWN_VAULT_MCP_INSTRUCTIONS replaces the default server instructions entirely, including the OKF guidance sentence. |
MARKDOWN_VAULT_MCP_OKF_WRITE |
bool | false |
No | OKF enforced write layer. When true on an OKF-active vault, every write and edit stamps generated: {by, at} provenance and clears any verified attestation (a content change invalidates prior review); rename does not. The actor is human:<subject> when the caller is authenticated, otherwise a tool actor. On a content write it also keeps the affected folder's log.md and index.md current as secondary writes (failures are logged and skipped, never rolling back the note write). Enabling it also exposes the okf_verify tool. Requires OKF_MODE to be auto or on; a true value with OKF_MODE=off is a configuration error. See the OKF guide. |
MARKDOWN_VAULT_MCP_OKF_VERIFY |
string | elicit |
No | How the okf_verify tool confirms a human review. This applies only when OKF_WRITE is on, which gates the tool. With elicit (the default), the tool issues an MCP elicitation asking the human to confirm the review, and writes the verified entry only on an affirmative reply. It fails closed (a tool error, nothing written) when the client cannot elicit or the human declines, so a model that holds the human's token cannot self-attest a note as human-reviewed. Use trust-auth to attribute to the authenticated caller with no confirmation (refuses under auth mode none; safe only when the sole caller is a human-driven UI, not an agent), or off to hide the tool so verified is set only by external tooling. A non-default value with OKF_WRITE off is a configuration error. See the OKF guide. |
Upgrading from 3.x: the write tools are now on by default
Through 3.1, MARKDOWN_VAULT_MCP_READ_ONLY defaulted to true, so a
server that never set it exposed search and reading only. It now defaults
to false, which unhides the write-tagged tool set.
On a default install that means write, edit, append, delete,
rename, move_folder, and fetch — enough for a connected model to
create, rewrite, and delete notes in your vault. The rest of the
write-tagged set stays behind its own gates and stays hidden even after
this flag flips: git_sync still needs managed git mode
(GIT_REPO_URL), create_upload_link still needs an HTTP transport with
BASE_URL, and the okf_* tools still need OKF semantics active
(OKF_MODE, which under the default auto switches on only for a vault
declaring okf_version) — with okf_verify additionally requiring
OKF_WRITE.
Doing nothing on upgrade widens what the server may do to your data. If you relied on the old default, set the variable explicitly before you restart:
MARKDOWN_VAULT_MCP_READ_ONLY=true
Deployments that already set it either way are unaffected — the value you set still wins. The default changed because a vault server that cannot write to the vault is a search index, and reaching the rest of the product required discovering a flag first.
The file watcher never watches the directories that hold INDEX_PATH,
EMBEDDINGS_PATH, and STATE_PATH (or .git), so the writes the server makes
while indexing do not trigger it again. If you place one of these paths inside a
content directory, that whole top-level directory stops being watched, so keep
them at the vault root or outside the vault to keep sibling content watched live.
Index Build Timeout¶
MARKDOWN_VAULT_MCP_BUILD_TIMEOUT_S¶
Default: 60 (seconds).
Maximum time the MCP-layer needs_queryable decorator waits for
the FTS index to become queryable before raising
IndexUnavailableError(reason="timeout") to the client. Applied to bucket-3/4 tool and resource calls during
a cold-start background FTS build. Increase for large vaults
where the initial scan takes longer; decrease for tighter feedback
on stuck builds.
MARKDOWN_VAULT_MCP_DRAIN_TIMEOUT_S¶
Default: 60 (seconds).
Maximum time an index-querying read tool (search, list_documents,
list_folders, list_tags, stats, get_recent, get_backlinks,
get_outlinks, get_broken_links, get_similar, get_context,
get_orphan_notes, get_most_linked, get_connection_path) waits for
the IndexWriter to drain when called with wait_for_pending_writes=true. On
timeout the tool answers from the current index rather than raising
(best-effort fresh read) and reports index_stale=true in the response's
_meta. Increase for large vaults where reindex / build_embeddings
jobs take longer; decrease for faster client feedback when the index has
chronic backlog.
Server Identity¶
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_SERVER_NAME |
string | markdown-vault-mcp |
MCP server name shown to clients; useful for multi-instance setups |
MARKDOWN_VAULT_MCP_INSTRUCTIONS |
string | (auto) | System-level instructions injected into LLM context; defaults to a description that reflects read-only vs read-write state |
MARKDOWN_VAULT_MCP_DISABLE_APPS_UI |
bool | false |
Hide MCP-Apps UI tools (browse_vault, show_context) from the tool listing for clients that do not render MCP Apps panels (saves a few listing tokens) |
MARKDOWN_VAULT_MCP_HTTP_PATH |
path | /mcp |
HTTP endpoint path for streamable HTTP transport (serve --transport http) |
MARKDOWN_VAULT_MCP_BASE_URL |
url | (none) | Public base URL of the server (such as https://mcp.example.com). Required for OIDC auth, MCP Apps domain computation, and the one-time transfer link tools |
MARKDOWN_VAULT_MCP_KV_STORE_URL |
url | file:///data/state |
Unified key-value backend for HTTP session persistence (the events keyspace is namespaced inside the directory). file:///path survives restarts; memory:// for dev (lost on restart). Preferred over EVENT_STORE_URL. |
MARKDOWN_VAULT_MCP_EVENT_STORE_URL |
url | (unset) | Legacy alias for KV_STORE_URL; honoured only when KV_STORE_URL is unset, and logs a one-shot deprecation warning. Prefer KV_STORE_URL. |
MARKDOWN_VAULT_MCP_APP_DOMAIN |
string | (auto) | Override the Claude app domain used for MCP Apps iframe sandboxing. Auto-computed from BASE_URL when not set |
FASTMCP_LOG_LEVEL |
string | INFO |
Log level for FastMCP internals (DEBUG, INFO, WARNING, ERROR). -v CLI flag overrides both app and FastMCP loggers to DEBUG |
FASTMCP_ENABLE_RICH_LOGGING |
bool | true |
Rich key=value text by default. Set to false for one-JSON-object-per-record output, recommended for production / log-aggregator deployments |
Search Ranking and Snippet Truncation¶
| Env var | Default | Type | Notes |
|---|---|---|---|
MARKDOWN_VAULT_MCP_CHUNKS_PER_FILE |
2 |
int ≥ 1 | Maximum number of matching sections returned per file (field collapsing). 0 is rejected. Per-call override available on the search and get_similar tools; get_context defaults to 1 for compact dossiers. |
MARKDOWN_VAULT_MCP_SNIPPET_WORDS |
200 |
int ≥ 0 | Approximate word budget for SearchResult.content. 0 returns the full chunk. Per-call override on the search tool. |
MARKDOWN_VAULT_MCP_LENGTH_DOWNWEIGHT_ALPHA |
0.25 |
float ≥ 0 | Strength of the per-channel length downweight: score / (1 + alpha · log(chunk_count)). 0 disables. Applied only to search modes (keyword/semantic/hybrid); get_similar and get_context.similar skip the downweight because grouping already handles multi-chunk dedup (see #472). Operator-only (no per-call override). |
MARKDOWN_VAULT_MCP_FOLDER_WEIGHTS |
(none) | prefix:weight,... |
Folder-prefix score multipliers applied to every search mode (keyword, semantic, hybrid) just before results are grouped per file. A prefix matches a folder exactly or as a parent (Projects matches Projects/2026 but never ProjectsArchive); the deepest matching prefix wins; weights must be > 0 (0.5 demotes, 2 promotes). Query-time only: takes effect immediately, no reindex. get_similar/get_context are deliberately unaffected. |
MARKDOWN_VAULT_MCP_FTS_WEIGHTS |
(none) | column:weight,... |
Per-column BM25 weights for keyword ranking, persisted into the FTS5 rank configuration at startup. Columns: path, title, folder, heading, content, summary; weights must be ≥ 0; unlisted columns weigh 1.0. Unset (or all 1.0) resets to the default bm25(). Takes effect on restart, no reindex. |
MARKDOWN_VAULT_MCP_MAX_CHUNK_WORDS |
400 |
int ≥ 1 | Hard cap on chunk word count. The adaptive chunker recursively re-splits at deeper heading levels (H1 → H6); anything still oversize (preambles and no-headings documents included) is further fragmented on paragraph and word boundaries. Match this to the embedding model's context window. The default FastEmbed model BAAI/bge-small-en-v1.5 has only a 512-token context, so the default 400 words (≈ 600 tokens) could exceed it; MAX_CHUNK_CHARS (derived as ~1434 chars for this 512-token model) caps each chunk to the model's context, so manual adjustment is not needed. For long-context models such as nomic-embed-text-v1.5 (8192 tokens natively; Ollama serves it with n_ctx_train=2048 by default, see OLLAMA_HOST) the default 400 is comfortable. Setting a high value (such as 100000) effectively disables the cap. When CHUNK_OVERLAP_WORDS is non-zero, an overlapped fragment can exceed this cap by up to the overlap word count. |
MARKDOWN_VAULT_MCP_MAX_CHUNK_CHARS |
(bounded default) | int ≥ 1, or -1 |
Hard cap on chunk character count, enforced by the chunker alongside the word cap (whichever budget is hit first triggers a split). Applies to token-dense content (CJK, code, tables). Unset uses the safe default min(1500, round(context_length × 2.8)): retrieval quality peaks at ~256 to 512 tokens per chunk regardless of the model's context, and 1500 chars keeps the fastembed/ONNX path clear of the out-of-memory regime seen with oversize chunks (#306). For BAAI/bge-small-en-v1.5 (512-token context) this derives to ~1434 chars (unchanged from prior releases). A model with a shorter context (below ~536 tokens) uses its own smaller round(context_length × 2.8) value; a longer-context model is capped at the 1500 ceiling; an unknown context (no provider, or an unreachable Ollama instance) falls back to 1500. Set a positive integer to force an exact cap. Set -1 to opt into unbounded context-scaling (round(context_length × 2.8) with no ceiling, or 1500 when the context is unknown), which reproduces the pre-#790 context-scaling and can OOM-kill the host on the fastembed/ONNX path with a long-context model; use only with Ollama (out-of-process) or a remote provider. Like MAX_CHUNK_WORDS, this changes the chunk index, so a reindex is required for a new value to take effect. When CHUNK_OVERLAP_WORDS is non-zero, an overlapped fragment can exceed this cap by up to the overlap word count. |
MARKDOWN_VAULT_MCP_CHUNK_OVERLAP_WORDS |
40 |
int ≥ 0 | Words of overlap copied from the previous fragment onto each budget-split fragment of the same heading section. Improves retrieval recall at arbitrary (non-heading) split points; 0 disables. Overlap applies only where heading-based splitting has been exhausted and the chunker falls back to paragraph, line, or word splitting, never across a heading boundary. It is excluded from the warm-restart key, so changing it (or the default on upgrade) applies to new builds and any note re-indexed after an edit, without forcing a rebuild. Run reindex to apply a new value to an existing vault. A non-zero value can push an overlapped fragment past MAX_CHUNK_WORDS or MAX_CHUNK_CHARS by up to this many words; the excess is small and does not risk an out-of-memory condition. |
The first three knobs adjust ranking and rendering and take effect immediately. MAX_CHUNK_WORDS and MAX_CHUNK_CHARS change the chunk index; a reindex is required for a new value to take effect. Because the char cap is derived from the embedding model, changing the embedding model also changes the effective chunk boundaries: the FTS index is re-chunked, not just the embeddings. Changing the embedding model (or the explicit MAX_CHUNK_CHARS override) rejects the warm-restart short-circuit and triggers an automatic cold rebuild of the index on the next startup. No manual reindex is needed. One-time rebuild on upgrade: an existing embedding-enabled vault upgrading from a release before this one will also cold-rebuild once on next startup, because builds now record chunking provenance (embedding model + override) that older builds did not track. No action is required; subsequent restarts warm-restart as normal. FTS-only vaults (no embedding provider) are unaffected. Upgrading note: search now returns snippets (≤ ~200 words) by default. Set snippet_words=0 per call or MARKDOWN_VAULT_MCP_SNIPPET_WORDS=0 globally to restore full-chunk output. Existing vaults are also re-chunked on the next reindex when MAX_CHUNK_WORDS is set. Bounded-cap upgrade (#790): the smaller default (min(1500, round(context_length × 2.8)), reduced from the old unbounded round(context_length × 2.8)) applies to new builds and whenever the embedding model or an explicit MAX_CHUNK_CHARS changes. It does not auto-apply on upgrade. The warm-restart key is the model name together with the explicit override rather than the derived cap, so an existing vault on a long-context model with no explicit MAX_CHUNK_CHARS keeps its current coarser chunk boundaries. That old index stays usable; it is only larger-chunked. Run reindex when you want the smaller default to take effect, noting that a full re-embed can take many minutes on a large vault. bge-small vaults are unaffected. To keep unbounded scaling, set MAX_CHUNK_CHARS=-1.
Search and Embeddings¶
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_EMBEDDING_PROVIDER |
string | auto-detect | Embedding provider: openai, ollama, or fastembed. Breaking change from EMBEDDING_PROVIDER in older versions |
MARKDOWN_VAULT_MCP_EMBED_CONTEXT |
bool | false |
Enrich each chunk's embedding input with the note title, the chunk heading, and (on the first chunk) the SEARCHABLE_FIELDS values, improving semantic recall for short or context-poor chunks. The raw note content on disk and in search snippets is unchanged. The active format is recorded in the vector sidecar, so flipping this (or changing SEARCHABLE_FIELDS) re-embeds the whole vault once on next startup |
MARKDOWN_VAULT_MCP_EMBED_TIMEOUT_S |
float | 30.0 |
Per-request wall-clock budget in seconds for a single embedding HTTP call (OpenAI/Ollama). The local FastEmbed backend runs in-process with no network call and ignores this. CPU-only or large-model workloads may need 60-120 s; raise this if batches time out. |
MARKDOWN_VAULT_MCP_EMBEDDING_BATCH_SIZE |
int | 4 |
Number of chunks sent per embedding request. Smaller batches shorten each request (useful under a tight timeout on slow models) at the cost of more round-trips. |
OLLAMA_HOST |
url | http://localhost:11434 |
Ollama server URL. Not MARKDOWN_VAULT_MCP_-prefixed |
OPENAI_API_KEY |
string | (none) | OpenAI API key for the OpenAI embedding provider. Not MARKDOWN_VAULT_MCP_-prefixed |
MARKDOWN_VAULT_MCP_OPENAI_BASE_URL / OPENAI_BASE_URL |
url | https://api.openai.com/v1 |
OpenAI-compatible API base URL for embeddings |
MARKDOWN_VAULT_MCP_OPENAI_EMBEDDING_MODEL / OPENAI_EMBEDDING_MODEL |
string | text-embedding-3-small |
OpenAI-compatible embedding model name |
MARKDOWN_VAULT_MCP_OLLAMA_MODEL |
string | nomic-embed-text |
Ollama embedding model name |
MARKDOWN_VAULT_MCP_OLLAMA_CPU_ONLY |
bool | false |
Force Ollama to use CPU only |
MARKDOWN_VAULT_MCP_FASTEMBED_MODEL |
string | BAAI/bge-small-en-v1.5 |
FastEmbed model name |
MARKDOWN_VAULT_MCP_FASTEMBED_CACHE_DIR |
path | FastEmbed default | FastEmbed model cache directory (in Docker, stored under /data/state/fastembed) |
Embedding provider auto-detection
When MARKDOWN_VAULT_MCP_EMBEDDING_PROVIDER is not set, the server tries providers in this order:
- OpenAI, if
OPENAI_API_KEYis set - Ollama, if
OLLAMA_HOSTis reachable - FastEmbed, if the
fastembedpackage is installed
Both API providers speak the OpenAI-compatible embeddings protocol through the official openai SDK: the ollama provider is a preset that targets {OLLAMA_HOST}/v1 with no key required. The OLLAMA_* settings and their behavior are unchanged; OLLAMA_CPU_ONLY uses Ollama's native API, which is the only way to request CPU-only inference.
Explicit vs. auto-detect failure handling: when you set MARKDOWN_VAULT_MCP_EMBEDDING_PROVIDER to a specific backend and it cannot be constructed at startup — a missing dependency, missing/empty credentials, or an unrecognised value — the server fails fast with a ConfigurationError rather than silently falling back to keyword-only search. (An unreachable Ollama/OpenAI service does not prevent startup — the provider still loads; the failure surfaces later as an embedding error during index build.) When the variable is unset (auto-detect) and no backend is available, the server logs a warning and continues with semantic search disabled. Set the variable explicitly if you want a missing provider to be a hard startup error.
Summarization¶
Powers the optional summarize tool. The backend speaks the OpenAI-compatible chat-completions API, so one endpoint/key/model triple covers OpenAI, a local Ollama, the Anthropic compatibility endpoint, vLLM, and any other compatible server. The tool is only registered when a backend is configured: an API key, or an explicit base URL for local endpoints that need no key. Requires the openai SDK: pip install 'markdown-vault-mcp[summarize]'.
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_SUMMARIZE_PROVIDER |
string | auto-detect | Summarization backend. Currently openai. Leave unset to auto-detect from available credentials |
MARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_API_KEY |
string | (none) | API key for the summarize endpoint. Presence enables the summarize tool. Falls back to the bare OPENAI_API_KEY |
OPENAI_API_KEY |
string | (none) | Fallback API key (shared with embeddings). Presence enables the summarize tool. Not MARKDOWN_VAULT_MCP_-prefixed |
MARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_BASE_URL |
string | https://api.openai.com/v1 |
OpenAI-compatible endpoint base URL. Setting it enables the tool even without a key (local endpoints) |
MARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_MODEL |
string | gpt-5-mini |
Chat model id used for summaries |
MARKDOWN_VAULT_MCP_SUMMARIZE_MAX_TOKENS |
int | 8192 |
Upper bound on generated tokens per call. On reasoning models (such as the default gpt-5-mini) this budget also covers internal reasoning tokens, so it must be comfortably larger than the summary itself; the server requests low reasoning effort where supported. Raise this if summarize fails with an exhausted-budget error |
MARKDOWN_VAULT_MCP_SUMMARIZE_MAX_NOTES |
int | 50 |
Cap on notes summarised per tool call; this is the coverage and cost lever. It is also the ceiling for the tool's per-call max_notes parameter. Notes beyond it are omitted and counted in the response's notes_omitted |
MARKDOWN_VAULT_MCP_SUMMARIZE_MAX_INPUT_CHARS |
int | 200000 |
Character budget of a single model request. A larger input is split into batches, each batch summarized, and the partial summaries combined (map-reduce), so this bounds request size, not coverage. Smaller values mean more model calls |
MARKDOWN_VAULT_MCP_SUMMARIZE_TIMEOUT |
float (s) | 120 |
Per-request wall-clock budget for a single backend call. On timeout the tool fails with a clear message that says how to fit under the limit (narrow the request, or raise this) instead of running to the ~600 s default of the OpenAI SDK while the client abandons the request. Set it below your MCP client's request timeout |
The former MARKDOWN_VAULT_MCP_SUMMARIZE_INLINE_TIMEOUT variable is gone: the
soft deadline before a slow summarize call is promoted to a background job
is no longer summarize-specific. It now belongs to the
background jobs subsystem as
MARKDOWN_VAULT_MCP_JOBS_SOFT_DEADLINE_S.
The bare OPENAI_BASE_URL is honored as a value fallback when a key is set (such as an organization-wide proxy), but it never enables the tool by itself: setting it purely for embeddings does not switch summarization on.
Provider recipes
- OpenAI: set
OPENAI_API_KEY(or the prefixedSUMMARIZE_OPENAI_API_KEY). Done; the default model isgpt-5-mini. - Ollama (local, no key):
MARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_BASE_URL=http://localhost:11434/v1andMARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_MODEL=llama3.2(any installed model). No API key needed. - Anthropic (Claude via the OpenAI compatibility endpoint):
MARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_BASE_URL=https://api.anthropic.com/v1, the Anthropic key inMARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_API_KEY, andMARKDOWN_VAULT_MCP_SUMMARIZE_OPENAI_MODEL=claude-haiku-4-5.
Note content leaves your environment
The summarize tool sends the referenced notes to the external model provider. Do not enable it for vaults whose content must not leave your environment.
Explicit vs. auto-detect failure handling
Same posture as embeddings: if you set MARKDOWN_VAULT_MCP_SUMMARIZE_PROVIDER explicitly and the backend cannot be loaded (missing SDK, empty key, unrecognised value), startup fails fast with a ConfigurationError. When the provider is unset (auto-detect) and the backend cannot be loaded, the server logs a warning and the summarize tool stays hidden.
Background Jobs¶
Long-running tools (summarize, reindex, build_embeddings) are dual-mode: an MCP client that
speaks background tasks runs them as protocol-native tasks (see
Background tasks for the backend), while any other
client runs them in the foreground up to a soft deadline. A call that beats
the deadline returns its result inline; a slower call keeps running in the
background and immediately returns {"status": "working", "job_id": …}.
Fetch the outcome with the get_job_result
tool. These knobs are owned by the shared jobs subsystem, not by any single
tool:
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_JOBS_SOFT_DEADLINE_S |
float (s) | 25 |
Foreground window before a still-running call is promoted to a background job. Keep it below the strictest MCP-client request timeout in play |
MARKDOWN_VAULT_MCP_JOBS_RESULT_TTL_S |
float (s) | 3600 |
Job-record retention, measured from creation. After expiry the job_id is unknown, so fetch results promptly |
MARKDOWN_VAULT_MCP_JOBS_MAX_PER_SUBJECT |
int | 256 |
Cap on live job records per calling subject; promotion past it fails the call |
Job records live in the unified key-value backend
(MARKDOWN_VAULT_MCP_KV_STORE_URL, namespace jobs). When that variable is
unset, job records use an in-process memory store rather than the file-tree
default of other subsystems: records are ephemeral by design, and the file
default's /data/state path only exists in the Docker image. A promoted job
runs on the serving process and does not survive a restart either way;
durable cross-restart execution is what the native task path with a
redis:// tasks backend is for.
Git Integration¶
Git integration has three modes:
- Managed (
GIT_REPO_URL+GIT_TOKEN): server manages clone, pull, commit, and push. - Unmanaged / commit-only (no
GIT_REPO_URL, repo already exists): server commits writes locally only; you manage pull/push externally. - No-git (default): plain directory; no git operations.
Backward compatibility: GIT_TOKEN without GIT_REPO_URL still works (legacy behavior) and logs a deprecation warning.
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_GIT_REPO_URL |
string | (none) | HTTPS repo URL for managed mode. On startup, empty SOURCE_DIR is cloned from this URL |
MARKDOWN_VAULT_MCP_GIT_USERNAME |
string | x-access-token |
Username for HTTPS auth prompts (x-access-token GitHub, oauth2 GitLab, account name Bitbucket) |
MARKDOWN_VAULT_MCP_GIT_TOKEN |
string | (none) | Token/password for HTTPS auth via GIT_ASKPASS |
MARKDOWN_VAULT_MCP_GIT_PULL_INTERVAL_S |
int | 600 |
Seconds between git fetch + ff-only update attempts; 0 disables periodic pull |
MARKDOWN_VAULT_MCP_GIT_PUSH_DELAY_S |
float | 30 |
Seconds of write-idle time before pushing; 0 = push only on shutdown |
MARKDOWN_VAULT_MCP_GIT_COMMIT_NAME |
string | markdown-vault-mcp |
Git committer name for auto-commits; set this in Docker where git config user.name is empty |
MARKDOWN_VAULT_MCP_GIT_COMMIT_EMAIL |
string | noreply@markdown-vault-mcp |
Git committer email for auto-commits |
MARKDOWN_VAULT_MCP_GIT_COMMIT_NAME_CLAIM |
string | (none) | OIDC claim key to use as the commit author name when a token is present (such as name); falls back to GIT_COMMIT_NAME when absent |
MARKDOWN_VAULT_MCP_GIT_COMMIT_EMAIL_CLAIM |
string | (none) | OIDC claim key to use as the commit author e-mail when a token is present (such as email); falls back to GIT_COMMIT_EMAIL when absent |
MARKDOWN_VAULT_MCP_GIT_LFS |
bool | true |
Run git lfs pull on startup to resolve LFS pointers; set to false if git-lfs is not installed |
Push delay
The push delay batches rapid writes into a single push. Set to 0 to disable automatic pushing; the server will then push only on shutdown via close().
HTTPS remotes only with token auth
When GIT_TOKEN is used, SSH remotes are rejected. Use an HTTPS URL for origin or GIT_REPO_URL.
GitHub Webhook (push-triggered pull)¶
In multi-author deployments, the periodic pull loop introduces up to GIT_PULL_INTERVAL_S seconds of staleness. Setting a webhook secret enables a POST /github-webhook endpoint that triggers an immediate force_pull + reindex when GitHub delivers a push event, reducing the staleness window to webhook delivery latency (~2 s).
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_GITHUB_WEBHOOK_SECRET |
string | (none) | Shared secret for GitHub HMAC-SHA256 signature verification; when unset the endpoint is not mounted |
The endpoint is only available on HTTP/SSE transports. To set up:
- Set
MARKDOWN_VAULT_MCP_GITHUB_WEBHOOK_SECRETto a random secret (runopenssl rand -hex 32to generate one). - In your GitHub repository, add a webhook pointing at
https://<your-host>/github-webhookwith content typeapplication/jsonand the same secret. - Select the
pushevent (other events are acknowledged with 200 and ignored).
The periodic pull loop (GIT_PULL_INTERVAL_S) remains active as a belt-and-suspenders fallback for missed webhook deliveries. For the same setup in context, alongside the other sync mechanisms and what each delivery outcome means, see the git integration guide.
File Watcher¶
Detects external file changes (edits by a local editor, sync daemon, or cp -r) without requiring git integration. Enabled by default for vaults that are not managed by git pull; automatically disabled when the periodic git pull loop (GIT_PULL_INTERVAL_S > 0) or the GitHub webhook (GITHUB_WEBHOOK_SECRET) is active, since those mechanisms already trigger reindex on their own cadence.
Requires the optional watchdog dependency: pip install 'markdown-vault-mcp[file-watcher]'.
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_FILE_WATCHER |
bool | true |
Enable filesystem-event watcher; auto-disabled when git pull or webhook is active |
MARKDOWN_VAULT_MCP_FILE_WATCHER_DEBOUNCE_S |
float | 2.0 |
Seconds of quiet after the last event before triggering reindex; tune down for faster response on small vaults |
MARKDOWN_VAULT_MCP_FILE_WATCHER_ROOT_FLOOR |
bool | true |
Keep the non-recursive watch on the vault root so root-level *.md changes trigger a reindex; set false to drop it (root-level files then rely on scans) |
The watcher schedules one recursive watch per non-excluded immediate child directory of the source directory rather than a single recursive watch on the root, so excluded directories (node_modules, .venv, and similar) are never registered and content under a deliberately watched dot-directory (a vault rooted at $HOME with content under .notes/) delivers its own edits. New top-level directories created after start are not watched until the server restarts; the startup log line lists the scheduled root count and names.
macOS access prompts
On macOS the root floor watch still opens an OS-level recursive FSEvents stream over the whole tree even though it delivers only direct entries, so a vault rooted at $HOME can trigger repeated "would like to access data from other apps" prompts. Set MARKDOWN_VAULT_MCP_FILE_WATCHER_ROOT_FLOOR=false to register zero source-directory-rooted FSEvents streams, accepting that root-level files are then only picked up by scans.
Mutual exclusion with git
When GIT_PULL_INTERVAL_S > 0 or GITHUB_WEBHOOK_SECRET is set, the file watcher is automatically disabled even if FILE_WATCHER=true. This prevents mid-checkout partial scans where git is modifying the working tree.
Attachments¶
Non-markdown file support for PDFs, images, spreadsheets, and more.
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_ATTACHMENT_EXTENSIONS |
csv | (built-in list) | Comma-separated allowed extensions without dot (such as pdf,png,jpg); use * to allow all non-.md files |
MARKDOWN_VAULT_MCP_MAX_ATTACHMENT_SIZE_MB |
float | 1.0 |
Maximum attachment size in MB returned by read() / accepted by write(); 0 disables the limit |
MARKDOWN_VAULT_MCP_MAX_NOTE_READ_BYTES |
int | 262144 |
Maximum bytes returned by full-document read() for .md files; raises ValueError if exceeded. Use read(path, section=...) for partial reads. 0 disables the limit. |
Default allowed extensions: pdf, docx, xlsx, pptx, odt, ods, odp, png, jpg, jpeg, gif, webp, svg, bmp, tiff, zip, tar, gz, mp3, mp4, wav, ogg, txt, csv, tsv, json, yaml, toml, xml, html, css, js, ts
Hidden directories
Attachments inside hidden directories (.git/, .obsidian/, .markdown_vault_mcp/, etc.) are never listed, regardless of extension settings. MARKDOWN_VAULT_MCP_EXCLUDE patterns are also applied to attachments.
Upgrading
MAX_ATTACHMENT_SIZE_MB default lowered from 10 MB to 1 MB. Most LLM contexts can't survive a 10 MB base64-encoded attachment; the old default was a silent context-blow-up. If non-LLM consumers (scripts, CI) need the old behaviour, set MARKDOWN_VAULT_MCP_MAX_ATTACHMENT_SIZE_MB=10 explicitly.
MAX_NOTE_READ_BYTES is a new env var (default 256 KB). Whole-document .md reads above this raise ValueError. Partial reads via read(path, section=heading) bypass the cap.
Bearer Token Authentication¶
Simple static token auth for HTTP deployments. Set a single env var; clients must send Authorization: Bearer <token>.
| Variable | Type | Required | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_BEARER_TOKEN |
string | Yes | Static bearer token; any non-empty string enables auth |
Multi-auth
If both BEARER_TOKEN and all OIDC variables are set, the server accepts either credential. Useful when different clients use different auth flows (Claude web via OIDC, Claude Code via bearer token).
One-Time Transfer Links¶
Short-lived capability URLs for transferring vault files out-of-band (browser download or third-party service upload) without inflating the LLM context window. The GET /transfer/{token} route is mounted outside the auth middleware; the unguessable token is the authorization.
The capability-link machinery (the token store, the /transfer/{token} route, and the two tools) is provided by fastmcp-pvl-core. The token store is backed by the configured state backend (MARKDOWN_VAULT_MCP_KV_STORE_URL, on-disk by default), so live links survive a server restart.
MARKDOWN_VAULT_MCP_BASE_URL is required for the transfer tools; it is used to construct the capability URL returned to the LLM. When BASE_URL is not set, the transfer tools and the /transfer/{token} route are not registered.
| Variable | Type | Default | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_TRANSFER_TTL_DEFAULT_S |
float | 3600 |
Default token lifetime in seconds when the caller omits ttl_seconds. Clamped to MARKDOWN_VAULT_MCP_TRANSFER_TTL_MAX_S |
MARKDOWN_VAULT_MCP_TRANSFER_TTL_MAX_S |
float | 86400 |
Maximum permitted TTL. A requested ttl_seconds above this value is clamped to the ceiling |
MARKDOWN_VAULT_MCP_TRANSFER_GRACE_TTL_S |
float | 60 |
Grace window in seconds after a successful transfer. The token's remaining lifetime shrinks to this, so a stalled or retried transfer can reclaim the link rather than be stranded by a spent one |
MARKDOWN_VAULT_MCP_TRANSFER_LEASE_S |
float | 60 |
Reclaim window in seconds for an in-flight reservation. A crashed handler's token becomes claimable again once this lease lapses |
MARKDOWN_VAULT_MCP_TRANSFER_MAX_UPLOAD_BYTES |
int | 104857600 (100 MiB) |
Per-upload size cap for the upload route. Requests whose body exceeds this limit are rejected with HTTP 413 |
HTTP/SSE transport only
Transfer tools and the /transfer/{token} route are registered only when the server is running HTTP or SSE transport. They are not available on stdio transport.
OIDC Authentication¶
Optional token-based authentication for HTTP deployments. OIDC activates when all four required variables are set. See OIDC deployment for setup details.
| Variable | Type | Required | Description |
|---|---|---|---|
MARKDOWN_VAULT_MCP_BASE_URL |
url | Yes | Public base URL (see Server Identity above); required for OIDC |
MARKDOWN_VAULT_MCP_OIDC_CONFIG_URL |
url | Yes | OIDC discovery endpoint (such as https://auth.example.com/.well-known/openid-configuration) |
MARKDOWN_VAULT_MCP_OIDC_CLIENT_ID |
string | Yes | OIDC client ID registered with your provider |
MARKDOWN_VAULT_MCP_OIDC_CLIENT_SECRET |
string | Yes | OIDC client secret |
MARKDOWN_VAULT_MCP_OIDC_JWT_SIGNING_KEY |
string | No | JWT signing key; required on Linux/Docker (the default is ephemeral and invalidates tokens on restart). Generate with openssl rand -hex 32 |
MARKDOWN_VAULT_MCP_OIDC_AUDIENCE |
string | No | Expected JWT audience claim; leave unset if your provider does not set one |
MARKDOWN_VAULT_MCP_OIDC_REQUIRED_SCOPES |
csv | openid |
Comma-separated required scopes |
MARKDOWN_VAULT_MCP_OIDC_ADVERTISED_SCOPES |
csv | openid offline_access |
Scopes advertised to MCP clients in protected-resource metadata; oidc_required_scopes is always added on top |
MARKDOWN_VAULT_MCP_OIDC_VERIFY_ACCESS_TOKEN |
bool | false |
Set true to verify the upstream access token as JWT instead of the id token. Only needed when your provider issues JWT access tokens and you require audience-claim validation on that token |
Boolean Parsing¶
Boolean environment variables accept true, 1, or yes (case-insensitive) as truthy. Everything else is treated as false.
Example .env Files¶
| File | Description |
|---|---|
examples/obsidian-readonly.env |
Obsidian vault, read-only, Ollama embeddings |
examples/obsidian-readwrite.env |
Obsidian vault, read-write with git auto-commit |
examples/obsidian-oidc.env |
Obsidian vault, read-only, OIDC authentication (Authelia) |
examples/ifcraftcorpus.env |
Strict frontmatter enforcement, read-only corpus |