Hooks or MCP and why #
Two ways exist for an agent to reach a brain and they answer different questions. Hooks answer "did the agent arrive knowing what this project knows": the host fires them, so the brief and the prompt-matched recall land in context whether or not the model thought to look. MCP answers "can this agent reach the brain at all": it is the only door for a client that cannot run a shell command. Both run the same code and read the same store. Pick by what the client can do, not by preference.
| Hooks and the CLI | MCP | |
|---|---|---|
| Who fires it | The host, on session start, every prompt and stop. Nothing depends on the model remembering. | The model, when it decides a tool call is warranted. Pull only. |
| Needs | A client with hooks and a shell: Claude Code, Codex, Cursor, Gemini. | A client that speaks MCP. That is most of them, including the ones with no shell. |
| Context cost | The brief once, then only what a prompt matches. | Every tool schema on every session, whether or not one is called. |
| Flags | Whatever the agent types. A wrong flag is a refusal with the fix in it. | Typed inputs derived from the same flags. The model cannot invent one. |
| Reaching a brain on another box | Enrol the machine against it. Every command works, the hooks keep firing. | ns-brain mcp --http on the box, a bearer token on the client. |
| Set up with | ns-brain init, which wires the hooks. --agent codex,cursor,gemini for the others. | claude mcp add or the same three things in your client's own file. |
One machine, an agent with a shell: hooks and nothing else. This is the install page and it is most people.
Several machines, agents with a shell: the brain on one box, every other machine enrolled, hooks on all of them. Not MCP: the CLI on each machine is already the remote client and adding MCP beside it is a second transport to the same store for the price of the schemas.
An agent with no shell: MCP and there is no second choice. The Claude app, a hosted agent, an IDE that runs no hooks. Point it at the brain over HTTP and accept that reading is now the model's decision.
Per project, both of them. Hooks live in the project's own .claude/settings.json, written by ns-brain init there and nowhere else, so a project you never initialised has none. MCP registered with -s local or -s project belongs to the directory you ran it in. Mix freely: one project on hooks, another on MCP, a third on both. The one setting that crosses projects is an MCP server added with -s user and that is the server that cannot tell which project you are in, which the next section but one is about.
Both at once: fine and sometimes right. A client that has hooks and also speaks MCP gets arrival from the first and typed inputs from the second. The one thing not to do is drop the hooks because MCP is connected; the section on what MCP gives up is why.
Which transport you want #
stdio if the brain is on the machine the agent runs on. The client launches ns-brain mcp itself: no port, nothing to keep running, nothing to authenticate. This is the common case and it is the one to start with.
HTTP if the brain is somewhere else and the model should reach it without a shell. For a laptop and a desktop sharing one brain, MCP is no longer the way to do it: a brain on a server and a machine enrolled against it — from anywhere, including outside your own network — gives you every command rather than the ones a model chooses to call and the session hooks can use it. MCP stays in the free edition because the commercial line is a second person, never a second machine.
$ ns-brain mcp # stdio, launched by the client $ ns-brain mcp --http 127.0.0.1:7801 # HTTP, endpoint is /mcp
The HTTP endpoint is /mcp, not the root. The root returns 404 and it costs everyone one round trip to find that out.
stdio, wired to Claude Code #
$ cd ~/projects/yours $ claude mcp add brain -s local \ -e BRAIN_HOME=/Users/you/projects/yours/brain \ -e BRAIN_PROJECT=yours \ -- ns-brain mcp $ claude mcp list brain: ns-brain mcp - ✔ Connected
BRAIN_HOMEis the brain directory, the one holdingbrain.json. Without it the server resolves the store from its own working directory, which the client promises nothing about.BRAIN_PROJECTnames the project. It is the setting that matters most and the next section is why.-s localkeeps the entry in your own config rather than a.mcp.jsoncommitted to the repository. Use project scope only if everyone working on that repo has a brain, because for anyone who does not it is a server that fails to start on every session.
Codex, Cursor and Gemini each take the same three things in their own file: the command, the two environment variables and a name. Whatever the syntax, the shape is identical.
Naming the project and why it refuses without one #
MCP carries no notion of the client's working directory. A server fronting a store that holds several projects has no way to work out which one you are in, so it does not guess:
this server fronts a shared brain, so it cannot tell which project you are working in: pass _project. already here: FreightHorizon, advisor-dollie, cmys, fightclub, maestro, ns-brain
That refusal is the good outcome. The bad one is what it replaced: the server used its own label and every project's memories landed under one name, silently. There are three ways to answer it, in the order you should prefer them.
| Where | How | When |
|---|---|---|
| Config, stdio | BRAIN_PROJECT=yours in the server's env | Always, for stdio |
| Config, HTTP | X-Brain-Project: yours header | Always, for HTTP |
| Per call | _project argument on any tool | Reaching across projects on purpose |
Config beats an argument because config cannot forget and a model can. Declare it once and the only reason to pass _project is when you deliberately want one connection to touch another project. A brain that holds one project needs none of this: it knows what it is.
HTTP, for a brain on another box #
# on the box that holds the brain $ BRAIN_HOME=/srv/brains/yours ns-brain mcp \ --http 127.0.0.1:7801 --token "$TOKEN" # on each machine you work from $ claude mcp add brain -s user -t http https://brain.example.com/mcp \ -H "Authorization: Bearer $TOKEN" \ -H "X-Brain-Project: yours"
Bind loopback and put nginx or Caddy in front with TLS. The binary refuses to start on a public interface with no token and when you do bind one it says on stderr that every memory in the brain is now reachable from the network.
A hosted client that cannot be handed a bearer token, the Claude app on a phone above all, needs OAuth rather than this. --public-url mounts it and the phone page is that whole story.
What the server enforces #
All of it out of the transport specification's own security section, none of it optional.
- Origin is validated on every request, so a web page cannot drive a server listening on localhost through DNS rebinding.
--origintakes a comma-separated list, or*. - The bearer token is compared in constant time and is required off loopback.
Mcp-Session-Idcomes back on the initialize response and is required on everything after it.DELETEends the session.GETreturns 405. Nothing here is long-running or streamed, so an idle stream would be a pretence.- Protocol
2025-06-18, with2025-03-26accepted. Anything else is a 400. - Request bodies are capped at 16MB.
The tools #
A command becomes a tool named brain_<command> with hyphens turned into underscores, so hook-stop is brain_hook_stop. That is 60 tools against 67 commands in the current build and the number tracks the CLI rather than a hand-written list.
Four commands are deliberately not tools. mcp and ui each start a server and block until it is killed, so an agent calling one has hung its own session. axon deals in this machine’s private key and also has a form that blocks. And hook-recall reads its payload from stdin, which over stdio is the JSON-RPC stream: calling it as a tool deadlocked the connection rather than failing. It is a hook the host fires on a real message and there is nothing there for an agent to call.
A tool call builds an argv and runs the ordinary dispatch, so guards, refusals and output cannot drift from the CLI. They are not reimplemented; they are the same code. The destructive ones (forget, prune, import, restore, absorb, vacuum, relabel --apply) are marked destructive in their annotations so a client can put a human in front of them and their own guards do not relax either way.
Three inputs exist on every tool and have no CLI equivalent
| Input | What it carries |
|---|---|
| args | Positional arguments, exactly as they would follow the command on a shell: a recall query, an id, a subcommand like add or list. |
| _project | The project this session is working in. |
| _session | Who is calling, as distinct from which project they are in. Memories record the session that stored them; without it every write through one server carries that server's id. It defaults to the MCP connection, so two anonymous clients are still told apart. The header form is X-Brain-Session. |
Everything else is the command's own flags. json: true returns the machine-readable envelope instead of the human rendering and for recall that matters: the envelope carries matched and a caller acting on results has to check it, because a miss returns nothing rather than the whole scope.
What MCP gives up #
The CLI is wired to session hooks. One fires before your first turn and puts the brief into context; another matches each prompt against the store and injects what it finds. Neither depends on the agent deciding to look.
MCP has no equivalent. It is pull only. The server hands the client an instructions block on connect, saying the brain is there and which six commands carry the work and after that reading it is the model's choice. That is a real difference in kind and it is the argument this project makes about memory generally: what depends on being remembered will eventually not be remembered.
So run both where you can. Hooks for arrival, MCP for reach. Drop the hooks only when the agent cannot run shell commands, or when the brain is on another machine and the CLI has nothing local to talk to.
Removing the hooks is one edit to .claude/settings.json: delete the entries whose command contains ns-brain. ns-brain uninstall does it properly, as a dry run first, along with everything else the install wrote.
When it does not work #
| What you see | What it is |
|---|---|
| 404 on every request | The endpoint is /mcp. The root serves nothing. |
| "this server fronts a shared brain" | No project declared. Set BRAIN_PROJECT or the header. |
| Memories under the wrong project | A project was declared and it was the wrong name. ns-brain relabel claims rows filed under another label. |
| memories=0 from a brain you know has some | Right server, wrong store. brain_stats prints the database path on its second line and a project on its own file is invisible to a server fronting the shared one. |
| "missing or unknown Mcp-Session-Id" | The client did not carry the session id back from initialize. |
| A brief with no git or host block | Deliberate. Serving a project that is not on this machine, the server's own tree and hostname say nothing true about you, so they are left out rather than reported with a straight face. |
| Refused to start on 0.0.0.0 | No token. Bind loopback, or pass --token and put TLS in front. |