Docs · MCP

Every command
is a tool.

You get every command, not a hand-picked few. The only ones left out start a server, handle a key or read stdin. We build each tool's inputs from the flags the command itself defines, so a new CLI flag shows up in MCP on the next build and the two never fall out of step. Point an agent at your brain. It reads and writes the same memory through the same code.

Hooks or MCP? #

An agent can reach a brain in two ways. Hooks make sure the agent starts every session already knowing what the project knows. Your client fires them, so the brief and the matching memories arrive whether or not the model thinks to look. MCP is how an agent reaches the brain when it cannot run shell commands. Both run the same code against the same memory. Choose by what your client can do.

Hooks and the CLIMCP
Who fires itYour client, at session start, on every prompt and at the end. Nothing relies on the model remembering.The model, when it decides to call a tool. The agent only gets what it asks for.
NeedsA client with hooks and a shell: Claude Code, Codex, Cursor, Gemini.A client that speaks MCP. That is most of them, including ones with no shell.
Context costThe brief once, then only what each prompt matches.Every tool's schema in every session, used or not.
FlagsWhatever the agent types. A wrong flag gets a refusal that tells it the fix.Typed inputs built from the same flags. The model cannot make one up.
Set up withns-brain init, which wires the hooks. Add --agent codex,cursor,gemini,opencode,kilo,copilot,devin,qwen,hermes,continue for the other clients.claude mcp add, or the same three settings in your client's own config file.

One machine, an agent with a shell: use hooks and nothing else. That is what the install page sets up and it covers most people.

An agent with no shell: MCP is the only choice. That means the Claude app, a hosted agent or an IDE that runs no hooks. Point it at the brain over HTTP and accept that the model now decides when to read.

Each project chooses for itself. Hooks live in the project's own .claude/settings.json. ns-brain init writes them there and nowhere else, so a project you never initialised has none. An MCP server added with -s local or -s project belongs to the directory you ran it in. Mix as you like: one project on hooks, another on MCP, a third on both. The only setting that spans projects is an MCP server added with -s user. That server cannot tell which project you are in, which is what naming the project covers.

Both at once: fine and sometimes the right call. A client with hooks that also speaks MCP gets the brief up front from the hooks and typed inputs from MCP. Just do not drop the hooks because MCP is connected. What you lose with MCP explains why.

Which transport to use #

stdio if the brain is on the same machine as the agent. Your client starts ns-brain mcp itself: no port, nothing to keep running, nothing to log in to. This is the usual case. Start here.

HTTP if your client cannot launch ns-brain mcp itself and the model has to reach the brain without a shell.

$ ns-brain mcp                              # stdio, launched by the client
$ ns-brain mcp --http 127.0.0.1:7801        # HTTP, endpoint is /mcp, bearer token required

The HTTP endpoint is /mcp, not the root. The root returns 404, so save yourself the round trip.

stdio with 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

Codex, Cursor and Gemini each take the same three things in their own config file: the command, the two environment variables and a name. The syntax differs. The content is the same.

Name the project or it refuses #

MCP does not tell the server which directory your client is in. When one brain holds several projects, the server cannot work out which one you mean. 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: billing-api, ops-runbooks, orchard, orchard-web
Use the name this project calls itself. A project new to this brain simply names itself, and its first write creates it.
A client can also declare it once in its own config, with an X-Brain-Project header, instead of passing it every call.

That refusal is what you want. Before it existed, the server used its own name and every project's memories ended up filed under that one name, with no warning. There are three ways to answer it. Prefer them in this order.

WhereHowWhen
Config, stdioBRAIN_PROJECT=yours in the server's envAlways, for stdio
Config, HTTPX-Brain-Project: yours headerAlways, for HTTP
Per call_project argument on any toolWhen you mean to reach another project

Put it in config. Config cannot forget and a model can. Set it once and you only pass _project when you want one connection to reach into another project on purpose. A brain with only one project needs none of this. It already knows which project it is.

HTTP, for a client with no shell #

# serve the brain
$ BRAIN_HOME=/srv/brains/yours ns-brain mcp \
      --http 127.0.0.1:7801 --token "$TOKEN"

# point the client at it
$ claude mcp add brain -s user -t http https://brain.example.com/mcp \
      -H "Authorization: Bearer $TOKEN" \
      -H "X-Brain-Project: yours"

The token can also come from NS_BRAIN_MCP_TOKEN in the environment. That keeps it off the command line, where any user on the machine can read it with ps. A token passed as a flag wins.

Bind to loopback and put nginx or Caddy in front with TLS. If the proxy passes its public name through as Host, name it with --allow-host brain.example.com. Any other Host is refused.

Or let the binary serve TLS itself, with no proxy:

# a PEM certificate for the name clients dial, and its key
$ BRAIN_HOME=/srv/brains/yours ns-brain mcp \
      --http 0.0.0.0:7801 --token "$TOKEN" \
      --tls-cert /etc/ns-brain/cert.pem --tls-key /etc/ns-brain/key.pem

The certificate can come from your own CA, from tailscale cert for a tailnet name or from an ACME client such as certbot. TLS 1.2 is the minimum. On any address other than loopback the binary will not start without both a certificate and a token. No flag overrides that. A firewall decides who can reach a port. It does nothing about who can read what passes through it.

A hosted client that cannot be given a bearer token (above all the Claude app on a phone) needs OAuth instead. --public-url turns it on and the phone page covers the whole setup.

What the server checks #

All of this comes from the security section of the MCP transport spec. None of it can be switched off.

The tools #

Each command becomes a tool called brain_<command>, with hyphens turned into underscores. So hook-stop becomes brain_hook_stop. The list is generated from the CLI, not written by hand, so a new command becomes a tool in the same build.

A few commands are left out on purpose. mcp, ui and channel each start a server and run until killed, so an agent that called one would hang its own session. release-sign reads a private key. verify-release is the installer's job. hook-recall, hook-tool and hook-end read their input from stdin. Over stdio, stdin is the channel MCP itself runs on, so calling one as a tool locked the connection up instead of failing. Your client fires a hook on a real message. There is nothing in it for an agent to call. hook-stop reads no stdin, so it stays a tool. Last, outstanding and status are other names for open. A model picking from a list does not need the same tool three times.

A tool call builds the same command line you would type and runs it through the normal CLI code. Guards, refusals and output are that same code, not a copy, so they cannot drift. Every tool that can remove or overwrite something (forget, prune, import, restore, absorb, vacuum, relabel, update, task and uninstall among them) is marked destructive in its annotations. It is marked by its worst case even when the default is a dry run, so a client can ask a human first. Its own guards stay just as strict either way.

Some calls are refused over MCP whatever you pass, because a person should make them at a terminal: axiom and standard add, edit or rm, release, wakeup trust, dm --accept and backup --whole-store. relabel --claim and upgrade --allow-downgrade are not in the schemas at all. A body cannot come from stdin over MCP either: pass the text itself. Each refusal names the command for the person to run.

Every tool has three extra inputs the CLI does not

InputWhat it carries
argsPositional arguments, exactly as they would follow the command in a shell: a recall query, an id, a subcommand such as add or list.
_projectThe project this session is working in.
_sessionWho is calling, separate from which project they are in. Each memory records the session that saved it. Without this, every write through one server carries that server's id. It defaults to the MCP connection, so two unnamed clients are still told apart. The header version is X-Brain-Session.

Everything else is the command's own flags. json: true returns the machine-readable envelope instead of the text meant for people. For recall that matters. A miss returns nothing instead of the whole scope, so anything acting on the results has to check matched in the envelope first.

What you lose with MCP #

The CLI is wired to session hooks. One fires before your first turn and puts the brief in front of the agent. Another matches each prompt against the brain and adds what it finds. Neither waits for the agent to decide to look.

MCP has nothing like that. The agent only gets what it asks for. When a client connects, the server hands it a short set of instructions: read the brain before searching files, write to it before finishing, check matched on a recall. It also says where this brain is. After that, the model decides whether to read it. That is a real difference. It is also our whole argument about memory: anything that relies on being remembered will one day be forgotten.

So use both where you can: hooks for the brief, MCP for reach. Only drop the hooks when the agent cannot run shell commands.

Removing the hooks is one edit to .claude/settings.json: delete the entries whose command contains ns-brain. ns-brain uninstall does it properly. It shows a dry run first and also removes everything else the install wrote.

When it does not work #

What you seeWhat it means
404 on every requestThe endpoint is /mcp. Nothing is served at the root.
"this server fronts a shared brain"No project set. Set BRAIN_PROJECT or the header.
Memories under the wrong projectA project was set, but with the wrong name. ns-brain relabel claims rows filed under another label.
memories=0 from a brain you know has someRight server, wrong store. brain_stats prints the database path on its second line. A project with its own file is invisible to a server running on the shared one.
"missing Mcp-Session-Id"The client did not send back the session id it got from initialize.
404 "session not found; re-initialize"The server restarted and lost its sessions. A client that follows the spec initializes again by itself.
"this server cannot tell which brain you mean" from ClineCline starts every MCP server from one shared process, so the working directory points at the wrong project. Set BRAIN_HOME (and BRAIN_PROJECT on a shared store) in the server's env.
Refused to start on 0.0.0.0No certificate or no token. Pass --tls-cert, --tls-key and --token, or bind loopback behind a proxy that handles TLS.
429 on every requestToo many wrong tokens from this address. Fix the token and wait for the time in Retry-After.

More of these, including the CLI side →