noid's MCP server #
noid ships a local stdio MCP server, mcp.js, so an agent can work with a
vault's graph - connections, their types and notes, who wrote what - without
noid's window being involved. It works whether or not the noid app is running.
Every file in a vault is a card, of any type, except dot paths, node_modules and files
ignored by the vault's git ignore rules (its .gitignore files at any depth, the
repo's .git/info/exclude, and the .gitignore files above the vault inside its repo).
Agents keep changing files with their own tools; the MCP tools cover what
the file system can't express: the graph, authorship, activity, and move_file
(a rename that keeps a file's connections).
Any existing folder may be a vault. Every tool call takes a vault argument -
the absolute path of the folder to work in - so one server process can serve
any number of vaults; there's no list_vaults call, so the agent must be told
(or figure out) the path itself.
Build it #
npm run build # compiles main.js, out/renderer and mcp.js
Register it with an agent #
One entry per agent, not per vault - the agent names its vault on every call. Claude Code, at user scope, so it's available in every session regardless of project (it only loads when a session starts, so restart after adding it).
The usual way is the installed launcher. npm run install-local release-builds
noid, installs it under ~/.local/share/noid with noid and noid-mcp in
~/.local/bin, and registers noid-mcp at user scope as --agent claude:
claude mcp add noid --scope user -- ~/.local/bin/noid-mcp --agent claude
The installed copy is separate from the repo, so rebuilding in development never
touches it; re-run the installer to update it. A project picks its own agent name
with a .mcp.json in its root; a project entry wins over the user one:
{"mcpServers": {"noid": {"command": "noid-mcp", "args": ["--agent", "noid-dev"]}}}
To run the server from the repo instead (no install):
claude mcp add noid --scope user -e ELECTRON_RUN_AS_NODE=1 -- <electron> <repo>/mcp.js --agent <name>
<electron> is the real Electron binary acting as node, since there usually
isn't a node on the agent's PATH. node_modules/.bin/electron won't work for
this - it's a node script itself (#!/usr/bin/env node), so it fails in
exactly the case this is for. Use the real binary instead:
- Linux:
<repo>/node_modules/electron/dist/electron - macOS:
<repo>/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron - Windows:
<repo>\node_modules\electron\dist\electron.exe
The name comes before -e, since -e takes several values and would
otherwise swallow it. If node is on the agent's PATH, this also works
without ELECTRON_RUN_AS_NODE:
claude mcp add noid --scope user -- node <repo>/mcp.js --agent <name>
Options:
--vault <abs>(optional): the default vault for a call that omits its ownvault. Handy for a per-vault registration; without it, every call must name its own vault.--agent <name>(defaultdefault): the agent's identity, matching[a-z0-9-]{1,40}.-h,--help: print the usage and exit without starting the server.
Names are identities #
The --agent name is the agent's identity in the graph: --agent claude-research
writes as agent:claude-research, shown on the board as its own ◆ and stored in
graph.edn as :agent/claude-research. Give each agent its own name - two agents
sharing a name share their notes and permissions. The name comes only from the
launch argument; no tool argument can change it.
Rules the tools enforce ("add, not override") #
- An agent may set a connection's type only when it has none or the agent set it;
otherwise
forbidden(usewrite_noteto add your own view). - Every author has their own note on a connection;
write_noteonly ever changes the caller's. disconnectonly removes connections the caller created (a connection with no recorded creator counts as the human's).claim_fileonly claims files nobody created.move_fileis allowed on any file, because refusing would push agents tomv, which strands connections on a ghost card.- The human may override anything from the UI.
The vault lock #
Only the holder of <vault>/.noid/lock writes graph.edn. noid holds it while a
vault is open. The MCP server takes it for one tool call at a time (a 30 s lease),
checks it still holds it right before writing, and releases it afterwards. A lock
whose process is dead, or whose lease has run out, is stale and taken over
(moved aside to lock.stale-<ms>).
Two modes, chosen per call by who holds the lock:
- Direct (noid closed, or open on another folder): the server takes the lock,
reads
graph.ednand scans the folder, plans the call, writesgraph.edn, and releases. The next time noid opens the vault, it shows the result. - Relay (noid has the vault open): the server sends the call over noid's local
socket - a unix socket under Electron's userData dir, falling back to the OS temp
dir when that path would be too long for a unix socket (
<dir>/noid-<pid>.sock, owner-only; recorded in the app's lock, but only once the app is actually listening on it - never a stale or dead path). The app applies it as an action, so the change appears on the board as it happens, and replies. Before planning a call, noid looks at the files it names on disk, so a note an agent has only just written is already known. If the app's socket doesn't answer, the lock counts as stale and the server falls back to direct mode. If noid doesn't answer within 10 s, the agent is toldunavailable, but the change may still be applied afterwards.
move_file on a card the human has unsaved text in answers busy.
A read-only call (vault_info, describe_file, find_connections, list_types,
recent_activity) against a folder with no .noid/ yet never creates one, or
the lock, or a lease - it just scans the folder and answers. .noid/ (and
graph.edn) only appears once a write actually happens there.
If the vault is itself a git repository, .noid/lock shows as untracked; add it
to that repository's ignore file.
Limitations #
- Durability differs by mode. In relay mode the
okcomes back beforegraph.ednis written (the app saves 300 ms after a change), so a crash inside that window loses a change the agent was told succeeded. Direct mode is durable when it replies. - If noid doesn't answer a relayed call within 10 s, the agent is told
unavailable, but the change may still be applied afterwards, once noid gets around to it. recent_activityreflects current state only: no deletions, only the latest type of a connection, no edits to a card's text.- Agents can still overwrite or
rmfiles with their own tools; a deleted card shows as a ghost. - Any folder can be a vault; every call rescans it (skipping dot-folders, node_modules and git-ignored folders), so a very large folder such as your home directory makes calls slow.
- Taking over a stale lock can, in a narrow race, move aside a lock another process just created; token checks before every graph.edn write (server and app) turn that into a failed or retried call, not a lost write. What remains is the gap between the token check and the write.