mastervault-mcp-server
v1.4.0
Published
A headless, filesystem-backed MCP server that serves any MasterVault — its files and its operating protocol — to any MCP client. No Obsidian required.
Downloads
33
Maintainers
Readme
MasterVault MCP Server
A headless, filesystem-backed Model Context Protocol server that serves any MasterVault — its files and its operating protocol — to any MCP client, over stdio. No Obsidian required.
Point it at a vault directory; any MCP-capable LLM client (ModelForge, Claude Desktop, AnythingLLM, etc.) can then orient on the vault's protocol, read and search its contents, log decisions, and soft-delete files — all behind the client's own approval flow.
Why this exists
An Obsidian vault can already be served over MCP via the Local REST API plugin, but that ties it to Obsidian. This server drops the Obsidian dependency: it speaks the same vault semantics against a plain directory, so the MasterVault becomes portable to any client and any model. It also surfaces the vault's protocol (orientation, decision-logging, soft-delete), not just its files — so a fresh LLM can run the system, not merely read it.
Install
Run it directly, no install step:
npx -y mastervault-mcp-server /absolute/path/to/vaultOr install it globally for a shorter command:
npm install -g mastervault-mcp-server
mastervault-mcp-server /absolute/path/to/vaultRequires Node.js 18+.
From source (for contributing)
git clone https://github.com/JustMichael-80/mastervault-mcp-server.git
cd mastervault-mcp-server
npm install # also builds via the prepare script
npm run build # (if you skipped prepare)Run
mastervault-mcp-server /absolute/path/to/vault
# or
MASTERVAULT_ROOT=/absolute/path/to/vault node dist/index.jsThe vault path is the only configuration. Nothing is hardcoded — the same binary serves any vault.
Multi-vault discovery
Point the server at a parent directory instead of a single vault, and it discovers every MasterVault beneath it (any directory containing an _orientation.md):
mastervault-mcp-server --discover /path/to/projects-rootThe first discovered vault (alphabetically) becomes the active vault; the mastervault_list_vaults tool lists all of them by name. The scan is depth-bounded, skips hidden and dependency directories, and does not follow symlinks out of the tree. Discovery only locates vaults — every file operation stays confined to the active vault's sanitized root, so a vault name can never select an arbitrary path.
Discovery was contributed by VDMO (https://github.com/vdmo).
Bundled single-file build
For vendoring or shipping the server as one self-contained file with no npm install at the consumer end:
npm run build:bundled
# produces dist-bundled/index.js — all dependencies inlined (~890kb)
node dist-bundled/index.js /absolute/path/to/vaultThe bundle is produced by esbuild (Node 18 target, ESM) with a createRequire shim for CommonJS interop. Useful when another application packages this server as a component rather than depending on it as an installed module.
Connecting a client
ModelForge
In Settings → MCP servers, add a stdio server:
- Command:
node - Args:
/absolute/path/to/mastervault-mcp-server/dist/index.js/absolute/path/to/vault
(or use the mastervault-mcp-server bin directly if installed globally). The server's tools then appear in Agent mode's tool list, each behind ModelForge's Allow/Deny approval — exactly like its built-in file tools.
Any MCP client (generic stdio config)
{
"mcpServers": {
"mastervault": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js", "/absolute/path/to/vault"]
}
}
}Tools
| Tool | Tier | Read-only | What it does |
|------|------|:---------:|--------------|
| mastervault_orient | protocol | ✅ | Reads the orientation file and the protocol files it points to, in order. Also returns context_file_status — CLEO_context.md's mtime, its age in days, and whether it's ≥14 days old — so a client can run the protocol's staleness check without a separate stat tool. null if that file doesn't exist. Call this first. |
| mastervault_get_confidence_summary | protocol | ✅ | Returns the calibration dashboard (_Meta/Confidence Summary.md). |
| mastervault_log_decision | protocol | ✍️ | Appends a consequential decision (proposal + confidence + verdict) to the right category in _Meta/Decision Log.md. |
| mastervault_read | files | ✅ | Reads a file, optionally by line range; parses markdown frontmatter. |
| mastervault_list | files | ✅ | Lists a directory, paginated, directories first. |
| mastervault_search | files | ✅ | Case-insensitive full-text search across text files. |
| mastervault_write | files | ✍️ | Creates or overwrites a file. |
| mastervault_patch | files | ✍️ | Replaces one exact, unique text block in a file. |
| mastervault_stage_delete | delete | ✍️ | Moves a file to _ToDelete/ and logs the proposal. Never hard-deletes. |
| mastervault_git_status | git | ✅ | Git status of the vault (if under version control). |
| mastervault_git_log | git | ✅ | Recent commits. |
| mastervault_git_diff | git | ✅ | Working-tree or staged diff, optionally for one file. |
Every tool supports response_format: "markdown" (default) or "json". The three git tools are read-only; on a non-git vault they return a clear "not a repository" message rather than an error. In --discover mode, mastervault_list_vaults is also exposed.
Large responses in json format
Every tool response carries two representations: a text rendering (content[0].text) and structured data (structuredContent). This server enforces a response size limit on the text side (CHARACTER_LIMIT, 25,000 characters) so a single call can't blow out a client's context window. structuredContent is never truncated by this limit, regardless of format — it always carries the complete data. The size limit only ever shapes what shows up in the text body.
For response_format: "markdown", exceeding the limit just shortens the document with a trailing [Response truncated at N characters...] note — safe, since it's free text.
For response_format: "json", content[0].text is a JSON.stringify'd blob, and slicing that string by raw character count can cut it mid-token — producing text that looks like JSON but fails to parse. Every JSON-format tool response avoids this by bounding the object before stringifying, not the string after, so content[0].text is always valid JSON. Depending on the tool, this shows up as one of:
truncated: true+truncated_fields: [...]— one or more string fields (e.g.mastervault_read'scontent,mastervault_git_diff'soutput,mastervault_orient's largest file bodies, named individually astruncated_files) were shortened, with a visible[...elided: <field> truncated at N of M characters for size...]marker left at the cut point. The kept text is never cut mid-character — a cut that would split a multi-byte character (e.g. mid-emoji) backs off by one position first, so the result always round-trips cleanly through UTF-8 encode/decode.json_size_truncated: true+json_size_truncated_count: N(mastervault_list,mastervault_search) — the array field itself (entries,hits) was too large as a whole page, soNtrailing elements were dropped from the text body and the last surviving element was replaced with a sentinel object —{ _elided: true, note, dropped_count }— so the gap is visible directly in the array, not only in a sibling key easy to miss when scanning entries. This is distinct frommastervault_search's owntruncated/total/countfields, which mean "more matches exist beyondlimit" — a pagination concern, not a size-on-the-wire concern. Both can betrueat once and mean different things.- A minimal fallback envelope (
{ truncated: true, truncation_note, keys }) — only for a tool response with no field the server knows how to shrink. As of this fix, every tool reachable with a large realistic payload (read,git_diff,list,search,orient) has a proper elidable field instead; this fallback exists as a safety net for anything unforeseen, not as expected behavior for any current tool.
Security model
- Path confinement is the single security boundary. Every path is resolved and confined to the vault root before any filesystem call. Lexical
..traversal, absolute paths, and null bytes are rejected; a leading slash is treated as vault-relative, not filesystem-absolute. Existing paths get a secondrealpathcheck so a symlink inside the vault can't point out of it. - No hard delete. The server has no tool that destroys data.
stage_deleteonly moves files into_ToDelete/; a human is the sole final actor who empties it. This is whystage_deleteis marked non-destructive — it's reversible by design. - No shell execution. File operations only. If a client needs shell access it provides that itself (ModelForge does, sandboxed and gated separately).
- stdio hygiene. All logging goes to stderr; stdout carries only the MCP protocol.
The MasterVault protocol layer
This server is more than a file server because of three conventions it understands:
_orientation.md— the entry point a fresh LLM reads first (viamastervault_orient) to inherit the vault's working rules._Meta/Decision Log.md+Confidence Summary.md— a decision-logging + calibration system: consequential proposals are logged with a pre-verdict confidence estimate, and the gap between estimate and outcome accumulates into a per-category calibration signal._ToDelete/— the soft-delete staging area; the human is always the final actor on removal.
A vault that lacks these still works as a plain file tree — the protocol tools report what's missing rather than failing.
Development
npm run dev # tsx watch
npm run build # tsc -> dist/
npm run build:bundled # esbuild -> dist-bundled/index.js (single file)
npm test # node --test, 42 tests
npm start # node dist/index.js <vault>Tests
The suite (test/vault.test.mjs, test/tools.test.mjs, test/git.test.mjs, test/wire.test.mjs) covers the filesystem layer, the path-confinement security boundary (traversal, absolute paths, null bytes, symlink-escape), and the tool layer (patch match rejection, section-aware decision logging, soft-delete collision handling). A note on platform coverage: automated CI typically runs on Linux, which cannot reproduce every macOS-specific filesystem behavior (symlink canonicalization, path case-sensitivity, /var→/private/var-style redirects). A green CI run is necessary but not sufficient for those behaviors specifically — verify on macOS directly (not a Linux CI runner or container) before relying on filesystem-boundary tests. A green run on macOS is evidence for macOS behavior; it says nothing about Linux or Windows.
test/vault.test.mjs and test/tools.test.mjs stub the MCP SDK's registerTool and call handlers directly in-process — fast, but structurally blind to anything that only breaks during actual MCP wire serialization. test/wire.test.mjs closes that gap: it spawns the real built server (dist/index.js) and drives it through an actual MCP Client over stdio, with fixtures sized to exceed CHARACTER_LIMIT on ordinary content (a large file read, an uncommitted diff, a directory listing with long filenames, a search with many hits, and emoji content sized to force a cut exactly inside a UTF-16 surrogate pair). It asserts content[0].text always parses as valid JSON, elided text round-trips byte-identically through UTF-8 encode/decode, and structuredContent is always the complete, untruncated data.
License
MIT
