VFS Mount Guide — zero to a mounted ~/wiki
Status: canonical newcomer walkthrough · 2026-10-13 · targets develop
Scope: the full path from a bare macOS machine to a live, read-write mount of a
Docmost wiki at ~/wiki, using only this document. Deeper references are linked at the
bottom (§7); nothing earlier is required reading.
1. What this is
mcp-mount projects a remote MCP-VFS tree — here: a Docmost wiki served by the
docmost-mcp MCP server — as real local files in a directory you choose, and keeps
both sides in sync over a WebSocket: server changes land in your files live, and your
local edits (250 ms debounce) push back to the server. cat, grep, tail -f, editors,
pipes, and cron all work on wiki content as ordinary files.
It is not a kernel FUSE mount. The files are ordinary files in an ordinary directory; there is no kernel extension, no macOS approval prompt, and no FUSE runtime to install. (If you came here expecting to install macFUSE — you don’t need it for this flow; see §2.)
| Piece | What it does | Where it comes from |
|---|---|---|
noizu_mcp (hex) |
VFS substrate: backend behaviour + DSL, generation-stamped cache, WebSocket transport, /etc/dev control tree |
hex.pm/packages/noizu_mcp — a server-side dependency, nothing you install |
docmost-mcp server |
Serves the wiki as a VFS tree at ws://127.0.0.1:4700/vfs |
Built from the docmost-mcp repo (sibling checkout); full server recipe: the VFS-RUNBOOK §2–3 |
mcp-mount client |
Connects to the endpoint, snapshots the tree to ~/wiki, syncs both ways |
Built from this repo (daemon/mcp_mount) — it is not on hex; there is no mix install or download shortcut |
2. Prerequisites (macOS)
| # | Requirement | Get it | Why |
|---|---|---|---|
| a | FUSE runtime | None. Do not install one. |
mcp-mount is the no-driver path: it materializes real files and needs no FUSE/WinFsp (docs/MCP-VFS-MOUNTING.md §6). macFUSE / FUSE-T belong to the separate mcp-fuse kernel companion (a different binary, over a local unix socket) — not to this flow. Consequences: no kernel-extension approval in System Settings, no reboot, nothing to verify here. |
| b | Xcode Command Line Tools |
xcode-select --install (then cc --version to confirm) |
mix deps.get compiles the file_system watcher’s native mac_listener binary — a C compiler is required. (Documented nowhere else before this guide.) |
| c | Elixir + Erlang/OTP | mise or asdf — commands below |
The repo root .tool-versions pins elixir 1.20.1-otp-29 / erlang 29.0.2. The client floor is Elixir ≥ 1.18 (daemon/mcp_mount/README.md), so the pinned versions or anything newer work. |
| d | git + repo access |
git clone [email protected]:noizu-labs-ml/elixir-mcp-lib.git |
noizu-labs-ml is org-scoped: if the clone fails with permission denied (publickey), you need an org invite + your SSH key registered on GitHub. |
| e |
A running docmost-mcp server + tokens |
Operator side — see below | The mount has nothing to connect to without it. |
Erlang/Elixir via mise (or asdf — both read .tool-versions; run inside the repo checkout):
mise install # full pinned toolchain (.tool-versions)
mise install [email protected] [email protected] # …or just the two you need
eval "$(mise activate zsh)" # or per-command: mise exec -- <cmd>
asdf plugin add erlang && asdf plugin add elixir
asdf install # reads .tool-versions
Server + tokens (operator convention; full recipe in VFS-RUNBOOK §3):
umask 077
mkdir -p ~/.config/docmost-mcp
openssl rand -hex 32 > ~/.config/docmost-mcp/mount-token # authorizes the WS mount only
# ~/.config/docmost-mcp/api-key is your Docmost instance API key (from the Docmost UI),
# NOT locally generated — store it there by convention and never print it.
The server then starts with DOCMOST_MCP_MOUNT_TOKEN="$(cat ~/.config/docmost-mcp/mount-token)"
and DOCMOST_MCP_MOUNT_PORT=4700 (the listener binds 127.0.0.1 only and exists only while
a token is configured). Verify it is up: lsof -iTCP:4700 -sTCP:LISTEN. Writes need
DOCMOST_MCP_WRITES=1 and directory deletes need DOCMOST_MCP_VFS_RMDIR=1 at boot.
3. Get + build the client
git clone [email protected]:noizu-labs-ml/elixir-mcp-lib.git
cd elixir-mcp-lib
git checkout develop
cd daemon/mcp_mount
MIX_BUILD_ROOT=/tmp/mcp-mount-build mix deps.get
MIX_BUILD_ROOT=/tmp/mcp-mount-build mix escript.build
MIX_BUILD_ROOT keeps _build out of the checkout (shared checkouts stay
collision-free); deps/ still lands in daemon/mcp_mount/deps/ — which the run recipe
in §4 needs anyway, because that is where mac_listener gets built.
Success looks like:
Generated escript mcp-mount
ls -la mcp-mount # the escript exists, is executable
file deps/file_system/priv/mac_listener # a Mach-O binary — note its arch
The file_system dep compiles mac_listener during mix deps.get for the
interpreter’s arch (see the Rosetta row in §6). First build takes a few minutes.
4. Mount it
export FILESYSTEM_FSMAC_EXECUTABLE_FILE="$PWD/deps/file_system/priv/mac_listener"
./mcp-mount \
--url ws://127.0.0.1:4700/vfs \
--token "$(cat ~/.config/docmost-mcp/mount-token)" \
--mount ~/wiki > /tmp/mcp-mount.log 2>&1 &
-
FILESYSTEM_FSMAC_EXECUTABLE_FILEis required for write-back on macOS: an escript cannot embedmac_listener, and without this variable the mount silently degrades to pull-only (server→local sync works; local edits are never pushed). -
--deletesis optional — opt in to pushing local deletions (§5.3). Without it, a localrmis a local-only no-op. -
--roinstead gives a read-only mount (no watcher, never pushes;--deletes+--rologs a warning and has no effect). Flags are strict:--url,--token,--mount,--ro,--deletes— anything else prints usage and exits 64.
First-mount expectations: no macOS permission dialogs — no kernel extension is
involved, and ~/wiki is not a TCC-protected location. If you see an approval prompt, it
belongs to something else you installed (macFUSE for another tool, perhaps).
A healthy boot log (/tmp/mcp-mount.log):
mcp-mount: ws://127.0.0.1:4700/vfs -> /Users/you/wiki
mcp-mount[mcp-mount]: connected (was authing)
mcp-mount[mcp-mount]: live (N files, ro=false)
If you instead see watcher unavailable (mount stays pull-only), fix the env var and
restart (§6).
Verify:
ls ~/wiki # one directory per Docmost space + README.md (server-generated)
grep " live " /tmp/mcp-mount.log # live (N files, ro=false) after the snapshot
Caps. The vfs/auth handshake result carries a caps list — a current docmost-mcp
server advertises ["move", "rmdir"]. Caps gate optional operations: move lets a local
mv pair into one vfs/move RPC (§5.2); rmdir gates --deletes directory removals
(§5.3). The CLI does not print caps — observe them via behavior, or run a direct VFS
client against the endpoint. An older server with no caps degrades safely: moves/deletes
report unsupported and park instead of misbehaving.
Process tree while running (ps -ef | grep -E 'mcp-mount|mac_listener'):
mcp-mount (escript → BEAM VM)
├── mac_listener # file_system port program — the filesystem watcher (write-back)
└── erl_child_setup # BEAM helper for port subprocesses
Unmount: Ctrl-C / kill the foreground process (or pkill -f mcp-mount). The files stay
on disk; the next mount diff-resyncs against .mcp-mount/manifest.json.
5. Everyday use
5.1 Writes toggle
The server’s writes gate (DOCMOST_MCP_WRITES) can be flipped without a restart:
~/wiki/etc/dev/config/writes is the one writable /etc node — edit it to contain
true or false and the mount pushes it back as a vfs/write; the gate re-evaluates
immediately. Every other /etc/** path is read-only (local edits are a no-op; the
watcher never queues them).
5.2 Move
mv ~/wiki/engineering/foo.md ~/wiki/engineering/bar.md just works: when the server
advertises the move cap, the rename+edit is paired into a single vfs/move RPC and the
server retitles/re-parents the page, fanning out events so the local tree re-paths
without a full resync.
5.3 Deletes (--deletes mounts only)
All deletes are soft deletes — recoverable in Docmost’s trash.
-
Files:
rmpushesvfs/removeafter a short double-confirm window (an editor’s atomic save — remove+recreate inside the window — aborts the delete). -
Page directories use a marker handshake. Author the marker inside the directory, then remove the directory:
cat > ~/wiki/engineering/notes/.confirm-delete <<'EOF' {"confirm_delete": "/engineering/notes", "ts": "2026-10-13T12:00:00Z"} EOF rm ~/wiki/engineering/notesThe marker must be fresh (±5 minutes), must name the directory it sits in, and binds to the page the directory resolves to now — a rename or a new page claiming the path invalidates it. It is consumed on success, retained on failure (retry without re-authoring), and lives in server memory only — a server restart between authoring and
rmdrops it (eperm; re-author). -
Refusals park instead of retrying:
eperm(missing/stale marker),eacces(serverDOCMOST_MCP_VFS_RMDIRoff or writes gate off),enotempty(remove child.mdfiles first — deletes decompose bottom-up, no cascade). Spaces (root-level dirs) are never deletable via the mount.
5.4 Conflicts and parked paths
If the server moved ahead of your local edit, your version is saved aside as
<path>.conflict-<ISO-timestamp> and the server copy is re-pulled — reconcile by hand;
conflict files are never pushed. A path that keeps failing (or staying unconfirmed) is
attempted 4 times, then parked with a warning; it re-arms only when you edit the
file again. .mcp-mount/ is local bookkeeping — never edit, never pushed.
5.5 Updating the client
The client is not on hex — updates are always git + rebuild + restart:
cd /path/to/elixir-mcp-lib && git pull origin develop
cd daemon/mcp_mount
MIX_BUILD_ROOT=/tmp/mcp-mount-build mix deps.get && MIX_BUILD_ROOT=/tmp/mcp-mount-build mix escript.build
pkill -f mcp-mount # then re-run the §4 recipe (same manifest → fast diff resync)
A server restart bumps every version (boot-epoch stamping), so the first sync after one re-pulls the full tree once — expected, not a bug.
6. Troubleshooting
| Symptom | Cause / fix |
|---|---|
| You expected a macOS approval prompt / installed macFUSE for this |
None is needed — mcp-mount is not kernel FUSE. Uninstalling macFUSE is optional; it is unrelated to this flow. (Only the mcp-fuse kernel companion uses it.) |
| Mount lists files but local edits never propagate |
Pull-only degrade: watcher unavailable (mount stays pull-only) in the log. Set FILESYSTEM_FSMAC_EXECUTABLE_FILE (§4) and restart mcp-mount. |
deps/file_system/priv/mac_listener missing after mix deps.get |
The native watcher did not compile — usually no Xcode CLT (§2b). xcode-select --install, then re-run mix deps.get. |
| Arch mismatch (Rosetta) |
mac_listener is built for the building interpreter’s arch; file it and compare with file $(which erl)‘s arch. If mcp-mount runs under Rosetta, rebuild the deps under an x86_64 toolchain so the watcher binary matches — a mismatching mac_listener fails to spawn and the mount degrades to pull-only. |
Files/checkout vanish mid-session under ~/Work |
~/Work is an automount that can drop. Keep the repo clone and the mount point off it (~/wiki is safe); recover the mount with sudo /usr/local/bin/fstab-remount and restart mcp-mount. |
Dir delete fails eperm though --deletes is on |
Stale marker: .confirm-delete is ±5 minutes, must name the current dir, and dies with a server restart. Re-author it at the current path (§5.3). |
eacces on deletes / writes |
Server opt-ins: DOCMOST_MCP_VFS_RMDIR=1 (dir deletes) and DOCMOST_MCP_WRITES=1 (all mutations) — or flip the gate at runtime via etc/dev/config/writes (§5.1). |
| Port 4700 busy |
lsof -iTCP:4700 -sTCP:LISTEN — a stale or second server holds it. Kill it or start the server on another DOCMOST_MCP_MOUNT_PORT and point --url at the same port. |
| HTTP 401 before any socket traffic |
Wrong/missing mount token — the WS upgrade bearer check failed. Compare ~/.config/docmost-mcp/mount-token with the server’s DOCMOST_MCP_MOUNT_TOKEN. |
Error -32001 right after connect |
The first frame must be vfs/auth with the same token; a mismatch closes the connection. Same check as above. |
Optional demo server (demo/vfs_demo_server) won’t compile — yamerl errors |
yamerl is a demo-server dependency only; mcp-mount does not use it. After an OTP switch its cached build can go stale — wipe and recompile fresh: rm -rf demo/vfs_demo_server/deps demo/vfs_demo_server/_build, then mix deps.get && mix compile in that dir. |
mcp-mount exits immediately, usage + exit 64 |
Strict flags only: --url --token --mount --ro --deletes — no --transport, no --write, no positional target. |
| Docmost web edits don’t show up in the mount |
Live mounts see VFS events only; web-side changes surface at the next reconnect/resync. Restart mcp-mount to force a diff resync (and reconcile before writing — mount writes replace page content wholesale). |
| Full re-pull after a server restart | Expected: versions are boot-epoch-stamped; one full re-pull per server restart. |
7. Pointers
| Doc | Covers |
|---|---|
MCP-VFS-MOUNTING.md |
Operator reference for mcp-mount: full option surface, mount commands, backend choices (DB-backed, directory-backed), the kernel mcp-fuse companion (Linux FUSE3, macFUSE/FUSE-T, WinFsp), and the resources/prompts content bridge |
MCP-VFS-GROUP-MOUNTS.md |
Design of record: how each MCP group exposes its content through the mounting stack |
daemon/mcp_mount/README.md |
Client internals: write-back safety rules, deletes state machine, Conn behaviour/transports |
Projects/docmost-mcp/docs/VFS-RUNBOOK.md (monorepo sibling checkout) |
Server-side runbook: env vars, token minting, starting the mount listener, .meta sharing/permissions, page CRUD, full troubleshooting matrix |
| hex.pm/packages/noizu_mcp · hexdocs.pm/noizu_mcp |
The server-side VFS substrate (noizu_mcp hex package) |