A mental bridge between a hacker and their LLM(s) of choice.
noid docs mcp.md
7.4 kB · 153 lines
Markdown
at main

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 own vault. Handy for a per-vault registration; without it, every call must name its own vault.
  • --agent <name> (default default): 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 (use write_note to add your own view).
  • Every author has their own note on a connection; write_note only ever changes the caller's.
  • disconnect only removes connections the caller created (a connection with no recorded creator counts as the human's).
  • claim_file only claims files nobody created.
  • move_file is allowed on any file, because refusing would push agents to mv, 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.edn and scans the folder, plans the call, writes graph.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 told unavailable, 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 ok comes back before graph.edn is 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_activity reflects current state only: no deletions, only the latest type of a connection, no edits to a card's text.
  • Agents can still overwrite or rm files 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.